55# For python 3.5 compatibility we import asynccontextmanager from async_generator instead of
66# contextlib, and we `await yield_()` instead of just `yield`
77from async_generator import asynccontextmanager , async_generator , yield_
8+ from contextlib import contextmanager
89
910from time import monotonic
1011from queue import Empty
1516
1617from nbformat .v4 import output_from_msg
1718
18- from .exceptions import CellTimeoutError , DeadKernelError , CellExecutionComplete , CellExecutionError
19- from .util import run_sync
19+ from .exceptions import (
20+ CellControlSignal ,
21+ CellTimeoutError ,
22+ DeadKernelError ,
23+ CellExecutionComplete ,
24+ CellExecutionError
25+ )
26+ from .util import run_sync , await_or_block
2027
2128
2229def timestamp ():
@@ -324,7 +331,28 @@ def start_kernel_manager(self):
324331 self .km .client_class = 'jupyter_client.asynchronous.AsyncKernelClient'
325332 return self .km
326333
327- async def start_new_kernel_client (self , ** kwargs ):
334+ async def _async_cleanup_kernel (self ):
335+ try :
336+ # Send a polite shutdown request
337+ await await_or_block (self .kc .shutdown )
338+ try :
339+ # Queue the manager to kill the process, sometimes the built-in and above
340+ # shutdowns have not been successful or called yet, so give a direct kill
341+ # call here and recover gracefully if it's already dead.
342+ await await_or_block (self .km .shutdown_kernel , now = True )
343+ except RuntimeError as e :
344+ # The error isn't specialized, so we have to check the message
345+ if 'No kernel is running!' not in str (e ):
346+ raise
347+ finally :
348+ # Remove any state left over even if we failed to stop the kernel
349+ await await_or_block (self .km .cleanup )
350+ await await_or_block (self .kc .stop_channels )
351+ self .kc = None
352+
353+ _cleanup_kernel = run_sync (_async_cleanup_kernel )
354+
355+ async def async_start_new_kernel_client (self , ** kwargs ):
328356 """Creates a new kernel client.
329357
330358 Parameters
@@ -346,22 +374,44 @@ async def start_new_kernel_client(self, **kwargs):
346374 if self .km .ipykernel and self .ipython_hist_file :
347375 self .extra_arguments += ['--HistoryManager.hist_file={}' .format (self .ipython_hist_file )]
348376
349- await self .km .start_kernel ( extra_arguments = self .extra_arguments , ** kwargs )
377+ await await_or_block ( self .km .start_kernel , extra_arguments = self .extra_arguments , ** kwargs )
350378
351379 self .kc = self .km .client ()
352- self .kc .start_channels ( )
380+ await await_or_block ( self .kc .start_channels )
353381 try :
354- await self .kc .wait_for_ready ( timeout = self .startup_timeout )
382+ await await_or_block ( self .kc .wait_for_ready , timeout = self .startup_timeout )
355383 except RuntimeError :
356- self .kc .stop_channels ()
357- await self .km .shutdown_kernel ()
384+ await self ._async_cleanup_kernel ()
358385 raise
359386 self .kc .allow_stdin = False
360387 return self .kc
361388
389+ start_new_kernel_client = run_sync (async_start_new_kernel_client )
390+
391+ @contextmanager
392+ def setup_kernel (self , ** kwargs ):
393+ """
394+ Context manager for setting up the kernel to execute a notebook.
395+
396+ The assigns the Kernel Manager (`self.km`) if missing and Kernel Client(`self.kc`).
397+
398+ When control returns from the yield it stops the client's zmq channels, and shuts
399+ down the kernel.
400+ """
401+ # Can't use run_until_complete on an asynccontextmanager function :(
402+ if self .km is None :
403+ self .start_kernel_manager ()
404+
405+ if not self .km .has_kernel :
406+ self .start_new_kernel_client (** kwargs )
407+ try :
408+ yield
409+ finally :
410+ self ._cleanup_kernel ()
411+
362412 @asynccontextmanager
363413 @async_generator # needed for python 3.5 compatibility
364- async def setup_kernel (self , ** kwargs ):
414+ async def async_setup_kernel (self , ** kwargs ):
365415 """
366416 Context manager for setting up the kernel to execute a notebook.
367417
@@ -374,12 +424,11 @@ async def setup_kernel(self, **kwargs):
374424 self .start_kernel_manager ()
375425
376426 if not self .km .has_kernel :
377- await self .start_new_kernel_client (** kwargs )
427+ await self .async_start_new_kernel_client (** kwargs )
378428 try :
379429 await yield_ (None ) # would just yield in python >3.5
380430 finally :
381- self .kc .stop_channels ()
382- self .kc = None
431+ await self ._async_cleanup_kernel ()
383432
384433 async def async_execute (self , ** kwargs ):
385434 """
@@ -392,15 +441,16 @@ async def async_execute(self, **kwargs):
392441 """
393442 self .reset_execution_trackers ()
394443
395- async with self .setup_kernel (** kwargs ):
444+ async with self .async_setup_kernel (** kwargs ):
396445 self .log .info ("Executing notebook with kernel: %s" % self .kernel_name )
397446 for index , cell in enumerate (self .nb .cells ):
398447 # Ignore `'execution_count' in content` as it's always 1
399448 # when store_history is False
400449 await self .async_execute_cell (
401450 cell , index , execution_count = self .code_cells_executed + 1
402451 )
403- info_msg = await self ._wait_for_reply (self .kc .kernel_info ())
452+ msg_id = await await_or_block (self .kc .kernel_info )
453+ info_msg = await self .async_wait_for_reply (msg_id )
404454 self .nb .metadata ['language_info' ] = info_msg ['content' ]['language_info' ]
405455 self .set_widgets_metadata ()
406456
@@ -450,12 +500,12 @@ def _update_display_id(self, display_id, msg):
450500 outputs [output_idx ]['data' ] = out ['data' ]
451501 outputs [output_idx ]['metadata' ] = out ['metadata' ]
452502
453- async def _poll_for_reply (self , msg_id , cell , timeout , task_poll_output_msg ):
503+ async def _async_poll_for_reply (self , msg_id , cell , timeout , task_poll_output_msg ):
454504 if timeout is not None :
455505 deadline = monotonic () + timeout
456506 while True :
457507 try :
458- msg = await self .kc .shell_channel .get_msg ( timeout = timeout )
508+ msg = await await_or_block ( self .kc .shell_channel .get_msg , timeout = timeout )
459509 if msg ['parent_header' ].get ('msg_id' ) == msg_id :
460510 if self .record_timing :
461511 cell ['metadata' ]['execution' ]['shell.execute_reply' ] = timestamp ()
@@ -474,12 +524,12 @@ async def _poll_for_reply(self, msg_id, cell, timeout, task_poll_output_msg):
474524 timeout = max (0 , deadline - monotonic ())
475525 except Empty :
476526 # received no message, check if kernel is still alive
477- await self ._check_alive ()
478- await self ._handle_timeout (timeout , cell )
527+ await self ._async_check_alive ()
528+ await self ._async_handle_timeout (timeout , cell )
479529
480- async def _poll_output_msg (self , parent_msg_id , cell , cell_index ):
530+ async def _async_poll_output_msg (self , parent_msg_id , cell , cell_index ):
481531 while True :
482- msg = await self .kc .iopub_channel .get_msg ( timeout = None )
532+ msg = await await_or_block ( self .kc .iopub_channel .get_msg , timeout = None )
483533 if msg ['parent_header' ].get ('msg_id' ) == parent_msg_id :
484534 try :
485535 # Will raise CellExecutionComplete when completed
@@ -498,39 +548,42 @@ def _get_timeout(self, cell):
498548
499549 return timeout
500550
501- async def _handle_timeout (self , timeout , cell = None ):
551+ async def _async_handle_timeout (self , timeout , cell = None ):
502552 self .log .error ("Timeout waiting for execute reply (%is)." % timeout )
503553 if self .interrupt_on_timeout :
504554 self .log .error ("Interrupting kernel" )
505- await self .km .interrupt_kernel ( )
555+ await await_or_block ( self .km .interrupt_kernel )
506556 else :
507557 raise CellTimeoutError .error_from_timeout_and_cell (
508558 "Cell execution timed out" , timeout , cell
509559 )
510560
511- async def _check_alive (self ):
512- if not await self .kc .is_alive ( ):
561+ async def _async_check_alive (self ):
562+ if not await await_or_block ( self .kc .is_alive ):
513563 self .log .error ("Kernel died while waiting for execute reply." )
514564 raise DeadKernelError ("Kernel died" )
515565
516- async def _wait_for_reply (self , msg_id , cell = None ):
566+ async def async_wait_for_reply (self , msg_id , cell = None ):
517567 # wait for finish, with timeout
518568 timeout = self ._get_timeout (cell )
519569 cummulative_time = 0
520- self .shell_timeout_interval = 5
521570 while True :
522571 try :
523- msg = await self .kc .shell_channel .get_msg ( timeout = self .shell_timeout_interval )
572+ msg = await await_or_block ( self .kc .shell_channel .get_msg , timeout = self .shell_timeout_interval )
524573 except Empty :
525- await self ._check_alive ()
574+ await self ._async_check_alive ()
526575 cummulative_time += self .shell_timeout_interval
527576 if timeout and cummulative_time > timeout :
528- await self ._handle_timeout (timeout , cell )
577+ await self ._async_async_handle_timeout (timeout , cell )
529578 break
530579 else :
531580 if msg ['parent_header' ].get ('msg_id' ) == msg_id :
532581 return msg
533582
583+ wait_for_reply = run_sync (async_wait_for_reply )
584+ # Backwards compatability naming for papermill
585+ _wait_for_reply = wait_for_reply
586+
534587 def _timeout_with_deadline (self , timeout , deadline ):
535588 if deadline is not None and deadline - monotonic () < timeout :
536589 timeout = deadline - monotonic ()
@@ -596,7 +649,7 @@ async def async_execute_cell(self, cell, cell_index, execution_count=None, store
596649 cell ['metadata' ]['execution' ] = {}
597650
598651 self .log .debug ("Executing cell:\n %s" , cell .source )
599- parent_msg_id = self .kc .execute (
652+ parent_msg_id = await await_or_block ( self .kc .execute ,
600653 cell .source , store_history = store_history , stop_on_error = not self .allow_errors
601654 )
602655 # We launched a code cell to execute
@@ -607,11 +660,20 @@ async def async_execute_cell(self, cell, cell_index, execution_count=None, store
607660 self .clear_before_next_output = False
608661
609662 task_poll_output_msg = asyncio .ensure_future (
610- self ._poll_output_msg (parent_msg_id , cell , cell_index )
611- )
612- exec_reply = await self ._poll_for_reply (
613- parent_msg_id , cell , exec_timeout , task_poll_output_msg
663+ self ._async_poll_output_msg (parent_msg_id , cell , cell_index )
614664 )
665+ try :
666+ exec_reply = await self ._async_poll_for_reply (
667+ parent_msg_id , cell , exec_timeout , task_poll_output_msg
668+ )
669+ except Exception as e :
670+ # Best effort to cancel request if it hasn't been resolved
671+ try :
672+ # Check if the task_poll_output is doing the raising for us
673+ if not isinstance (e , CellControlSignal ):
674+ task_poll_output_msg .cancel ()
675+ finally :
676+ raise
615677
616678 if execution_count :
617679 cell ['execution_count' ] = execution_count
0 commit comments