Skip to content

Commit 9c941f9

Browse files
authored
Merge pull request #37 from davidbrochart/nest_asyncio
Make nest_asyncio optional
2 parents 5fc8bf4 + 0c61be4 commit 9c941f9

File tree

3 files changed

+70
-68
lines changed

3 files changed

+70
-68
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)"
49+
"nb = nbclient.execute(nb, nest_asyncio=True)"
5050
]
5151
},
5252
{

nbclient/client.py

Lines changed: 22 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
# contextlib, and we `await yield_()` instead of just `yield`
77
from async_generator import asynccontextmanager, async_generator, yield_
88

9-
import nest_asyncio
10-
119
from time import monotonic
1210
from queue import Empty
1311
import asyncio
@@ -18,6 +16,7 @@
1816
from nbformat.v4 import output_from_msg
1917

2018
from .exceptions import CellTimeoutError, DeadKernelError, CellExecutionComplete, CellExecutionError
19+
from .util import run_sync
2120

2221

2322
def timestamp():
@@ -97,6 +96,21 @@ class NotebookClient(LoggingConfigurable):
9796
),
9897
).tag(config=True)
9998

99+
nest_asyncio = Bool(
100+
False,
101+
help=dedent(
102+
"""
103+
If False (default), then blocking functions such as `execute`
104+
assume that no event loop is already running. These functions
105+
run their async counterparts (e.g. `async_execute`) in an event
106+
loop with `asyncio.run_until_complete`, which will fail if an
107+
event loop is already running. This can be the case if nbclient
108+
is used e.g. in a Jupyter Notebook. In that case, `nest_asyncio`
109+
should be set to True.
110+
"""
111+
),
112+
).tag(config=True)
113+
100114
force_raise_errors = Bool(
101115
False,
102116
help=dedent(
@@ -367,21 +381,9 @@ async def setup_kernel(self, **kwargs):
367381
self.kc.stop_channels()
368382
self.kc = None
369383

370-
def execute(self, **kwargs):
371-
"""
372-
Executes each code cell (blocking).
373-
374-
Returns
375-
-------
376-
nb : NotebookNode
377-
The executed notebook.
378-
"""
379-
loop = get_loop()
380-
return loop.run_until_complete(self.async_execute(**kwargs))
381-
382384
async def async_execute(self, **kwargs):
383385
"""
384-
Executes each code cell asynchronously.
386+
Executes each code cell.
385387
386388
Returns
387389
-------
@@ -404,6 +406,8 @@ async def async_execute(self, **kwargs):
404406

405407
return self.nb
406408

409+
execute = run_sync(async_execute)
410+
407411
def set_widgets_metadata(self):
408412
if self.widget_state:
409413
self.nb.metadata.widgets = {
@@ -550,48 +554,9 @@ def _check_raise_for_error(self, cell, exec_reply):
550554
if (exec_reply is not None) and exec_reply['content']['status'] == 'error':
551555
raise CellExecutionError.from_cell_and_msg(cell, exec_reply['content'])
552556

553-
def execute_cell(self, cell, cell_index, execution_count=None, store_history=True):
554-
"""
555-
Executes a single code cell (blocking).
556-
557-
To execute all cells see :meth:`execute`.
558-
559-
Parameters
560-
----------
561-
cell : nbformat.NotebookNode
562-
The cell which is currently being processed.
563-
cell_index : int
564-
The position of the cell within the notebook object.
565-
execution_count : int
566-
The execution count to be assigned to the cell (default: Use kernel response)
567-
store_history : bool
568-
Determines if history should be stored in the kernel (default: False).
569-
Specific to ipython kernels, which can store command histories.
570-
571-
Returns
572-
-------
573-
output : dict
574-
The execution output payload (or None for no output).
575-
576-
Raises
577-
------
578-
CellExecutionError
579-
If execution failed and should raise an exception, this will be raised
580-
with defaults about the failure.
581-
582-
Returns
583-
-------
584-
cell : NotebookNode
585-
The cell which was just processed.
586-
"""
587-
loop = get_loop()
588-
return loop.run_until_complete(
589-
self.async_execute_cell(cell, cell_index, execution_count, store_history)
590-
)
591-
592557
async def async_execute_cell(self, cell, cell_index, execution_count=None, store_history=True):
593558
"""
594-
Executes a single code cell asynchronously.
559+
Executes a single code cell.
595560
596561
To execute all cells see :meth:`execute`.
597562
@@ -654,6 +619,8 @@ async def async_execute_cell(self, cell, cell_index, execution_count=None, store
654619
self.nb['cells'][cell_index] = cell
655620
return cell
656621

622+
execute_cell = run_sync(async_execute_cell)
623+
657624
def process_message(self, msg, cell, cell_index):
658625
"""
659626
Processes a kernel message, updates cell state, and returns the
@@ -809,15 +776,3 @@ def execute(nb, cwd=None, km=None, **kwargs):
809776
if cwd is not None:
810777
resources['metadata'] = {'path': cwd}
811778
return NotebookClient(nb=nb, resources=resources, km=km, **kwargs).execute()
812-
813-
814-
def get_loop():
815-
try:
816-
loop = asyncio.get_event_loop()
817-
except RuntimeError:
818-
loop = asyncio.new_event_loop()
819-
asyncio.set_event_loop(loop)
820-
return loop
821-
822-
823-
nest_asyncio.apply()

nbclient/util.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""General utility methods"""
2+
3+
# Copyright (c) Jupyter Development Team.
4+
# Distributed under the terms of the Modified BSD License.
5+
6+
import asyncio
7+
8+
9+
def run_sync(coro):
10+
"""Runs a coroutine and blocks until it has executed.
11+
12+
An event loop is created if no one already exists. If an event loop is
13+
already running, this event loop execution is nested into the already
14+
running one if `nest_asyncio` is set to True.
15+
16+
Parameters
17+
----------
18+
coro : coroutine
19+
The coroutine to be executed.
20+
21+
Returns
22+
-------
23+
result :
24+
Whatever the coroutine returns.
25+
"""
26+
def wrapped(self, *args, **kwargs):
27+
try:
28+
loop = asyncio.get_event_loop()
29+
except RuntimeError:
30+
loop = asyncio.new_event_loop()
31+
asyncio.set_event_loop(loop)
32+
if self.nest_asyncio:
33+
import nest_asyncio
34+
nest_asyncio.apply(loop)
35+
try:
36+
result = loop.run_until_complete(coro(self, *args, **kwargs))
37+
except RuntimeError as e:
38+
if str(e) == 'This event loop is already running':
39+
raise RuntimeError(
40+
'You are trying to run nbclient in an environment where an '
41+
'event loop is already running. Please pass `nest_asyncio=True` in '
42+
'`NotebookClient.execute` and such methods.'
43+
)
44+
raise
45+
return result
46+
wrapped.__doc__ = coro.__doc__
47+
return wrapped

0 commit comments

Comments
 (0)