Skip to content

Commit

Permalink
fix: always run a coroutine and work with tornado
Browse files Browse the repository at this point in the history
  • Loading branch information
maartenbreddels committed May 27, 2020
1 parent 6add32d commit 88beb2f
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 36 deletions.
2 changes: 1 addition & 1 deletion binder/run_nbclient.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"nb = nbf.read('./empty_notebook.ipynb', nbf.NO_CONVERT)\n",
"\n",
"# Execute our in-memory notebook, which will now have outputs\n",
"nb = nbclient.execute(nb, nest_asyncio=True)"
"nb = nbclient.execute(nb)"
]
},
{
Expand Down
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Major Changes

- Mimic an Output widget at the frontend so that the Output widget behaves correctly [#68](https://github.com/jupyter/nbclient/pull/68)
- Nested asyncio is automatic, and works with Tornado [#71](https://github.com/jupyter/nbclient/pull/71)

## 0.3.1

Expand Down
15 changes: 0 additions & 15 deletions nbclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,6 @@ class NotebookClient(LoggingConfigurable):
),
).tag(config=True)

nest_asyncio = Bool(
False,
help=dedent(
"""
If False (default), then blocking functions such as `execute`
assume that no event loop is already running. These functions
run their async counterparts (e.g. `async_execute`) in an event
loop with `asyncio.run_until_complete`, which will fail if an
event loop is already running. This can be the case if nbclient
is used e.g. in a Jupyter Notebook. In that case, `nest_asyncio`
should be set to True.
"""
),
).tag(config=True)

force_raise_errors = Bool(
False,
help=dedent(
Expand Down
58 changes: 58 additions & 0 deletions nbclient/tests/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import asyncio

import tornado

from nbclient.util import run_sync


@run_sync
async def some_async_function():
await asyncio.sleep(0.01)
return 42


def test_nested_asyncio_with_existing_ioloop():
ioloop = asyncio.new_event_loop()
try:
asyncio.set_event_loop(ioloop)
assert some_async_function() == 42
assert asyncio.get_event_loop() is ioloop
print(ioloop, id(ioloop))
finally:
asyncio._set_running_loop(None) # it seems nest_asyncio doesn't reset this


def test_nested_asyncio_with_no_ioloop():
asyncio.set_event_loop(None)
try:
assert some_async_function() == 42
finally:
asyncio._set_running_loop(None) # it seems nest_asyncio doesn't reset this


def test_nested_asyncio_with_tornado():
# This tests if tornado accepts the pure-Python Futures, see
# https://github.com/tornadoweb/tornado/issues/2753
# https://github.com/erdewit/nest_asyncio/issues/23
asyncio.set_event_loop(asyncio.new_event_loop())
ioloop = tornado.ioloop.IOLoop.current()

async def some_async_function():
future = asyncio.ensure_future(asyncio.sleep(0.1))
# this future is a different future after nested-asyncio has patched
# the asyncio module, check if tornado likes it:
ioloop.add_future(future, lambda f: f.result())
await future
return 42

def some_sync_function():
return run_sync(some_async_function)()

async def run():
# calling some_async_function directly should work
assert await some_async_function() == 42
# but via a sync function (using nested-asyncio) can lead to issues:
# https://github.com/tornadoweb/tornado/issues/2753
assert some_sync_function() == 42

ioloop.run_sync(run)
59 changes: 39 additions & 20 deletions nbclient/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,46 @@
# Distributed under the terms of the Modified BSD License.

import asyncio
import sys
import inspect


def check_ipython():
# original from vaex/asyncio.py
IPython = sys.modules.get('IPython')
if IPython:
IPython_version = tuple(map(int, IPython.__version__.split('.')))
if IPython_version < (7, 0, 0):
raise RuntimeError(f'You are using IPython {IPython.__version__} while we require'
'7.0.0+, please update IPython')


def check_patch_tornado():
"""If tornado is imported, add the patched asyncio.Future to its tuple of acceptable Futures"""
# original from vaex/asyncio.py
if 'tornado' in sys.modules:
import tornado.concurrent
if asyncio.Future not in tornado.concurrent.FUTURES:
tornado.concurrent.FUTURES = tornado.concurrent.FUTURES + (asyncio.Future, )


def just_run(coro):
"""Make the coroutine run, even if there is an event loop running (using nest_asyncio)"""
# original from vaex/asyncio.py
try:
loop = asyncio.get_event_loop()
had_loop = True
except RuntimeError:
had_loop = False
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if had_loop:
check_ipython()
import nest_asyncio
nest_asyncio.apply()
check_patch_tornado()
return loop.run_until_complete(coro)

def run_sync(coro):
"""Runs a coroutine and blocks until it has executed.
Expand All @@ -24,26 +61,8 @@ def run_sync(coro):
result :
Whatever the coroutine returns.
"""
def wrapped(self, *args, **kwargs):
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if self.nest_asyncio:
import nest_asyncio
nest_asyncio.apply(loop)
try:
result = loop.run_until_complete(coro(self, *args, **kwargs))
except RuntimeError as e:
if str(e) == 'This event loop is already running':
raise RuntimeError(
'You are trying to run nbclient in an environment where an '
'event loop is already running. Please pass `nest_asyncio=True` in '
'`NotebookClient.execute` and such methods.'
) from e
raise
return result
def wrapped(*args, **kwargs):
return just_run(coro(*args, **kwargs))
wrapped.__doc__ = coro.__doc__
return wrapped

Expand Down

0 comments on commit 88beb2f

Please sign in to comment.