Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PROPOSAL] Python Plugin System #891

Closed
3 tasks done
fpliger opened this issue Oct 26, 2022 · 13 comments
Closed
3 tasks done

[PROPOSAL] Python Plugin System #891

fpliger opened this issue Oct 26, 2022 · 13 comments
Labels
tag: plugins Related to the infrastructure of how plugins work, or to specific plugins. type: feature New feature or request

Comments

@fpliger
Copy link
Contributor

fpliger commented Oct 26, 2022

Checklist

  • I added a descriptive title
  • I searched for other feature requests and couldn't find a duplicate (including also the type-feature tag)
  • I confirmed that it's not related to another project area (see the above section)

What is the idea?

This is not a new topic but I'm opening a new issue to start a clean and focused discussion on the topic, considering that we already had a lot of preliminary discussions in #763 and #642 (in addition to a few experiments by many of the maintainers...) I'd like to have a clean place for us to converge on a proposal (and we can eventually close all the related issues once we are done).

I'll try to summarize where I think we left and what were the main items we agreed on (anyone, please, correct me here if you don't think it's right).

For starters, I believe that today we are in a much better place to actually converge on a [Python] Plugins design. In fact, in #763 @antocuni mentions a few steps toward that path:

My reasoning was the following:

  1. the current workflow is a mess and needs to be refactored
  2. I designed in my head a possible internal refactoring
  3. while thinking of (2), I thought it could be the basis for the JS-based plugin system
  4. finally, once we have a clear execution flow, we can easily add a hook to execute python plugins.

and I'd claim that 1, 2 and 3 basically happened already or are very close.

So, with all that said...

Proposal

General Characteristics

  • I'm referring to Plugin as any code that is registered and extends the feature of PyScript by hooking into specific lifecycle events. These can either be plugins that create new Web Components or not.
  • The initial implementation of Python Plugins will focus mainly on Plugins that add a new Web Component. Other minor use cases may surface but will not be the main focus of the first iteration
  • NOTE: The reason for that is also that when the Python runtime is ready, it's already to late to hook into many of the execution lifecycle events that happen in JS before Python is ready.
  • Python Plugins authored by users should be written in Python, as python modules (that means a normal python module that is imported and executed at a very well defined time during the startup)
  • These can be installed by using the <py-config> configuration by using a dedicated plugins section
  • It will [likely] also be possible that plugins can be shipped as python packages that can be specified as a dependency in the packages section of the config (My proposal is that we support that but do not promote it as the primary method)
  • A Plugin is just a set of Python code that is loaded by the main pyscript logic and contains specific functions which will be called at appropriate times by using the hooks
  • A plugin can define 0 to many components. Each one is registered separately. E.g. a hypotetical pyGui plugin could define py-inputbox, py-button, py-title, etc. But they are all in the same plugin
  • At this point, Plugins are supported only at the PyScript on the browser level. That said, I'd really like to keep the door open and explore extending that concept to the server side also (in the next iterations). For instance on the pyscript-cli or as an extension for other major web frameworks so that, for instance, server side rendering of Custom Elements via Plugin can also be achieved.

Higher level API (Python)

Was thinking we might support plugins via a few ways... loosely something like this:

or a class decorator

from datetime import datetime as dt
from pyscript import plugins
from js import document

@plugins.register_plugin(tag='my-hello', attributes=["name"] )
class MyHello:
    name: str

    def connect(self):
        div = document.createElement('div')
        div.innerHTML = "<b>Hello world, I'm a custom widget</b>"
        return div

    def click(self, evt=None):
        print ("I've been clicked!")

or simple functions decorator:

from datetime import datetime as dt
from pyscript import plugins
from js import document

plugin = plugins.create_plugin('MyHello', tag='my-hello', attributes=['name'])

@plugin.register
def connect(name):
        div = document.createElement('div')
        div.innerHTML = f"<b>Hello {name}, I'm a custom widget</b>"
        return div

@plugin.register
def click(self, evt=None):
        print ("I've been clicked!")

or what's in #642 (honestly that's my least favorite...)

from datetime import datetime as dt
from pyscript import plugins
from js import document

class MyHello:
    __tag__ = 'my-hello'

    def __init__(self, parent):
        self.parent = parent

    def connect(self):
        div = document.createElement('div')
        div.innerHTML = "<b>Hello world, I'm a custom widget</b>"
        self.parent.shadow.appendChild(div)

    def click(self, evt=None):
        print ("I've been clicked!")

# this is automatically called by <py-plugin>
def pyscript_init_plugin():
    plugins.register_custom_widget(MyHello)
  • Attributes values that were specified in the HTML tag are passed to the plugin via keyword
  • Events are explicitly defined on the plugin class or registered via function decorator
  • Notice a small change in some of the plugins methods compared to the previous discussion we had, like connect for instance, in that it returns what should be appended to the parent instead of actually manipulating the DOM (since it can have implications if we actually execute in web workers). Noticed that in @pauleveritt experiments and think it plays really well here (and is also more promising when we support execution on web workers)

Finally, there are a couple of things I intentionally left out and open:

  • naming: None of the names of modules and functions/decorators above are really strongly opinionated. Open for proposals
  • Scope of a Plutin: should we logically separate Web Components plugins vs Plugins that hook into other execution events that are not tied to a Web Component? For instance, a plugin that handles execution errors (that doesn't need a Web Component) . Do we allow it to be in the same class/registry of a Web Component in the same Plugin or do force separation (for instance, having 2 registration methods: register_component and register_<whatever_name_we_want_to_call_the_other_type_of_plugins>)?

I am probably am missing a bunch of things but seems enough to start a discussion (in hopes we can converge :) )

Why is this needed

The current plugin system is very hacky and unstable

What should happen?

We should converge on a Plugins implementation proposal

Additional Context

No response

@fpliger fpliger added type: feature New feature or request needs-triage Issue needs triage labels Oct 26, 2022
@antocuni
Copy link
Contributor

  1. the current workflow is a mess and needs to be refactored
  2. I designed in my head a possible internal refactoring
  3. while thinking of (2), I thought it could be the basis for the JS-based plugin system
  4. finally, once we have a clear execution flow, we can easily add a hook to execute python plugins.

and I'd claim that 1, 2 and 3 basically happened already or are very close.

I agree.

  • I'm referring to Plugin as any code that is registered and extends the feature of PyScript by hooking into specific lifecycle events. These can either be plugins that create new Web Components or not.
  • The initial implementation of Python Plugins will focus mainly on Plugins that add a new Web Component. Other minor use cases may surface but will not be the main focus of the first iteration
  • NOTE: The reason for that is also that when the Python runtime is ready, it's already to late to hook into many of the execution lifecycle events that happen in JS before Python is ready.
  • Python Plugins authored by users should be written in Python, as python modules (that means a normal python module that is imported and executed at a very well defined time during the startup)

+1 to all of these.

  • These can be installed by using the <py-config> configuration by using a dedicated plugins section
  • It will [likely] also be possible that plugins can be shipped as python packages that can be specified as a dependency in the packages section of the config (My proposal is that we support that but do not promote it as the primary method)

+1. I guess that for now we will allow only "single file" pure-python plugins, so that the config would look like this:

<py-config>
plugins = ["http://example.com/mygui.py"]
</py-config>

and the semantics is that during the startup we will download the file, put it into a directory which is inside sys.path, import it and run the hooks.
Then later we can think of more advanced ways to publish/deploy/download/search plugins, as well as versioning. E.g. I can imagine that eventually we might want something like this:

plugins = ['github:antocuni/mygui@1.0.0']  # or something like that

but that's orthogonal. Once we have a solid way to execute plugins, we can think of ways to discover/install them.

  • A Plugin is just a set of Python code that is loaded by the main pyscript logic and contains specific functions which will be called at appropriate times by using the hooks
  • A plugin can define 0 to many components. Each one is registered separately. E.g. a hypotetical pyGui plugin could define py-inputbox, py-button, py-title, etc. But they are all in the same plugin

+1

  • At this point, Plugins are supported only at the PyScript on the browser level. That said, I'd really like to keep the door open and explore extending that concept to the server side also (in the next iterations). For instance on the pyscript-cli or as an extension for other major web frameworks so that, for instance, server side rendering of Custom Elements via Plugin can also be achieved.

+1

Higher level API (Python)

Was thinking we might support plugins via a few ways... loosely something like this:

or a class decorator

from datetime import datetime as dt
from pyscript import plugins
from js import document

@plugins.register_plugin(tag='my-hello', attributes=["name"] )
class MyHello:
    name: str

    def connect(self):
        div = document.createElement('div')
        div.innerHTML = "<b>Hello world, I'm a custom widget</b>"
        return div

    def click(self, evt=None):
        print ("I've been clicked!")

I'm confused about the terminology here. Assuming that the file above is called mygui.py, and according to what we agreed earlier, MyHello is a component, not a plugin. The plugin is mygui.py.
So it should be @plugins.register_component, shouldn't it?

or simple functions decorator:

from datetime import datetime as dt
from pyscript import plugins
from js import document

plugin = plugins.create_plugin('MyHello', tag='my-hello', attributes=['name'])

@plugin.register
def connect(name):
        div = document.createElement('div')
        div.innerHTML = f"<b>Hello {name}, I'm a custom widget</b>"
        return div

@plugin.register
def click(self, evt=None):
        print ("I've been clicked!")

+1 for having simple functions which just returns HTML and/or an HTMLElement.
-1 for making it possible to attach complex behavior. If you want complex behavior, you use a class.

or what's in #642 (honestly that's my least favorite...)

from datetime import datetime as dt
from pyscript import plugins
from js import document

class MyHello:
    __tag__ = 'my-hello'

    def __init__(self, parent):
        self.parent = parent

    def connect(self):
        div = document.createElement('div')
        div.innerHTML = "<b>Hello world, I'm a custom widget</b>"
        self.parent.shadow.appendChild(div)

    def click(self, evt=None):
        print ("I've been clicked!")

# this is automatically called by <py-plugin>
def pyscript_init_plugin():
    plugins.register_custom_widget(MyHello)

I'm not particularly attached to this approach, but just to understand better: what is it that you don't like? The __tag__, the fact that it doesn't use a decorator, or the fact that the logic is inside a function pyscript_init_plugin() which is automatically called?

  • Attributes values that were specified in the HTML tag are passed to the plugin via keyword

+1

  • Events are explicitly defined on the plugin class or registered via function decorator

-0.5. It might work for simple stuff, but it quickly breaks as soon as you have multiple sub-nodes in your widget. Which of them respond to the click event?

Finally, there are a couple of things I intentionally left out and open:

  • naming: None of the names of modules and functions/decorators above are really strongly opinionated. Open for proposals

yes, happy to leave naming discussions for another time, apart the plugin/component distinction.

  • Scope of a Plutin: should we logically separate Web Components plugins vs Plugins that hook into other execution events that are not tied to a Web Component? For instance, a plugin that handles execution errors (that doesn't need a Web Component) . Do we allow it to be in the same class/registry of a Web Component in the same Plugin or do force separation (for instance, having 2 registration methods: register_component and register_<whatever_name_we_want_to_call_the_other_type_of_plugins>)?

no: I can imagine having plugins which want to do both (e.g. a "terminal" plugin which intercepts stdin/stdout and also creates a <py-terminal> component).

I am probably am missing a bunch of things but seems enough to start a discussion (in hopes we can converge :) )

I think we are not too far from converging, but there is another open question we should talk about.
What is the entry point of plugins?

In your first two examples, the component-registration logic happen at import time.
In my example, it happens when pyscript_init_plugin is called.
The module-level approach is admittedly easier to write, but the pyscript_plugin_init() approach is more flexible because it gives us more flexibility on when to execute things.
For example, in the future we might need to have a hook which is called before the components are registered (for whatever reason).
Or another very concrete example is priority: I can imagine that plugins might want to declare a "priority" so that they can ask to be executed very early or very late: in order to know the priority you need to import the module, but importing the module causes the registration logic to execute.

I think I have a solution which might represent the best of both worlds:

# mygui.py
from pyscript import Plugin
plugin = Plugin('mygui', priority=1)  # priority is optional, of course

@plugin.init
def init():
    print('this is executed as soon as the runtime is ready')

@plugin.register_component('my-button')
class MyButton:
    ...

@plugin.register_component('my-inputbox')
class MyInputBox:
   ...

# hypotetical hook
@plugin.afterExecution
def afterExecution():
    print('this is called e.g. after the execution of <py-script> blocks')

@plugin.another_hook
def another_hook():
    ...

Note the subtle but important difference: all the registration logic is done with the plugin instance, which is local to the module instead of being a global which is shared among all plugins.
And the decorators will just "remember" the various hooks, which can be then executed by main.ts in the right order at the right time.

This is basically the same pattern which is used by Flask blueprints: the blueprint decorators are very similar to the @app decorator, but basically delay the actual instantiation to a later point.

@fpliger
Copy link
Contributor Author

fpliger commented Oct 27, 2022

Higher level API (Python)
Was thinking we might support plugins via a few ways... loosely something like this:

or a class decorator

from datetime import datetime as dt
from pyscript import plugins
from js import document

@plugins.register_plugin(tag='my-hello', attributes=["name"] )
class MyHello:
name: str

def connect(self):
    div = document.createElement('div')
    div.innerHTML = "<b>Hello world, I'm a custom widget</b>"
    return div

def click(self, evt=None):
    print ("I've been clicked!")

I'm confused about the terminology here. Assuming that the file above is called mygui.py, and according to what we agreed earlier, MyHello is a component, not a plugin. The plugin is mygui.py.
So it should be @plugins.register_component, shouldn't it?

Yeah, I can see the source of confusion here, and do think we have to agree on terminology.

I've been using Plugin to refer both to the "thing" (let's say a python file in this first iteration) users add the path to in their config, that we download and run and the inner piece of code they need to register. If you take Pluggy, for instance, when your PlugginManager registers a plugin (basically something decorated with @hookimpl), you are registering them as Plugins at the code level, not file/module level. They have some glossary defined here and this example can be also useful.

I'm not too attached to terminology here but do think that component can be overloaded in this context. Do you mean component because the plugin creates a new Web Component of because that piece of code we are register is a component of the plugin itself? What if we are registering a plugin (using this word again for the lack of better options in my mind) that does not create a new Web Component?

or simple functions decorator:


from datetime import datetime as dt
from pyscript import plugins
from js import document

plugin = plugins.create_plugin('MyHello', tag='my-hello', attributes=['name'])

@plugin.register
def connect(name):
        div = document.createElement('div')
        div.innerHTML = f"<b>Hello {name}, I'm a custom widget</b>"
        return div

@plugin.register
def click(self, evt=None):
        print ("I've been clicked!")

+1 for having simple functions which just returns HTML and/or an HTMLElement.
-1 for making it possible to attach complex behavior. If you want complex behavior, you use a class.

That's fair and I'm +1, if we can define "complex behavior"

or what's in #642 (honestly that's my least favorite...)

from datetime import datetime as dt
from pyscript import plugins
from js import document

class MyHello:
    __tag__ = 'my-hello'

    def __init__(self, parent):
        self.parent = parent

    def connect(self):
        div = document.createElement('div')
        div.innerHTML = "<b>Hello world, I'm a custom widget</b>"
        self.parent.shadow.appendChild(div)

    def click(self, evt=None):
        print ("I've been clicked!")

# this is automatically called by <py-plugin>
def pyscript_init_plugin():
    plugins.register_custom_widget(MyHello)

I'm not particularly attached to this approach, but just to understand better: what is it that you don't like? The tag, the fact that it doesn't use a decorator, or the fact that the logic is inside a function pyscript_init_plugin() which is automatically called?

Great question. I think it's a combination of factors. Yeah, the above contributes to it and makes it have a more verbose/less clean approach to it but, the fact that users have to know that they need to define attributes like __tag__ at the classes level makes it worst to document and make straightforward. With a decorator, it seems easier and more immediate to me as it's part of the decorator's docstrings as well, and more explicit.

Events are explicitly defined on the plugin class or registered via function decorator
-0.5. It might work for simple stuff, but it quickly breaks as soon as you have multiple sub-nodes in your widget. Which of them respond to the click event?

Could you help me understand, with an example?

In your first two examples, the component-registration logic happen at import time.
In my example, it happens when pyscript_init_plugin is called.

That's most true as it is... but nothing prevent us to have the decorator itself to stage all plugins that need registration and for us to make it happen whenever it's more convenient. Not saying it's great or best option but that it's a possibility. (Overall, I'd like to keep it simple whenever possible tbh)

This is basically the same pattern which is used by Flask blueprints: the blueprint decorators are very similar to the @app decorator, but basically delay the actual instantiation to a later point.

(not focusing on the decorator names) I like the approach 👍

@fpliger fpliger removed the needs-triage Issue needs triage label Oct 27, 2022
@antocuni
Copy link
Contributor

Let me answer this first:

I'm not too attached to terminology here but do think that component can be overloaded in this context. Do you mean component because the plugin creates a new Web Component of because that piece of code we are register is a component of the plugin itself?

I'm using "component" because of web components: in the example above, my-button ends up being implemented as a Web Component/Custom Element, so we should just call it with its name.

I was curious to understand the difference between "Web Component" and "Custom Element": according to this answer:

  • py-script, py-repl, my-button, etc. are Custom Elements
  • "Web component" is just the name of the part of the standard which defines custom elements, shadow roots, templates, etc.

So, we should just call them custom elements, and use decorators such as @register_custom_element or similar.

Maybe @tedpatrick has opinions here.

Yeah, I can see the source of confusion here, and do think we have to agree on terminology.

I've been using Plugin to refer both to the "thing" (let's say a python file in this first iteration) users add the path to in their config, that we download and run and the inner piece of code they need to register. If you take Pluggy, for instance, when your PlugginManager registers a plugin (basically something decorated with @hookimpl), you are registering them as Plugins at the code level, not file/module level. They have some glossary defined here and this example can be also useful.

I think that we are basically aligned here and it's just a matter of terminology. That said, it's better to settle at least the basic terminology to avoid further confusion.

In pluggy, a "plugin" is a namespace which contains various functions will well-known names. The first pluggy examples shows that this namespace can also be the one of a class, but in the vast majority of real-world cases, the namespace is the one of the module and the hookimpl functions are at module-level.
I'm fine to implement the same system if we feel the need. But for the sake of the simplicity, in this discussion let's keep the equivalence "1 module == 1 plugin" which is easier to reason about.

That said, a single plugin/module can register many "things" of course, where each "thing" can be a hook, a custom element, or some other things which will be added in the future (random-ideas-which-are-not-necessarily-good: filesystem providers, error handlers, themes, etc.).

Let me write again my latest example:

# mygui.py
from pyscript import Plugin
plugin = Plugin('mygui', priority=1)  # priority is optional, of course

@plugin.init
def init():
    print('this is executed as soon as the runtime is ready')

# maybe just @plugin.custom_element('my-button') ?
@plugin.register_custom_element('my-button')
class MyButton:
    ...

@plugin.register_custom_element('my-inputbox')
class MyInputBox:
   ...

@plugin.register_filesystem_provider
class GithubProvider:
    ...

In this example:

  • mygui is the plugin (implemented in the namespace of the mygui.py module)
  • mygui registers/implements the init hook
  • mygui registers the MyButton custom element
  • mygui registers the MyInputBox custom element
  • mygui registers a hypotetical filesystem provider which can fetches files from github

Are we on the same page here?

What if we are registering a plugin (using this word again for the lack of better options in my mind) that does not create a new Web Component?

that's perfectly fine. In the example above, it's perfectly reasonable to have a github plugin which just registers the GitHubProvider and nothing else.

+1 for having simple functions which just returns HTML and/or an HTMLElement.
-1 for making it possible to attach complex behavior. If you want complex behavior, you use a class.

That's fair and I'm +1, if we can define "complex behavior"

Let's define "simple behavior" instead: you can implement custom elements with just a single function which returns HTML and/or a Node. That's it.

Events are explicitly defined on the plugin class or registered via function decorator
-0.5. It might work for simple stuff, but it quickly breaks as soon as you have multiple sub-nodes in your widget. Which of them respond to the click event?

Could you help me understand, with an example?

Sure:

@plugin.register_custom_element('my-dialog-box', attributes="message")
class MyDialogBlox:
    message: str
    def connect(self):
        return f"""
            <div class="message">{self.message}</div>
            <button>OK</button>
            <button>Cancel</button>
        """

    def click(self):
        """ click on WHAT? """

That's most true as it is... but nothing prevent us to have the decorator itself to stage all plugins that need registration and for us to make it happen whenever it's more convenient. Not saying it's great or best option but that it's a possibility. (Overall, I'd like to keep it simple whenever possible tbh)
[cut]
(not focusing on the decorator names) I like the approach +1

I think that the approach plugin = Plugin('mygui') solves the problem, so if you are ok with it I'm also ok.

@tedpatrick
Copy link
Contributor

tedpatrick commented Oct 27, 2022

"A Plugin is just a set of Python code that is loaded by the main pyscript logic and contains specific functions which will be called at appropriate times by using the hooks" - I rather love this definition. 🍔

Here is the JS to define a Custom Element.

class AutoButton extends HTMLElement {}
customElements.define("auto-button", AutoButton);

I think we should align around Web Standards naming if we are performing identical actions within plugins.

'Plugin' naming is growing on me. 🧱

@antocuni
Copy link
Contributor

"A Plugin is just a set of Python code that is loaded by the main pyscript logic and contains specific functions which will be called at appropriate times by using the hooks" - I rather love this definition. hamburger

A Python Plugin is a set of Python code etc. etc.
We will also have plugins written in JS but yes, I agree on the definition.

Here is the JS to define a Custom Element.

class AutoButton extends HTMLElement {}
customElements.define("auto-button", AutoButton);

I think we should align around Web Standards naming if we are performing identical actions within plugins.

definitely agree.

@fpliger
Copy link
Contributor Author

fpliger commented Oct 28, 2022

So, we should just call them custom elements, and use decorators such as @register_custom_element or similar.

Definitely like it better than component. +1

In pluggy, a "plugin" is a namespace which contains various functions will well-known names. The first pluggy examples shows that this namespace can also be the one of a class, but in the vast majority of real-world cases, the namespace is the one of the module and the hookimpl functions are at module-level.
I'm fine to implement the same system if we feel the need. But for the sake of the simplicity, in this discussion let's keep the equivalence "1 module == 1 plugin" which is easier to reason about.

That said, a single plugin/module can register many "things" of course, where each "thing" can be a hook, a custom element, or some other things which will be added in the future (random-ideas-which-are-not-necessarily-good: filesystem providers, error handlers, themes, etc.).

Yes! That capture what I was trying to convey... "A Plugin is a namespace which contains various functions will well-known names." That namespace can be a module or a class.. doesn't matter.

+1 on starting with 1 module = 1 plugin [that can register many "things"].

In this example:

mygui is the plugin (implemented in the namespace of the mygui.py module)
mygui registers/implements the init hook
mygui registers the MyButton custom element
mygui registers the MyInputBox custom element
mygui registers a hypotetical filesystem provider which can fetches files from github

Are we on the same page here?

Yes!

+1 for having simple functions which just returns HTML and/or an HTMLElement.
-1 for making it possible to attach complex behavior. If you want complex behavior, you use a class.

That's fair and I'm +1, if we can define "complex behavior"

Let's define "simple behavior" instead: you can implement custom elements with just a single function which returns HTML and/or a Node. That's it.

Ok. Anything more than this goes under complex behaviour?

Not sure it'd be that useful if we are limiting users to just that.... maybe. Need some time to digest :)

Sure:

@plugin.register_custom_element('my-dialog-box', attributes="message")
class MyDialogBlox:
    message: str
    def connect(self):
        return f"""
            <div class="message">{self.message}</div>
            <button>OK</button>
            <button>Cancel</button>
        """

    def click(self):
        """ click on WHAT? """

I fail to see it as "controversial" (and maybe it's on me) but if you attach a click to an object I expect it to represent a click on that object and not on its children. So click would be on the entire my-dialog-box element.

I can see how it'd be convenient to find an API that supports defining click events for the Custom Element children but I'd be -10 on trying to do it in the first implementation of Plugins. If users want that, they can create a function and explicitly attach it to events on specific elements.

I think that the approach plugin = Plugin('mygui') solves the problem, so if you are ok with it I'm also ok.

That's it? We are just going to agree like that?

Ok, fine.... deal, let's both be ok with it! 👍 😃

"A Plugin is just a set of Python code that is loaded by the main pyscript logic and contains specific functions which will be called at appropriate times by using the hooks" - I rather love this definition. hamburger

A Python Plugin is a set of Python code etc. etc.
We will also have plugins written in JS but yes, I agree on the definition.

+1 on something like the above

@antocuni
Copy link
Contributor

antocuni commented Oct 30, 2022

Wow, lots of things we agree on, I'm not used to it 😂

Let me answer only to the last remaining open point:

Let's define "simple behavior" instead: you can implement custom elements with just a single function which returns HTML and/or a Node. That's it.

Ok. Anything more than this goes under complex behaviour?

Not sure it'd be that useful if we are limiting users to just that.... maybe. Need some time to digest :)
[cut]
I fail to see it as "controversial" (and maybe it's on me) but if you attach a click to an object I expect it to represent a click on that object and not on its children. So click would be on the entire my-dialog-box element.

yes exactly, I'd also expect that. But I also expect that most custom elements will be composed of many DOM nodes, so a click event which responds on the whole custom element is probably not very useful. Let me quote again your original example:

plugin = plugins.create_plugin('MyHello', tag='my-hello', attributes=['name'])

@plugin.register
def connect(name):
        div = document.createElement('div')
        div.innerHTML = f"<b>Hello {name}, I'm a custom widget</b>"
        return div

@plugin.register
def click(self, evt=None):
        print ("I've been clicked!")

Basically, I claim that there are very few cases in which the click event is useful, unless you are implementing a button or button-like thing.

I have a counter-proposal: let's start humble and don't add features which might be not needed.
In the first iteration, custom elements will be defined with just a function/class which responds to the connected event and return a node, and you will have to connect events and event handlers manually. This is basically the equivalent of what you have to do in JS.

Then we start writing plugins for real, and we will see what are the convenience features that we need.

So, in the first iteration the example above could be something like this:

plugin = Plugin('mygui')

@plugin.register_custom_element('my-hello', attributes=['name'])
def connected(name):
        div = document.createElement('div')
        div.innerHTML = f"<b>Hello {name}, I'm a custom widget</b>"
        div.addEventListener('click', create_proxy(on_my_hello_click))
        return div

def on_my_hello_click(evt):
        print ("I've been clicked!")

(A note about addEventListener: we surely need a better way to do it, see e.g. #835 and #905, but that's not the point of this discussion).

Also, about register_custom_element: I just realized that we could put the tag name into angular brackets, which IMHO it's much clearer:

@plugin.register_custom_element('<my-hello>', attributes=['name'])
...

@fpliger
Copy link
Contributor Author

fpliger commented Nov 1, 2022

Yeah. I think it's a good starting point and nothing prevents us to add it soon after. +1 on actually working on a better API to interact with the DOM and nodes (and add addEventListener in a more Pythonic way). That actually might be a good thing to do before we add this on the plugin itself

@antocuni
Copy link
Contributor

antocuni commented Nov 1, 2022

I AGREE 😂

@fpliger
Copy link
Contributor Author

fpliger commented Nov 7, 2022

Ok... as I develop this I realize we didn't talk too much in details about the API for plugins [class] to access the Custom Element (CE) on the page and all it's attributes.

In the old extensions that we currently have in main, the CE is injected through the __init__ as the parent attribute. That was decided out of conveniency (and lack of time) because just passing the object avoids API design decisions and just gives full freedom to the plugin creator.

We could do this but I'd like to give the Plugin authors an API that doesn't have [any perception] of the separation between Python/Web worlds. Basically, Python Plugins API should really make the authors feel like their Python code is the Custom Element.

To step back a little, the main problem we are trying to solve here is:

  • How does a Plugin access the CE "code" inlined in it's tags? (We've been calling it code but really is the element innerHTML... Not sure what is the best nomenclature but we should take an effort to redefined it). Basically access to:
<my-plugin>
this is the inlined
code/content that I want to access
</my-plugin>
  • How does a Plugin access it's attributes passed on to the CE as attributes: `

  • How should Plugins the <my-plugin></my-plugin> element on the page? I.e., do we offer access though the API or we don't at all and let users access through it's id and queries?

.. and we should be intentional about how we solve for that in the API.

@antocuni
Copy link
Contributor

antocuni commented Nov 7, 2022

Ok... as I develop this I realize we didn't talk too much in details about the API for plugins [class] to access the Custom Element (CE) on the page and all it's attributes.

this is a broad question, and I think it's very related to introducing a "Pythonic API" to access/modify the DOM (let's call it pydom for now). Once we have pydom, it's very likely that we want plugins to use it and/or to expose an API which is consistent with it.

We could do this but I'd like to give the Plugin authors an API that doesn't have [any perception] of the separation between Python/Web worlds. Basically, Python Plugins API should really make the authors feel like their Python code is the Custom Element.

I'm not really sure to understand what you mean here.

To step back a little, the main problem we are trying to solve here is:

  • How does a Plugin access the CE "code" inlined in it's tags? (We've been calling it code but really is the element innerHTML... Not sure what is the best nomenclature but we should take an effort to redefined it). Basically access to:
    [cut]

see above, this seems to be related to the hypotetical pydom. As a very first iteration, I would just expose an attribute element which contains the relevant HTMLElement (what was called parent before, if I understand correctly).
So, self.element.innerHTML, self.element.getAttribute, etc.
But I agree that in the long run we need a more pythonic way.

  • How should Plugins the <my-plugin></my-plugin> element on the page? I.e., do we offer access though the API or we don't at all and let users access through it's id and queries?

in my proposal above, you would access it using self.element, if I understand correctly the question

@JeffersGlass JeffersGlass added the tag: plugins Related to the infrastructure of how plugins work, or to specific plugins. label Nov 13, 2022
@marimeireles
Copy link
Member

@fpliger is this issue still relevant or did we implemented everything that was needed?

@fpliger
Copy link
Contributor Author

fpliger commented Feb 14, 2023

Closing with the same comment I had for #763 : I think we covered [most of] it and changes should probably come up from a new and cleaner issue (also even just to keep things small and understandable).... Feel free to reopen if you disagree

@fpliger fpliger closed this as completed Feb 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
tag: plugins Related to the infrastructure of how plugins work, or to specific plugins. type: feature New feature or request
Projects
Archived in project
Development

No branches or pull requests

5 participants