Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@mEp3ii2
Copy link
Contributor

@mEp3ii2 mEp3ii2 commented Apr 18, 2025

Improve event binding across web back-end by replacing the old create_proxy() with the pyodide.ffi.wrappers.add_event_listener(), this has been done in the libs module.
This affects any web widgets using DOM events such as blur, focus, change etc
Changes has been made to the passwordinput widget as well as the window module to make use of this change

New usage is now
create_proxy(dom_element,"event-type",python_handler)
e.g.
create_proxy(self.native,"sl-blur",self.dom_onblur)

Resolves issues related to garbage-collected borrowed proxies that show in the console.

Background
Brought to my attention in my previous pull request

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix you've provided here looks like it resolves the issue; however, I'm not completely convinced it's the right approach.

Taking the solution "as is" - I'm not wild about the name change. You've taken a method called addEventListener and rebound it to the name create_proxy - which is a method name that also exists in the pyscript.ffi namespace. That means create_proxy() is (a) no longer doing what the built-in create_proxy method does, and (b) doesn't accurately describe what it does do, either. If we were going down this path, I'd be inclined to make a utility add_event_listener() method (possibly as member method on the widget class).

However, I'm not completely convinced this is needed. The ffi methods you're using here are low level pyodide methods; and one of the goals of PyScript is to provide a higher-level API that abstracts those methods and details behind a more Pythonic API.

To that end - Pyscript even provides a wrapper around create_proxy - but immediately advises that it shouldn't ever be needed if you're using the pyscript API as intended.

In particiular, the pyscript.web.* APIs (additional user guide here) provide a way to create any DOM element, and presumably it also "does the right thing" when it comes to attaching handlers - that would seem to be the intention of the when() method/decorator.

So - can we avoid the need to call pyodide internal APIs entirely by using the pyscript API exclusively? Can we replace Widget._create_dom_element with a native Pyscript call?

Briefcase is currently using PyScript 2024.11.1; if we need to switch to a newer version of PyScript to get bug fixes and updates, then that's absolutely something we can and should do. This is also something that would fit into the bigger piece of work of making Briefcase indepdendent of the specific needs of Toga - that is, the Pyscript and Shoelace version dependencies are dependencies of Toga, and should be defined there, not defined as part of Briefcase.

One other detail - when you're proposing a change that is purely code, and doesn't contain a change to tests, it's worth flagging how you've tested this yourself. If the code has tests (or a pre-existing, but failing test), then the "yes, this works" criteria is obvious; but when it's just code change, it's not necessarily obvious how you've established that code is working.

In this case, my go-to was the passwordinput example in the examples folder... and it won't work because (a) it's currently missing a travertino dependency, and (b) the dom_onchange implementation on PasswordInput is currently passing in a None argument whenever it is fired. You might not have hit the former problem (addressed in #3346) if you rolled your own test case - but I'm not sure how you could have tested any change event handling on PasswordInput and not hit the second problem.

mEp3ii2 added 2 commits April 20, 2025 20:50
Grabbing changes from upstream
@mEp3ii2
Copy link
Contributor Author

mEp3ii2 commented Apr 22, 2025

Briefcase is currently using PyScript 2024.11.1; if we need to switch to a newer version of PyScript to get bug fixes and updates, then that's absolutely something we can and should do. This is also something that would fit into the bigger piece of work of making Briefcase indepdendent of the specific needs of Toga - that is, the Pyscript and Shoelace version dependencies are dependencies of Toga, and should be defined there, not defined as part of Briefcase.

How would you suggest going about this. I'm assuming it would require coordinating PRs across both Toga and Briefcase to achieve this. Just want to make sure that I've understood this correctly. Also any tips or good to know things before i start would be greatly appreciated

@freakboy3742
Copy link
Member

How would you suggest going about this. I'm assuming it would require coordinating PRs across both Toga and Briefcase to achieve this. Just want to make sure that I've understood this correctly. Also any tips or good to know things before i start would be greatly appreciated

When Toga uses Briefcase, it's always using the "development" version; so as soon as any changes are made upstream, those changes will be used by Toga. And since the testing story for the web backend is weak, there's no chicken-and-egg coordination problem - there's very few automated tests that need to pass before we can merge changes to the web template, so we don't need to worry about fixing Toga so that the template change will pass automated testing so that we can merge a PR so that Toga can use new features :-)

The two possible places a change might be required are in Briefcase itself, and in https://github.com/beeware/briefcase-web-static-template - the template that is used to generate Web code.

The minimalist fix will be to update (2) to point at the newly required version. Assuming there's no other large configuration changes, this may be all you need to do. If you want to test things out, you can clone the template repository, then add template = "/path/to/template/checkout" in your pyproject.toml in the web configuration section, and Briefcase will use your local checkout. Modify the Pyscript version referenced in that template, test it, submit a PR, and you're essentially done.

The intermediate fix would be to decouple the PyScript version from Briefcase entirely. To do this, you'd modify the web template to make the Pyscript version a template variable, and use that template variable in the template (with a value of 2024.11.1 as a default, so that existing apps don't see a change in behavior). You then document in Briefcase that you can use pyscript_version = "X.Y.Z" as a Briefcase configuration variable, and modify the Briefcase Toga bootstrap to specify the new version of PyScript. This relies on the fact that any configuration item in pyproject.toml is passed down to the template as context - so you don't actually need to do anything in Briefcase to expose a new configuration item for a template to use.

The comprehensive fix would be to resurrect the parts of #1945 and beeware/briefcase#1285 that would allow Toga to specify the version of Pyscript (and any other dependencies, such as Shoelace) that it requires. Those PRs allow a wheel to specify "inserts" which will be processed by Briefcase, and inserted into index.html at locations that the template allows. The inserts defined in the PR only include Shoelace includes and a verbatim block of CSS... but it would make sense to also include the Pyscript version definition, so that the Pyscript version is bound to Toga, not Briefcase - avoiding this problem completely in future. This might require some design work on how that version specification would be declared.

In terms of a pragmatic approach, I'd be entirely comfortable with the minimalist approach right now, as that gets you unstuck on the immediate issue of updating Pyscript API usage.

However, I'd like to think you and the team would be able to take a swing at the intermediate or comprehensive fix as part of the work that you're doing improving BeeWare's web tooling.

@mEp3ii2
Copy link
Contributor Author

mEp3ii2 commented Apr 28, 2025

In particiular, the pyscript.web.* APIs (additional user guide here) provide a way to create any DOM element, and presumably it also "does the right thing" when it comes to attaching handlers - that would seem to be the intention of the when() method/decorator.

So - can we avoid the need to call pyodide internal APIs entirely by using the pyscript API exclusively? Can we replace Widget._create_dom_element with a native Pyscript call?

I've looked into using the @when decorator to attach event listeners to our web widgets. After some research, testing and experimentation, we've found that is not a good fit for our use case for a few reasons.

  • The @when decorators requires static selectors known at import time. Since widget elements are created dynamically at runtime (with IDs like toga_12345678 ) we cannot reference them with @when in a reliable way.
  • In contrast, attaching event listeners dynamically after element creation lets us handle arbitrary numbers of widgets cleanly without needing to pre-declare handlers.

So we can use pyscript.web.document.querySelector and addEventListener , from pyscript.web API rather then the current pyodide create_proxy methods.

This approach is fully compatible with the current version of Pyscript we are using, so no updates are needed at this stage. Happy to hear any further thoughts or suggestions on refining this.

I've already prepared the updated code based on this approach and it's ready to push if there are no issues. (For testing I used the switch example)

@freakboy3742
Copy link
Member

freakboy3742 commented Apr 28, 2025

  • The @when decorators requires static selectors known at import time. Since widget elements are created dynamically at runtime (with IDs like toga_12345678 ) we cannot reference them with @when in a reliable way.

We may not be able to use it as @when - but a decorator is just a function that accepts a function and returns a function. The ID of the widget is known as soon as it is created, so we're in a position to call

when("click", f"#{self.id}", handler=self.dom_onclick)

So we can use pyscript.web.document.querySelector and addEventListener , from pyscript.web API rather then the current pyodide create_proxy methods.

From a quick poke around, it looks like when() is essentially a wrapper around querySelector and addEventListener() APIs. However, it's got a lot of extra baggage to support being used as a decorator, and to support some legacy and utility use cases (like passing in a string instead of a method name). We're in a position to be slightly more optimised, as we don't have to deal with those cases; so if using the "raw" methods directly works better, then we might as well use the optimised path.

I've already prepared the updated code based on this approach and it's ready to push if there are no issues. (For testing I used the switch example)

I think we're definitely at the "show me the code" stage :-) I suspect both approaches (when() and direct calls to lower level pyscript.web APIs) will work; so if you've got the latter working, and it's not missing error handling or some other useful feature that when() has provided - then we might as well go with pyscript.web calls.

-Added helper method for event handling in web libs module.
-Updated current use of event handlers in widgets and window module.
-Updated element creation from js.document.createElement to pyscript.web.document.createElement
Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general ideas here make sense, but I'm not sure all the pieces are connecting together.

In particular, you say you've tested this with the switch example - and while the example loads without error, none of the event handlers are firing. From what I can make out, this is because the querySelector call is returning None from every request - so no event handlers are being installed.

def create(self):
self.native = self._create_native_widget("sl-switch")
self.native.addEventListener("sl-change", create_proxy(self.dom_onchange))
add_event_listener(self.native.id, "sl-change", self.dom_onchange)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So... is this actually doing anything?

As of this PR, add_event_listener is a wrapper around a call to document.querySelector(), then calling add_event_listener() on that object. But self.native should be the same object as is returned by the query selector... at which point the call to self.native.addEventListener will be functionally equivalent to calling add_event_listener(), except without the proxy.

It seems to me that the actual fix here is the change to use document.createElement() rather than the js.document API.

def on_size_allocate(self, widget, allocation):
pass

@when("focus", selector="body")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a consistency perspective, it would be preferable to stick to a single mechanism for binding events - and given @when will be processed when the class is parsed, it seems preferable to use the explicit invocation of addEventListener().



create_proxy = pyodide.ffi.create_proxy if pyodide else lambda f: f
from pyscript.web import document # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the type: ignore doing? While we have type annotations for documentation purposes, we're not enforcing them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This question is still outstanding.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh sorry the yellow squiggle was bothering me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah - ok; that's what the try: except ImportError: blocks on the other js/pyodide imports are doing; they're ensuring that there's a minimum of squiggles :-)

"""

element = document.querySelector(f"#{element_id}")
if element:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this isn't masking other problems? Under what conditions will the element not exist? From my testing... this is returning None for every widget, presumably because the widget hasn't been added to the DOM immediately after creation.

…ges so that the focus/blur and visbilitychange listeners in the window module are firing and proxy error is not occuring
@mEp3ii2
Copy link
Contributor Author

mEp3ii2 commented May 2, 2025

I've reverted to using the current create_proxy pyodide method to focus on fixing the issues in being raised in the window module. I've used the tutorial1 example to test and ensure that the focus/blur and visibility change events are firing and that no proxy error was being raised.
With focus and blur being attached to the body we need to set the useCapture arg in the addEventListener method to true to ensure that the event is captured and will fire. Ref: addEvetnListenerDoc.
Hope this will be okay for now and am planning on looking into updating to make better use of pyscript.web in the future.
Let me know if their is any issues

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to work well as an interim fix to get you and the team unstuck for now.

One question outstanding; plus a suggested improvement to the release note.

@@ -0,0 +1 @@
Improved web event listeners to prevent errors and improved stability
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Improved web event listeners to prevent errors and improved stability
Window visibility and focus events in the web backend no longer raise errors when the browser window loses focus.



create_proxy = pyodide.ffi.create_proxy if pyodide else lambda f: f
from pyscript.web import document # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This question is still outstanding.

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome - thanks for this cleanup. I'm sure we'll revisit the specifics over time, but for now, this is a good incremental improvement.

@freakboy3742 freakboy3742 merged commit be173a5 into beeware:main May 2, 2025
52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants