Hi all!
I've got a custom widget with some traits. This widget is basically a live stream, so I'm getting the data into JavaScript directly. I then programatically update a trait with this.model.set('mytrait', some_value). This works great and triggers the corresponding change:mytrait event.
What I'd really like would be for a user in a notebook to be able to get the value of mytrait from Python. Ideally, this would only do the serialization when the user calls widget.mytrait in Python. Right now, if I do that, all I get is the initial default value set in Python (None).
I thought this.touch() or this.model.save_changes() would help, but they did not. So how can I set a model value programatically in JavaScript and have the user in Python able to get that value on demand?
this.touch after the model.set is the way to go. It is basically the same thing as save_changes except that the update message has the right metadata so that outputs resulting from that message are displayed under the correct cell.
Is the serialization from JavaScript to Python done lazily then? I only want that serialization to occur if a user in Python specifically calls widget.mytrait. Is this possible or should I not be using traits for this?
Is the serialization from JavaScript to Python done lazily then
Both the Python model and the JavaScript model maintain their own copy of the state. Any state change is propagated by passing events. Changes in Python are pushed to the browser straightaway, and changes in the browser are pushed to the Python side using model.touch().
When you call widget.mytrait, this just looks at the current value of the mytrait attribute in the Python side. It doesn't call out to the JavaScript.
For a fairly simple example, I suggest looking at the implementation of checkboxes:
When the user changes the value trait of a checkbox Python-side, a websocket message like this is sent to the browser:
{state: {value: true}, method: "update"}
When the user clicks on the checkbox in the browser (line 116), the corresponding value attribute in the browser-side model is updated. The model is then touched, which causes it to propagate its updated state back to the Python by sending this message:
{method: "update", state: {value: false}}
You can view the websocket frames in your browser console (at least on Chrome).
One of the main issues to overcome in order to do what you want to do, is that any "fetch value" request sent to the frontend is going to be asynchronous. There is also the issue that there can be multiple frontends, which would each reply to such a request.
To read up on asynchronous widget code, see https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Asynchronous.html.
If you have asynchronous code set up, you can have a python function that sends a custom message to the frontend, which causes it to set the model value (which will then be synced to the backend in the regular manner). If you observe the trait, you should get one change notification once the update has been received.
Thanks @vidartf @pbugnion @SylvainCorlay for your help here! The lazy update idea is still something I may pursue at some point, but I ended up using .touch() to reflect the JavaScript changes back into Python. For the moment, it's fast enough with the right serialization / deserialization methods. This is basically a streaming camera with some custom WebGL for visualization.
I'm closing this now, I appreciate the help. Was very educational for me.
@pbugnion - I would love to see your very clear technical explanation in https://github.com/jupyter-widgets/ipywidgets/issues/1783#issuecomment-340365890 in the docs somewhere - it is great!
This is basically a streaming camera with some custom WebGL for visualization.
On the subject of streaming video with widgets and webgl, you can check out the work of @maartenbreddels with ipywebrtc. (It might solve parts of what you are working on..)
And his work with the mediastream widget: https://github.com/jupyter-widgets/ipywidgets/pull/1685
So, I'm reopening this because I've now actually attempted to do exactly what @vidartf suggested. Details are as follows...
I've got a nice custom widget that I'm very happy with. It has some attributes that are updatable only through JavaScript. On the Python side, I have a property that a user can use to get the latest value. This works by sending a custom event over to JavaScript, which reacts to the event by setting a hidden trait and then doing .touch(). So basically I have x as a property that sends the custom message and _x as a normal Jupyter widget trait.
What I want to do is wait for _x to be ready, which is @vidartf suggested. I have read up on asynchronous widgets (as per here). What I find is those two approaches (asyncio and the generator approach) allow me to schedule a function that is called when a widget value is changed, but they do not block the main calling thread. I've fiddled around with this a lot, but I seem to be unable to make this work.
What I can do right now is use the wait_for_change idea in the documentation:
def wait_for_change(widget, value):
future = asyncio.Future()
def getvalue(change):
# make the new value available
future.set_result(change.new)
widget.unobserve(getvalue, value)
widget.observe(getvalue, value)
return future
and make the property return a future that will eventually have the right value. But I can't block until that future is ready using the normal asyncio mechanisms (loop.run_until_complete(future)). Am I missing something obvious here?
As far as I understand it you are right: you cannot block, as it would prevent messages from being processed.
Ah, that really sucks. Is there any workaround for this? That basically means what I want to do is impossible (or, at least, reduced to returning a future).
And messages are processed in the main thread? I guess I'm surprised by that.
And messages are processed in the main thread? I guess I'm surprised by that.
For the python kernel, messages are processed in the main thread.
Most helpful comment
And his work with the mediastream widget: https://github.com/jupyter-widgets/ipywidgets/pull/1685