Skip to content

Commit 51c3ece

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 51c3ece

File tree

6 files changed

+322
-50
lines changed

6 files changed

+322
-50
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 notebook or cell execution
100+
-------------------------------------------------
101+
There are several configurable hooks that allow the user to execute code before and
102+
after a notebook or a cell is executed. Each one is configured with a function that will be called in its
103+
respective place in the 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: 95 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(
265276
default_value=None,
266277
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-
"""),
278+
help=dedent(
279+
"""
280+
A callable which executes after the kernel manager and kernel client are setup, and
281+
cells are about to execute.
282+
Called with kwargs `notebook`.
283+
"""
284+
),
272285
).tag(config=True)
273286

274-
on_cell_start: t.Optional[t.Callable] = Any(
287+
on_notebook_complete: t.Optional[t.Callable] = Callable(
275288
default_value=None,
276289
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-
"""),
290+
help=dedent(
291+
"""
292+
A callable which executes after the kernel is cleaned up.
293+
Called with kwargs `notebook`.
294+
"""
295+
),
281296
).tag(config=True)
282297

283-
on_cell_complete: t.Optional[t.Callable] = Any(
298+
on_notebook_error: t.Optional[t.Callable] = Callable(
284299
default_value=None,
285300
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-
"""),
301+
help=dedent(
302+
"""
303+
A callable which executes when the notebook encounters an error.
304+
Called with kwargs `notebook`.
305+
"""
306+
),
291307
).tag(config=True)
292308

293-
on_cell_error: t.Optional[t.Callable] = Any(
309+
on_cell_start: t.Optional[t.Callable] = Callable(
294310
default_value=None,
295311
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-
"""),
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+
),
319+
).tag(config=True)
320+
321+
on_cell_execute: t.Optional[t.Callable] = Callable(
322+
default_value=None,
323+
allow_none=True,
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+
),
330+
).tag(config=True)
331+
332+
on_cell_complete: t.Optional[t.Callable] = Callable(
333+
default_value=None,
334+
allow_none=True,
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+
),
342+
).tag(config=True)
343+
344+
on_cell_error: t.Optional[t.Callable] = Callable(
345+
default_value=None,
346+
allow_none=True,
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')
@@ -481,7 +534,7 @@ async def async_start_new_kernel_client(self) -> KernelClient:
481534
await self._async_cleanup_kernel()
482535
raise
483536
self.kc.allow_stdin = False
484-
run_hook(sself.on_execution_start)
537+
await run_hook(self.on_notebook_start, notebook=self.nb)
485538
return self.kc
486539

487540
start_new_kernel_client = run_sync(async_start_new_kernel_client)
@@ -553,10 +606,13 @@ def on_signal():
553606
await self.async_start_new_kernel_client()
554607
try:
555608
yield
609+
except RuntimeError as e:
610+
await run_hook(self.on_notebook_error, notebook=self.nb)
611+
raise e
556612
finally:
557613
if cleanup_kc:
558614
await self._async_cleanup_kernel()
559-
615+
await run_hook(self.on_notebook_complete, notebook=self.nb)
560616
atexit.unregister(self._cleanup_kernel)
561617
try:
562618
loop.remove_signal_handler(signal.SIGINT)
@@ -785,11 +841,9 @@ def _passed_deadline(self, deadline: int) -> bool:
785841
return True
786842
return False
787843

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

794848
if exec_reply is None:
795849
return None
@@ -803,11 +857,9 @@ def _check_raise_for_error(
803857
or exec_reply_content.get('ename') in self.allow_error_names
804858
or "raises-exception" in cell.metadata.get("tags", [])
805859
)
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'])
860+
await run_hook(self.on_cell_error, cell=cell, cell_index=cell_index)
861+
if not cell_allows_errors:
862+
raise CellExecutionError.from_cell_and_msg(cell, exec_reply_content)
811863

812864
async def async_execute_cell(
813865
self,
@@ -850,6 +902,9 @@ async def async_execute_cell(
850902
The cell which was just processed.
851903
"""
852904
assert self.kc is not None
905+
906+
await run_hook(self.on_cell_start, cell=cell, cell_index=cell_index)
907+
853908
if cell.cell_type != 'code' or not cell.source.strip():
854909
self.log.debug("Skipping non-executing cell %s", cell_index)
855910
return cell
@@ -867,13 +922,13 @@ async def async_execute_cell(
867922
self.allow_errors or "raises-exception" in cell.metadata.get("tags", [])
868923
)
869924

870-
run_hook(self.on_cell_start, cell=cell, cell_index=cell_index)
925+
await run_hook(self.on_cell_execute, cell=cell, cell_index=cell_index)
871926
parent_msg_id = await ensure_async(
872927
self.kc.execute(
873928
cell.source, store_history=store_history, stop_on_error=not cell_allows_errors
874929
)
875930
)
876-
run_hook(self.on_cell_complete, cell=cell, cell_index=cell_index)
931+
await run_hook(self.on_cell_complete, cell=cell, cell_index=cell_index)
877932
# We launched a code cell to execute
878933
self.code_cells_executed += 1
879934
exec_timeout = self._get_timeout(cell)
@@ -907,7 +962,7 @@ async def async_execute_cell(
907962

908963
if execution_count:
909964
cell['execution_count'] = execution_count
910-
self._check_raise_for_error(cell, cell_index, exec_reply)
965+
await self._check_raise_for_error(cell, cell_index, exec_reply)
911966
self.nb['cells'][cell_index] = cell
912967
return cell
913968

0 commit comments

Comments
 (0)