15
15
from jupyter_client .client import KernelClient
16
16
from nbformat import NotebookNode
17
17
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
+ )
19
30
from traitlets .config .configurable import LoggingConfigurable
20
31
21
32
from .exceptions import (
26
37
DeadKernelError ,
27
38
)
28
39
from .output_widget import OutputWidget
29
- from .util import ensure_async , run_sync
40
+ from .util import ensure_async , run_hook , run_sync
30
41
31
42
32
43
def timestamp (msg : Optional [Dict ] = None ) -> str :
@@ -261,6 +272,87 @@ class NotebookClient(LoggingConfigurable):
261
272
262
273
kernel_manager_class : KernelManager = Type (config = True , help = 'The kernel manager class to use.' )
263
274
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
+
264
356
@default ('kernel_manager_class' )
265
357
def _kernel_manager_class_default (self ) -> KernelManager :
266
358
"""Use a dynamic default to avoid importing jupyter_client at startup"""
@@ -442,6 +534,7 @@ async def async_start_new_kernel_client(self) -> KernelClient:
442
534
await self ._async_cleanup_kernel ()
443
535
raise
444
536
self .kc .allow_stdin = False
537
+ await run_hook (self .on_notebook_start , notebook = self .nb )
445
538
return self .kc
446
539
447
540
start_new_kernel_client = run_sync (async_start_new_kernel_client )
@@ -513,10 +606,13 @@ def on_signal():
513
606
await self .async_start_new_kernel_client ()
514
607
try :
515
608
yield
609
+ except RuntimeError as e :
610
+ await run_hook (self .on_notebook_error , notebook = self .nb )
611
+ raise e
516
612
finally :
517
613
if cleanup_kc :
518
614
await self ._async_cleanup_kernel ()
519
-
615
+ await run_hook ( self . on_notebook_complete , notebook = self . nb )
520
616
atexit .unregister (self ._cleanup_kernel )
521
617
try :
522
618
loop .remove_signal_handler (signal .SIGINT )
@@ -745,7 +841,9 @@ def _passed_deadline(self, deadline: int) -> bool:
745
841
return True
746
842
return False
747
843
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 :
749
847
750
848
if exec_reply is None :
751
849
return None
@@ -759,7 +857,7 @@ def _check_raise_for_error(self, cell: NotebookNode, exec_reply: t.Optional[t.Di
759
857
or exec_reply_content .get ('ename' ) in self .allow_error_names
760
858
or "raises-exception" in cell .metadata .get ("tags" , [])
761
859
)
762
-
860
+ await run_hook ( self . on_cell_error , cell = cell , cell_index = cell_index )
763
861
if not cell_allows_errors :
764
862
raise CellExecutionError .from_cell_and_msg (cell , exec_reply_content )
765
863
@@ -804,6 +902,9 @@ async def async_execute_cell(
804
902
The cell which was just processed.
805
903
"""
806
904
assert self .kc is not None
905
+
906
+ await run_hook (self .on_cell_start , cell = cell , cell_index = cell_index )
907
+
807
908
if cell .cell_type != 'code' or not cell .source .strip ():
808
909
self .log .debug ("Skipping non-executing cell %s" , cell_index )
809
910
return cell
@@ -821,11 +922,13 @@ async def async_execute_cell(
821
922
self .allow_errors or "raises-exception" in cell .metadata .get ("tags" , [])
822
923
)
823
924
925
+ await run_hook (self .on_cell_execute , cell = cell , cell_index = cell_index )
824
926
parent_msg_id = await ensure_async (
825
927
self .kc .execute (
826
928
cell .source , store_history = store_history , stop_on_error = not cell_allows_errors
827
929
)
828
930
)
931
+ await run_hook (self .on_cell_complete , cell = cell , cell_index = cell_index )
829
932
# We launched a code cell to execute
830
933
self .code_cells_executed += 1
831
934
exec_timeout = self ._get_timeout (cell )
@@ -859,7 +962,7 @@ async def async_execute_cell(
859
962
860
963
if execution_count :
861
964
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 )
863
966
self .nb ['cells' ][cell_index ] = cell
864
967
return cell
865
968
0 commit comments