Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client hooks #188

Merged
merged 2 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,36 @@ on both versions. Here the traitlet ``kernel_name`` helps simplify and
maintain consistency: we can just run a notebook twice, specifying first
"python2" and then "python3" as the kernel name.

Hooks before and after notebook or cell execution
-------------------------------------------------
There are several configurable hooks that allow the user to execute code before and
after a notebook or a cell is executed. Each one is configured with a function that will be called in its
respective place in the execution pipeline.
Each is described below:

**Notebook-level hooks**: These hooks are called with a single extra parameter:

- ``notebook=NotebookNode``: the current notebook being executed.

Here is the available hooks:

- ``on_notebook_start`` will run when the notebook client is initialized, before any execution has happened.
- ``on_notebook_complete`` will run when the notebook client has finished executing, after kernel cleanup.
- ``on_notebook_error`` will run when the notebook client has encountered an exception before kernel cleanup.

**Cell-level hooks**: These hooks are called with two parameters:

- ``cell=NotebookNode``: a reference to the current cell.
- ``cell_index=int``: the index of the cell in the current notebook's list of cells.

Here are the available hooks:

- ``on_cell_start`` will run for all cell types before the cell is executed.
- ``on_cell_execute`` will run right before the code cell is executed.
- ``on_cell_complete`` will run after execution, if the cell is executed with no errors.
- ``on_cell_error`` will run if there is an error during cell execution.


Handling errors and exceptions
------------------------------

Expand Down
115 changes: 109 additions & 6 deletions nbclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@
from jupyter_client.client import KernelClient
from nbformat import NotebookNode
from nbformat.v4 import output_from_msg
from traitlets import Any, Bool, Dict, Enum, Integer, List, Type, Unicode, default
from traitlets import (
Any,
Bool,
Callable,
Dict,
Enum,
Integer,
List,
Type,
Unicode,
default,
)
from traitlets.config.configurable import LoggingConfigurable

from .exceptions import (
Expand All @@ -26,7 +37,7 @@
DeadKernelError,
)
from .output_widget import OutputWidget
from .util import ensure_async, run_sync
from .util import ensure_async, run_hook, run_sync


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

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

on_notebook_start: t.Optional[t.Callable] = Callable(
default_value=None,
allow_none=True,
help=dedent(
"""
A callable which executes after the kernel manager and kernel client are setup, and
cells are about to execute.
Called with kwargs `notebook`.
"""
),
).tag(config=True)

on_notebook_complete: t.Optional[t.Callable] = Callable(
default_value=None,
allow_none=True,
help=dedent(
"""
A callable which executes after the kernel is cleaned up.
Called with kwargs `notebook`.
"""
),
).tag(config=True)

on_notebook_error: t.Optional[t.Callable] = Callable(
default_value=None,
allow_none=True,
help=dedent(
"""
A callable which executes when the notebook encounters an error.
Called with kwargs `notebook`.
"""
),
).tag(config=True)

on_cell_start: t.Optional[t.Callable] = Callable(
default_value=None,
allow_none=True,
help=dedent(
"""
A callable which executes before a cell is executed and before non-executing cells
are skipped.
Called with kwargs `cell` and `cell_index`.
"""
),
).tag(config=True)

on_cell_execute: t.Optional[t.Callable] = Callable(
default_value=None,
allow_none=True,
help=dedent(
"""
A callable which executes just before a code cell is executed.
Called with kwargs `cell` and `cell_index`.
"""
),
).tag(config=True)

on_cell_complete: t.Optional[t.Callable] = Callable(
default_value=None,
allow_none=True,
help=dedent(
"""
A callable which executes after a cell execution is complete. It is
called even when a cell results in a failure.
Called with kwargs `cell` and `cell_index`.
"""
),
).tag(config=True)

on_cell_error: t.Optional[t.Callable] = Callable(
default_value=None,
allow_none=True,
help=dedent(
"""
A callable which executes when a cell execution results in an error.
This is executed even if errors are suppressed with `cell_allows_errors`.
Called with kwargs `cell` and `cell_index`.
"""
),
).tag(config=True)

@default('kernel_manager_class')
def _kernel_manager_class_default(self) -> KernelManager:
"""Use a dynamic default to avoid importing jupyter_client at startup"""
Expand Down Expand Up @@ -442,6 +534,7 @@ async def async_start_new_kernel_client(self) -> KernelClient:
await self._async_cleanup_kernel()
raise
self.kc.allow_stdin = False
await run_hook(self.on_notebook_start, notebook=self.nb)
return self.kc

start_new_kernel_client = run_sync(async_start_new_kernel_client)
Expand Down Expand Up @@ -513,10 +606,13 @@ def on_signal():
await self.async_start_new_kernel_client()
try:
yield
except RuntimeError as e:
await run_hook(self.on_notebook_error, notebook=self.nb)
raise e
finally:
if cleanup_kc:
await self._async_cleanup_kernel()

await run_hook(self.on_notebook_complete, notebook=self.nb)
atexit.unregister(self._cleanup_kernel)
try:
loop.remove_signal_handler(signal.SIGINT)
Expand Down Expand Up @@ -745,7 +841,9 @@ def _passed_deadline(self, deadline: int) -> bool:
return True
return False

def _check_raise_for_error(self, cell: NotebookNode, exec_reply: t.Optional[t.Dict]) -> None:
async def _check_raise_for_error(
self, cell: NotebookNode, cell_index: int, exec_reply: t.Optional[t.Dict]
) -> None:

if exec_reply is None:
return None
Expand All @@ -759,7 +857,7 @@ def _check_raise_for_error(self, cell: NotebookNode, exec_reply: t.Optional[t.Di
or exec_reply_content.get('ename') in self.allow_error_names
or "raises-exception" in cell.metadata.get("tags", [])
)

await run_hook(self.on_cell_error, cell=cell, cell_index=cell_index)
if not cell_allows_errors:
raise CellExecutionError.from_cell_and_msg(cell, exec_reply_content)

Expand Down Expand Up @@ -804,6 +902,9 @@ async def async_execute_cell(
The cell which was just processed.
"""
assert self.kc is not None

await run_hook(self.on_cell_start, cell=cell, cell_index=cell_index)

if cell.cell_type != 'code' or not cell.source.strip():
self.log.debug("Skipping non-executing cell %s", cell_index)
return cell
Expand All @@ -821,11 +922,13 @@ async def async_execute_cell(
self.allow_errors or "raises-exception" in cell.metadata.get("tags", [])
)

await run_hook(self.on_cell_execute, cell=cell, cell_index=cell_index)
parent_msg_id = await ensure_async(
self.kc.execute(
cell.source, store_history=store_history, stop_on_error=not cell_allows_errors
)
)
await run_hook(self.on_cell_complete, cell=cell, cell_index=cell_index)
# We launched a code cell to execute
self.code_cells_executed += 1
exec_timeout = self._get_timeout(cell)
Expand Down Expand Up @@ -859,7 +962,7 @@ async def async_execute_cell(

if execution_count:
cell['execution_count'] = execution_count
self._check_raise_for_error(cell, exec_reply)
await self._check_raise_for_error(cell, cell_index, exec_reply)
self.nb['cells'][cell_index] = cell
return cell

Expand Down
Loading