jupyter / jupyter_client

Jupyter protocol client APIs
https://jupyter-client.readthedocs.io
BSD 3-Clause "New" or "Revised" License
383 stars 283 forks source link

Comms with async functions #868

Open mlucool opened 1 year ago

mlucool commented 1 year ago

Hi,

Is there a recommendation for when a comm needs to call an async function. While support for https://github.com/jupyter/jupyter_client/issues/433 would be ideal, the usecase is that there is some async def foo() that needs to be called from within a comm. For now, the best way to work around this was to make it synchronous via (using ideas from https://github.com/ipython/ipython/issues/13670#issuecomment-1140245675):

import nest_asyncio
nest_asyncio.apply()
asyncio.run(foo())

This seems to work, but I didn't know if there was a better way.

blink1073 commented 1 year ago

Hi @mlucool, you could use a pattern like the following to avoid using nest_asyncio:

import asyncio
loop = asyncio.get_event_loop()
loop.create_task(foo())

The above will only work if there is an asyncio loop running, which is the case when run in ipykernel.

Assuming https://github.com/python/cpython/pull/98440 gets merged instead of https://github.com/python/cpython/issues/93453, this pattern will continue to work in Python 3.12+.

graingert commented 1 year ago

@blink1073 https://github.com/python/cpython/pull/98440 made no such changes to what happens with get_event_loop when an event loop is already running. You only need to use asyncio.create_task(foo()) in this case

blink1073 commented 1 year ago

If I understand correctly, asyncio.create_task(foo()) will always work now with IPython, since an event loop is installed but potentially not running by default. I'll wait until I can actually test it with a patched version of CPython to verify.

blink1073 commented 1 year ago

No, I just tried Python 3.11.1 which has the backport. As expected, fut = asyncio.Future() no longer raises a RuntimeWarning because IPython has an event loop installed but not running. However, asyncio.create_task(foo()) raises:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[6], line 1
----> 1 asyncio.create_task(foo())

File /Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/tasks.py:371, in create_task(coro, name, context)
    366 def create_task(coro, *, name=None, context=None):
    367     """Schedule the execution of a coroutine object in a spawn task.
    368
    369     Return a Task object.
    370     """
--> 371     loop = events.get_running_loop()
    372     if context is None:
    373         # Use legacy API if context is not needed
    374         task = loop.create_task(coro)

RuntimeError: no running event loop

However, using the following still works and does not give a warning:

In [7]: loop = asyncio.get_event_loop()
   ...: loop.create_task(foo())
Out[7]: <Task pending name='Task-436' coro=<foo() running at <ipython-input-5-f3ec22a30cd8>:1>>
blink1073 commented 1 year ago

I get the same error with Python 3.12.0a3 and asyncio.create_task(foo())