Skip to content

Commit 2d5cee2

Browse files
authored
Merge pull request #71 from maartenbreddels/fix_auto_run_nested_with_tornado
fix: always run a coroutine and work with tornado
2 parents 35ff13d + a79ae70 commit 2d5cee2

File tree

5 files changed

+105
-36
lines changed

5 files changed

+105
-36
lines changed

binder/run_nbclient.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"nb = nbf.read('./empty_notebook.ipynb', nbf.NO_CONVERT)\n",
4747
"\n",
4848
"# Execute our in-memory notebook, which will now have outputs\n",
49-
"nb = nbclient.execute(nb, nest_asyncio=True)"
49+
"nb = nbclient.execute(nb)"
5050
]
5151
},
5252
{

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Major Changes
66

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

910
## 0.3.1
1011

nbclient/client.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -103,21 +103,6 @@ class NotebookClient(LoggingConfigurable):
103103
),
104104
).tag(config=True)
105105

106-
nest_asyncio = Bool(
107-
False,
108-
help=dedent(
109-
"""
110-
If False (default), then blocking functions such as `execute`
111-
assume that no event loop is already running. These functions
112-
run their async counterparts (e.g. `async_execute`) in an event
113-
loop with `asyncio.run_until_complete`, which will fail if an
114-
event loop is already running. This can be the case if nbclient
115-
is used e.g. in a Jupyter Notebook. In that case, `nest_asyncio`
116-
should be set to True.
117-
"""
118-
),
119-
).tag(config=True)
120-
121106
force_raise_errors = Bool(
122107
False,
123108
help=dedent(

nbclient/tests/util.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import asyncio
2+
3+
import tornado
4+
5+
from nbclient.util import run_sync
6+
7+
8+
@run_sync
9+
async def some_async_function():
10+
await asyncio.sleep(0.01)
11+
return 42
12+
13+
14+
def test_nested_asyncio_with_existing_ioloop():
15+
ioloop = asyncio.new_event_loop()
16+
try:
17+
asyncio.set_event_loop(ioloop)
18+
assert some_async_function() == 42
19+
assert asyncio.get_event_loop() is ioloop
20+
finally:
21+
asyncio._set_running_loop(None) # it seems nest_asyncio doesn't reset this
22+
23+
24+
def test_nested_asyncio_with_no_ioloop():
25+
asyncio.set_event_loop(None)
26+
try:
27+
assert some_async_function() == 42
28+
finally:
29+
asyncio._set_running_loop(None) # it seems nest_asyncio doesn't reset this
30+
31+
32+
def test_nested_asyncio_with_tornado():
33+
# This tests if tornado accepts the pure-Python Futures, see
34+
# https://github.com/tornadoweb/tornado/issues/2753
35+
# https://github.com/erdewit/nest_asyncio/issues/23
36+
asyncio.set_event_loop(asyncio.new_event_loop())
37+
ioloop = tornado.ioloop.IOLoop.current()
38+
39+
async def some_async_function():
40+
future = asyncio.ensure_future(asyncio.sleep(0.1))
41+
# this future is a different future after nested-asyncio has patched
42+
# the asyncio module, check if tornado likes it:
43+
ioloop.add_future(future, lambda f: f.result())
44+
await future
45+
return 42
46+
47+
def some_sync_function():
48+
return run_sync(some_async_function)()
49+
50+
async def run():
51+
# calling some_async_function directly should work
52+
assert await some_async_function() == 42
53+
# but via a sync function (using nested-asyncio) can lead to issues:
54+
# https://github.com/tornadoweb/tornado/issues/2753
55+
assert some_sync_function() == 42
56+
57+
ioloop.run_sync(run)

nbclient/util.py

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,53 @@
44
# Distributed under the terms of the Modified BSD License.
55

66
import asyncio
7+
import sys
78
import inspect
89

910

11+
def check_ipython():
12+
# original from vaex/asyncio.py
13+
IPython = sys.modules.get('IPython')
14+
if IPython:
15+
IPython_version = tuple(map(int, IPython.__version__.split('.')))
16+
if IPython_version < (7, 0, 0):
17+
raise RuntimeError(f'You are using IPython {IPython.__version__} while we require'
18+
'7.0.0+, please update IPython')
19+
20+
21+
def check_patch_tornado():
22+
"""If tornado is imported, add the patched asyncio.Future to its tuple of acceptable Futures"""
23+
# original from vaex/asyncio.py
24+
if 'tornado' in sys.modules:
25+
import tornado.concurrent
26+
if asyncio.Future not in tornado.concurrent.FUTURES:
27+
tornado.concurrent.FUTURES = tornado.concurrent.FUTURES + (asyncio.Future, )
28+
29+
30+
def just_run(coro):
31+
"""Make the coroutine run, even if there is an event loop running (using nest_asyncio)"""
32+
# original from vaex/asyncio.py
33+
loop = asyncio._get_running_loop()
34+
if loop is None:
35+
had_running_loop = False
36+
try:
37+
loop = asyncio.get_event_loop()
38+
except RuntimeError:
39+
# we can still get 'There is no current event loop in ...'
40+
loop = asyncio.new_event_loop()
41+
asyncio.set_event_loop(loop)
42+
else:
43+
had_running_loop = True
44+
if had_running_loop:
45+
# if there is a running loop, we patch using nest_asyncio
46+
# to have reentrant event loops
47+
check_ipython()
48+
import nest_asyncio
49+
nest_asyncio.apply()
50+
check_patch_tornado()
51+
return loop.run_until_complete(coro)
52+
53+
1054
def run_sync(coro):
1155
"""Runs a coroutine and blocks until it has executed.
1256
@@ -24,26 +68,8 @@ def run_sync(coro):
2468
result :
2569
Whatever the coroutine returns.
2670
"""
27-
def wrapped(self, *args, **kwargs):
28-
try:
29-
loop = asyncio.get_event_loop()
30-
except RuntimeError:
31-
loop = asyncio.new_event_loop()
32-
asyncio.set_event_loop(loop)
33-
if self.nest_asyncio:
34-
import nest_asyncio
35-
nest_asyncio.apply(loop)
36-
try:
37-
result = loop.run_until_complete(coro(self, *args, **kwargs))
38-
except RuntimeError as e:
39-
if str(e) == 'This event loop is already running':
40-
raise RuntimeError(
41-
'You are trying to run nbclient in an environment where an '
42-
'event loop is already running. Please pass `nest_asyncio=True` in '
43-
'`NotebookClient.execute` and such methods.'
44-
) from e
45-
raise
46-
return result
71+
def wrapped(*args, **kwargs):
72+
return just_run(coro(*args, **kwargs))
4773
wrapped.__doc__ = coro.__doc__
4874
return wrapped
4975

0 commit comments

Comments
 (0)