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

save_changes does not correctly synchronize to python-side #788

Closed
peekxc opened this issue Feb 3, 2025 · 4 comments
Closed

save_changes does not correctly synchronize to python-side #788

peekxc opened this issue Feb 3, 2025 · 4 comments
Labels
bug Something isn't working

Comments

@peekxc
Copy link

peekxc commented Feb 3, 2025

Describe the bug

As far as I can tell, one cannot actually do "two-way data binding" via synchronizing traitlet-managed data, as save_changes doesn't appear to actually work (at least synchronously...)

This might be a duplicate of #786, but the example is much smaller.

Reproduction

Firstly, a minimal widget that defines a supposedly synchronized List traitlet and a custom message to sync:

import anywidget
import traitlets

class SyncWidget(anywidget.AnyWidget):
    _esm = """
	function render({ model, el }) {
		model.on("msg:custom", (msg) => {
			if (msg.type == "msg:sync_values"){
				let values = model.get('values');
				model.set('values', values.map(val => val + 5));
				model.save_changes();
			}
		});
	};
    export default { render };
    """
    values = traitlets.List([]).tag(sync=True)

widget = SyncWidget()
widget

On jupyter, setting this value works as intended:

widget.values = [1,2,3,4,5]
print(widget.values)
# [1, 2, 3, 4, 5]

The issue is the synchronization doesn't work, at least programmatically:

widget.send({ "type" : "msg:sync_values"})
print(widget.values)
# [1, 2, 3, 4, 5]

Now, if you wait for about a second or so in the browser and then re-execute the getter for widget.values, you get the following:

print(widget.values)
# [6, 7, 8, 9, 10]

Logs

System Info

System:
    OS: macOS 14.5
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 184.09 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Browsers:
    Brave Browser: 130.1.71.118
    Chrome: 132.0.6834.160
    Edge: 132.0.2957.127

jupyter==1.0.0
jupyter-book==0.13.1
jupyter-cache==0.4.3
jupyter-console==6.6.3
jupyter-events==0.6.3
jupyter-server-mathjax==0.2.6
jupyter-sphinx==0.3.2
jupyter_client==7.4.9
jupyter_core==5.1.3
jupyter_server==2.1.0
jupyter_server_terminals==0.4.4
jupyterlab-pygments==0.2.2
jupyterlab-widgets==1.1.1
notebook==6.5.2
notebook_shim==0.2.2
sphinx-jupyterbook-latex==0.4.7

Severity

annoyance

@peekxc peekxc added the bug Something isn't working label Feb 3, 2025
@peekxc
Copy link
Author

peekxc commented Feb 3, 2025

Update: I confirmed that using e.g. traitlets observe decorator does not help

@observe("values")
def _observe_values(self, change):
    print(f"Old: {change['old']}")
    print(f"New: {change['new']}")

This only triggers when values is set from the Python side, not on the JS side (maybe this is to be expected though)

@manzt
Copy link
Owner

manzt commented Feb 5, 2025

This behavior is expected due to how Jupyter widgets synchronize state between Python and the frontend. There is two-way data binding, but synchronization between JS <> Python happens asynchronously, meaning there is no built-in way to block Python execution while waiting for a response from the frontend.

In your snippet, widget.send dispatches a message to the frontend and immediately continues executing Python code. There’s no code that "blocks" Python execution until a response is received. We can "block" on the JavaScript side since the environment is consistent (see: #54), the same is not true for Python in this context. This is a general limitation of Jupyter widgets, not specific to anywidget.

Some anywidget developers have found jupyter-ui-poll useful for this use case, but it is tied to the Jupyter ecosystem. I’m not aware of a general pattern that could be implemented in anywidget in a host-platform-agnostic way.

@peekxc
Copy link
Author

peekxc commented Feb 5, 2025

@manzt Ahh I see, I suspected it may be an async-related issue but I figured I'd ask because I actually had no idea if-or-how Python's async interacts with JS async.

Thanks for the explanation and the links too, I see now that this is effectively a duplicate of #54. Closing as a result.

In the meantime, I'm just going to get more clever with when the synchronizations happen.

@peekxc peekxc closed this as completed Feb 5, 2025
@manzt
Copy link
Owner

manzt commented Feb 5, 2025

Of course! Thanks for the clear and detailed issue. We should probably have a section in the docs detailing this as it comes up from time to time on the Discord too.

In the meantime, I'm just going to get more clever with when the synchronizations happen.

If you find any patterns that work well, please share!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants