Skip to content

Commit 3892c04

Browse files
committed
Rebased with master and added tests
Run_hook is now async and renamed util to test_util so it gets picked up by pytest. Also added new hooks: on_notebook_error, on_cell_execution Updated docs
1 parent c68b700 commit 3892c04

File tree

6 files changed

+483
-58
lines changed

6 files changed

+483
-58
lines changed

docs/client.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,36 @@ on both versions. Here the traitlet ``kernel_name`` helps simplify and
9696
maintain consistency: we can just run a notebook twice, specifying first
9797
"python2" and then "python3" as the kernel name.
9898

99+
Hooks before and after cell execution
100+
-------------------------------------
101+
There are several configurable hooks that allow the user to execute code before and
102+
after a cell is executed. Each one is configured with a function that will be called in its
103+
respective place in the cell execution pipeline.
104+
Each is described below:
105+
106+
**Notebook-level hooks**: These hooks are called with a single extra parameter:
107+
108+
- ``notebook=NotebookNode``: the current notebook being executed.
109+
110+
Here is the available hooks:
111+
112+
- ``on_notebook_start`` will run when the notebook client is initialized, before any execution has happened.
113+
- ``on_notebook_complete`` will run when the notebook client has finished executing, after kernel cleanup.
114+
- ``on_notebook_error`` will run when the notebook client has encountered an exception before kernel cleanup.
115+
116+
**Cell-level hooks**: These hooks are called with two parameters:
117+
118+
- ``cell=NotebookNode``: a reference to the current cell.
119+
- ``cell_index=int``: the index of the cell in the current notebook's list of cells
120+
121+
Here are the available hooks:
122+
123+
- ``on_cell_start`` will run for all cell types before the cell is executed.
124+
- ``on_cell_execute`` will run right before the code cell is executed.
125+
- ``on_cell_complete`` will run after execution, if the cell is executed with no errors.
126+
- ``on_cell_error`` will run if there is an error during cell execution.
127+
128+
99129
Handling errors and exceptions
100130
------------------------------
101131

nbclient/client.py

Lines changed: 96 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,18 @@
1515
from jupyter_client.client import KernelClient
1616
from nbformat import NotebookNode
1717
from nbformat.v4 import output_from_msg
18-
from traitlets import Any, Bool, Dict, Enum, Integer, List, Type, Unicode, default
18+
from traitlets import (
19+
Any,
20+
Bool,
21+
Callable,
22+
Dict,
23+
Enum,
24+
Integer,
25+
List,
26+
Type,
27+
Unicode,
28+
default,
29+
)
1930
from traitlets.config.configurable import LoggingConfigurable
2031

2132
from .exceptions import (
@@ -26,7 +37,7 @@
2637
DeadKernelError,
2738
)
2839
from .output_widget import OutputWidget
29-
from .util import ensure_async, run_sync, run_hook
40+
from .util import ensure_async, run_hook, run_sync
3041

3142

3243
def timestamp(msg: Optional[Dict] = None) -> str:
@@ -261,43 +272,85 @@ class NotebookClient(LoggingConfigurable):
261272

262273
kernel_manager_class: KernelManager = Type(config=True, help='The kernel manager class to use.')
263274

264-
on_execution_start: t.Optional[t.Callable] = Any(
275+
on_notebook_start: t.Optional[t.Callable] = Callable(
276+
default_value=None,
277+
allow_none=True,
278+
help=dedent(
279+
"""
280+
Called after the kernel manager and kernel client are setup, and cells
281+
are about to execute.
282+
Called with kwargs `notebook`
283+
"""
284+
),
285+
).tag(config=True)
286+
287+
on_notebook_complete: t.Optional[t.Callable] = Callable(
288+
default_value=None,
289+
allow_none=True,
290+
help=dedent(
291+
"""
292+
Called after kernel is cleaned up.
293+
Called with kwargs `notebook`
294+
"""
295+
),
296+
).tag(config=True)
297+
298+
on_notebook_error: t.Optional[t.Callable] = Callable(
299+
default_value=None,
300+
allow_none=True,
301+
help=dedent(
302+
"""
303+
Called when the notebook encountered an error.
304+
Called with kwargs `notebook`
305+
"""
306+
),
307+
).tag(config=True)
308+
309+
on_cell_start: t.Optional[t.Callable] = Callable(
265310
default_value=None,
266311
allow_none=True,
267-
help=dedent("""
268-
Called after the kernel manager and kernel client are setup, and cells
269-
are about to execute.
270-
Called with kwargs `kernel_id`.
271-
"""),
312+
help=dedent(
313+
"""
314+
A callable which executes before a cell is executed and before non-executing cells
315+
are skipped.
316+
Called with kwargs `cell` and `cell_index`.
317+
"""
318+
),
272319
).tag(config=True)
273320

274-
on_cell_start: t.Optional[t.Callable] = Any(
321+
on_cell_execute: t.Optional[t.Callable] = Callable(
275322
default_value=None,
276323
allow_none=True,
277-
help=dedent("""
278-
A callable which executes before a cell is executed.
279-
Called with kwargs `cell`, and `cell_index`.
280-
"""),
324+
help=dedent(
325+
"""
326+
A callable which executes just before a code cell is executed.
327+
Called with kwargs `cell` and `cell_index`.
328+
"""
329+
),
281330
).tag(config=True)
282331

283-
on_cell_complete: t.Optional[t.Callable] = Any(
332+
on_cell_complete: t.Optional[t.Callable] = Callable(
284333
default_value=None,
285334
allow_none=True,
286-
help=dedent("""
287-
A callable which executes after a cell execution is complete. It is
288-
called even when a cell results in a failure.
289-
Called with kwargs `cell`, and `cell_index`.
290-
"""),
335+
help=dedent(
336+
"""
337+
A callable which executes after a cell execution is complete. It is
338+
called even when a cell results in a failure.
339+
Called with kwargs `cell` and `cell_index`.
340+
"""
341+
),
291342
).tag(config=True)
292343

293-
on_cell_error: t.Optional[t.Callable] = Any(
344+
on_cell_error: t.Optional[t.Callable] = Callable(
294345
default_value=None,
295346
allow_none=True,
296-
help=dedent("""
297-
A callable which executes when a cell execution results in an error.
298-
This is executed even if errors are suppressed with `cell_allows_errors`.
299-
Called with kwargs `cell`, and `cell_index`.
300-
"""),
347+
help=dedent(
348+
"""
349+
A callable which executes when a cell execution results in an error.
350+
This is executed even if errors are suppressed with `cell_allows_errors`.
351+
Called with kwargs `cell` and `cell_index`.
352+
"""
353+
),
301354
).tag(config=True)
302355

303356
@default('kernel_manager_class')
@@ -478,10 +531,11 @@ async def async_start_new_kernel_client(self) -> KernelClient:
478531
try:
479532
await ensure_async(self.kc.wait_for_ready(timeout=self.startup_timeout))
480533
except RuntimeError:
534+
await run_hook(self.on_notebook_error, notebook=self.nb)
481535
await self._async_cleanup_kernel()
482536
raise
483537
self.kc.allow_stdin = False
484-
run_hook(sself.on_execution_start)
538+
await run_hook(self.on_notebook_start, notebook=self.nb)
485539
return self.kc
486540

487541
start_new_kernel_client = run_sync(async_start_new_kernel_client)
@@ -553,10 +607,13 @@ def on_signal():
553607
await self.async_start_new_kernel_client()
554608
try:
555609
yield
610+
except RuntimeError:
611+
await run_hook(self.on_notebook_error, notebook=self.nb)
612+
raise
556613
finally:
557614
if cleanup_kc:
558615
await self._async_cleanup_kernel()
559-
616+
await run_hook(self.on_notebook_complete, notebook=self.nb)
560617
atexit.unregister(self._cleanup_kernel)
561618
try:
562619
loop.remove_signal_handler(signal.SIGINT)
@@ -785,11 +842,9 @@ def _passed_deadline(self, deadline: int) -> bool:
785842
return True
786843
return False
787844

788-
def _check_raise_for_error(
789-
self,
790-
cell: NotebookNode,
791-
cell_index: int,
792-
exec_reply: t.Optional[t.Dict]) -> None:
845+
async def _check_raise_for_error(
846+
self, cell: NotebookNode, cell_index: int, exec_reply: t.Optional[t.Dict]
847+
) -> None:
793848

794849
if exec_reply is None:
795850
return None
@@ -803,11 +858,9 @@ def _check_raise_for_error(
803858
or exec_reply_content.get('ename') in self.allow_error_names
804859
or "raises-exception" in cell.metadata.get("tags", [])
805860
)
806-
807-
if (exec_reply is not None) and exec_reply['content']['status'] == 'error':
808-
run_hook(self.on_cell_error, cell=cell, cell_index=cell_index)
809-
if self.force_raise_errors or not cell_allows_errors:
810-
raise CellExecutionError.from_cell_and_msg(cell, exec_reply['content'])
861+
await run_hook(self.on_cell_error, cell=cell, cell_index=cell_index)
862+
if not cell_allows_errors:
863+
raise CellExecutionError.from_cell_and_msg(cell, exec_reply_content)
811864

812865
async def async_execute_cell(
813866
self,
@@ -850,6 +903,9 @@ async def async_execute_cell(
850903
The cell which was just processed.
851904
"""
852905
assert self.kc is not None
906+
907+
await run_hook(self.on_cell_start, cell=cell, cell_index=cell_index)
908+
853909
if cell.cell_type != 'code' or not cell.source.strip():
854910
self.log.debug("Skipping non-executing cell %s", cell_index)
855911
return cell
@@ -867,13 +923,13 @@ async def async_execute_cell(
867923
self.allow_errors or "raises-exception" in cell.metadata.get("tags", [])
868924
)
869925

870-
run_hook(self.on_cell_start, cell=cell, cell_index=cell_index)
926+
await run_hook(self.on_cell_execute, cell=cell, cell_index=cell_index)
871927
parent_msg_id = await ensure_async(
872928
self.kc.execute(
873929
cell.source, store_history=store_history, stop_on_error=not cell_allows_errors
874930
)
875931
)
876-
run_hook(self.on_cell_complete, cell=cell, cell_index=cell_index)
932+
await run_hook(self.on_cell_complete, cell=cell, cell_index=cell_index)
877933
# We launched a code cell to execute
878934
self.code_cells_executed += 1
879935
exec_timeout = self._get_timeout(cell)
@@ -907,7 +963,7 @@ async def async_execute_cell(
907963

908964
if execution_count:
909965
cell['execution_count'] = execution_count
910-
self._check_raise_for_error(cell, cell_index, exec_reply)
966+
await self._check_raise_for_error(cell, cell_index, exec_reply)
911967
self.nb['cells'][cell_index] = cell
912968
return cell
913969

0 commit comments

Comments
 (0)