1515from jupyter_client .client import KernelClient
1616from nbformat import NotebookNode
1717from 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+ )
1930from traitlets .config .configurable import LoggingConfigurable
2031
2132from .exceptions import (
2637 DeadKernelError ,
2738)
2839from .output_widget import OutputWidget
29- from .util import ensure_async , run_sync
40+ from .util import ensure_async , run_hook , run_sync
3041
3142
3243def 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