jupyter-widgets / ipywidgets

Interactive Widgets for the Jupyter Notebook
https://ipywidgets.readthedocs.io
BSD 3-Clause "New" or "Revised" License
3.16k stars 950 forks source link

Question: How to wait (in a blocking way) for messages from widget frontend? #3039

Open anant-k-singh opened 3 years ago

anant-k-singh commented 3 years ago

I have created a custom widget whose frontend renders a plot (using a NPM library). That library has an API that extracts the underlying data of the plot.

class My_Custom_Widget(widgets.DOMWidget):
    ...
    def wait_for_change(self, value):
        future = Future()
        # Callback for visual data
        def get_value(change):
            future.set_result(change.new)
            self.unobserve(get_value, value)

        self.observe(get_value, value)
        return future

    async def extract_data_async(self, visual_name):
        # "extract_data_request" is part of widget model (a traitlet)
        # Changing this model initiates the JS logic on frontend, data will be returned back by updating another model variable "visual_data"
        self.extract_data_request = {
            'visualName': visual_name
        }
        res = await self.wait_for_change('visual_data_response')
        return res

In the Jupyter notebook, I run the following:

extracted_data = await my_widget.extract_data_async(visual_name='vis_id#4')

I want to use this extracted data for ML purposes.

Current behavior: The kernel is blocked at line res = await self.wait_for_change(... forever since the get_value() never executes due to kernel being blocked, kinda deadlock :(

According to ipywidget docs,

You may want to pause your Python code to wait for some user interaction with a widget from the frontend. Typically this would be hard to do since running Python code blocks any widget messages from the frontend until the Python code is done.

Since the kernel is blocked (due to await), it doesn't execute the get_value() method hence future is never resolved/completed.

Please suggest an alternate approach. An approach where kernel is not blocked by the data is received in the extracted_data variable after some time would also be helpful

Thanks in advance!

ianhi commented 3 years ago

I think this is a tricky problem that has not yet been fully solved. There was some good discussion here: https://github.com/martinRenou/ipycanvas/issues/77

Two people who maybe are interested are @martinRenou and @jtpio

ianhi commented 3 years ago

imjoy is an alternative framework on which to build widgets and has implemented https://github.com/imjoy-team/imjoy-rpc which allows for what you want. So also cc. @oeway

I think the relevant lines of how imjoy-rpc does it are these: https://github.com/imjoy-team/imjoy-rpc/blob/30835b1e36d3868ea80a8e2cb3339b9e3048a230/python/imjoy_rpc/rpc.py#L324

It would be really nice to have something like that available through ipywidgets as well.

oeway commented 3 years ago

@ianhi Thanks for tagging me here and advertise ImJoy!

@anant-k-singh To clarify, we had the same issue as you describe here, and I think there isn't a good solution yet. As I understand, this limitation is come form the ipykernel, you can see it also here: https://github.com/ipython/ipykernel/issues/65. There is a work around by caching the jupyter command with https://github.com/kafonek/ipython_blocking, but I think it won't solve the issue you have. I was told that there is an async kernel might solve this, but I haven't figure it out yet. For now, what we did is to avoid using top-level await.

BTW, I saw you also posted another issue here. If that's the same task you are trying to solve, you might want to try imjoy-jupyter-extension indeed. It is a complementary solution for making interactive widgets. Instead of using a data binding model, we provide an RPC interface which allows you call your JS function in python and vice versa. Here are two examples that works in Jupyter notebooks with the imjoy-jupyter-extension: Kaibu in VueJS and vizarr in react (click the "Open in Binder" badge to run the demo). Briefly, assuming you have a web app in react/vuejs and you want to use it in jupyter notebook, what you do is to load the imjoy-rpc js library into your app, then export some js functions (e.g. process_in_js()), that's it. To use your web app in a notebook as a plugin, you can call imjoy api(api.createWindow(src="URL_OF_YOUR_APP")) to to instantiate an iframe window for your web app, then you can just call the js function as if they are defined in python. If you pass a callback function from python to js, then you can call that function also from your frontend. To learn more, a more complete tutorial is here.

jasongrout commented 3 years ago

Here is a possibly helpful snippet to try to get an unblocked await: https://gist.github.com/minrk/feaf2022bf43d1a94e03ceaf9a4ef355

ianhi commented 3 years ago

@jasongrout how do you mean by unblocked? Does that mean it doesn't prevent the message processing?


Also in re-searching about this I came across https://github.com/jupyter-widgets/ipywidgets/issues/2417 and with some more clicking to https://gitter.im/jupyter-widgets/Lobby?at=5e86fe9381a582042e972b4d which looks like it may be the best way to solve awaiting a message from the frontend?

davidbrochart commented 3 years ago

I was told that there is an async kernel might solve this, but I haven't figure it out yet.

akernel is a new Python asynchronous kernel I'm working on. It is still very experimental, but it allows to write the example in the documentation without creating a task, i.e. you can await at the top-level and it won't prevent receiving the value change:

for i in range(10):
    print('did work %s'%i)
    x = await wait_for_change(slider, 'value')
    print('async function continued with value %s'%x)