Skip to content

Commit 20081a6

Browse files
Merge pull request #188 from devintang3/client-hooks
Client hooks
2 parents 858e103 + 51c3ece commit 20081a6

File tree

6 files changed

+341
-8
lines changed

6 files changed

+341
-8
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: 109 additions & 6 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
40+
from .util import ensure_async, run_hook, run_sync
3041

3142

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

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

275+
on_notebook_start: t.Optional[t.Callable] = Callable(
276+
default_value=None,
277+
allow_none=True,
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+
),
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+
A callable which executes after the 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+
A callable which executes when the notebook encounters an error.
304+
Called with kwargs `notebook`.
305+
"""
306+
),
307+
).tag(config=True)
308+
309+
on_cell_start: t.Optional[t.Callable] = Callable(
310+
default_value=None,
311+
allow_none=True,
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+
),
354+
).tag(config=True)
355+
264356
@default('kernel_manager_class')
265357
def _kernel_manager_class_default(self) -> KernelManager:
266358
"""Use a dynamic default to avoid importing jupyter_client at startup"""
@@ -442,6 +534,7 @@ async def async_start_new_kernel_client(self) -> KernelClient:
442534
await self._async_cleanup_kernel()
443535
raise
444536
self.kc.allow_stdin = False
537+
await run_hook(self.on_notebook_start, notebook=self.nb)
445538
return self.kc
446539

447540
start_new_kernel_client = run_sync(async_start_new_kernel_client)
@@ -513,10 +606,13 @@ def on_signal():
513606
await self.async_start_new_kernel_client()
514607
try:
515608
yield
609+
except RuntimeError as e:
610+
await run_hook(self.on_notebook_error, notebook=self.nb)
611+
raise e
516612
finally:
517613
if cleanup_kc:
518614
await self._async_cleanup_kernel()
519-
615+
await run_hook(self.on_notebook_complete, notebook=self.nb)
520616
atexit.unregister(self._cleanup_kernel)
521617
try:
522618
loop.remove_signal_handler(signal.SIGINT)
@@ -745,7 +841,9 @@ def _passed_deadline(self, deadline: int) -> bool:
745841
return True
746842
return False
747843

748-
def _check_raise_for_error(self, cell: NotebookNode, 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:
749847

750848
if exec_reply is None:
751849
return None
@@ -759,7 +857,7 @@ def _check_raise_for_error(self, cell: NotebookNode, exec_reply: t.Optional[t.Di
759857
or exec_reply_content.get('ename') in self.allow_error_names
760858
or "raises-exception" in cell.metadata.get("tags", [])
761859
)
762-
860+
await run_hook(self.on_cell_error, cell=cell, cell_index=cell_index)
763861
if not cell_allows_errors:
764862
raise CellExecutionError.from_cell_and_msg(cell, exec_reply_content)
765863

@@ -804,6 +902,9 @@ async def async_execute_cell(
804902
The cell which was just processed.
805903
"""
806904
assert self.kc is not None
905+
906+
await run_hook(self.on_cell_start, cell=cell, cell_index=cell_index)
907+
807908
if cell.cell_type != 'code' or not cell.source.strip():
808909
self.log.debug("Skipping non-executing cell %s", cell_index)
809910
return cell
@@ -821,11 +922,13 @@ async def async_execute_cell(
821922
self.allow_errors or "raises-exception" in cell.metadata.get("tags", [])
822923
)
823924

925+
await run_hook(self.on_cell_execute, cell=cell, cell_index=cell_index)
824926
parent_msg_id = await ensure_async(
825927
self.kc.execute(
826928
cell.source, store_history=store_history, stop_on_error=not cell_allows_errors
827929
)
828930
)
931+
await run_hook(self.on_cell_complete, cell=cell, cell_index=cell_index)
829932
# We launched a code cell to execute
830933
self.code_cells_executed += 1
831934
exec_timeout = self._get_timeout(cell)
@@ -859,7 +962,7 @@ async def async_execute_cell(
859962

860963
if execution_count:
861964
cell['execution_count'] = execution_count
862-
self._check_raise_for_error(cell, exec_reply)
965+
await self._check_raise_for_error(cell, cell_index, exec_reply)
863966
self.nb['cells'][cell_index] = cell
864967
return cell
865968

0 commit comments

Comments
 (0)