5
5
# For python 3.5 compatibility we import asynccontextmanager from async_generator instead of
6
6
# contextlib, and we `await yield_()` instead of just `yield`
7
7
from async_generator import asynccontextmanager , async_generator , yield_
8
+ from contextlib import contextmanager
8
9
9
10
from time import monotonic
10
11
from queue import Empty
15
16
16
17
from nbformat .v4 import output_from_msg
17
18
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
20
27
21
28
22
29
def timestamp ():
@@ -324,7 +331,28 @@ def start_kernel_manager(self):
324
331
self .km .client_class = 'jupyter_client.asynchronous.AsyncKernelClient'
325
332
return self .km
326
333
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 ):
328
356
"""Creates a new kernel client.
329
357
330
358
Parameters
@@ -346,22 +374,44 @@ async def start_new_kernel_client(self, **kwargs):
346
374
if self .km .ipykernel and self .ipython_hist_file :
347
375
self .extra_arguments += ['--HistoryManager.hist_file={}' .format (self .ipython_hist_file )]
348
376
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 )
350
378
351
379
self .kc = self .km .client ()
352
- self .kc .start_channels ( )
380
+ await await_or_block ( self .kc .start_channels )
353
381
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 )
355
383
except RuntimeError :
356
- self .kc .stop_channels ()
357
- await self .km .shutdown_kernel ()
384
+ await self ._async_cleanup_kernel ()
358
385
raise
359
386
self .kc .allow_stdin = False
360
387
return self .kc
361
388
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
+
362
412
@asynccontextmanager
363
413
@async_generator # needed for python 3.5 compatibility
364
- async def setup_kernel (self , ** kwargs ):
414
+ async def async_setup_kernel (self , ** kwargs ):
365
415
"""
366
416
Context manager for setting up the kernel to execute a notebook.
367
417
@@ -374,12 +424,11 @@ async def setup_kernel(self, **kwargs):
374
424
self .start_kernel_manager ()
375
425
376
426
if not self .km .has_kernel :
377
- await self .start_new_kernel_client (** kwargs )
427
+ await self .async_start_new_kernel_client (** kwargs )
378
428
try :
379
429
await yield_ (None ) # would just yield in python >3.5
380
430
finally :
381
- self .kc .stop_channels ()
382
- self .kc = None
431
+ await self ._async_cleanup_kernel ()
383
432
384
433
async def async_execute (self , ** kwargs ):
385
434
"""
@@ -392,15 +441,16 @@ async def async_execute(self, **kwargs):
392
441
"""
393
442
self .reset_execution_trackers ()
394
443
395
- async with self .setup_kernel (** kwargs ):
444
+ async with self .async_setup_kernel (** kwargs ):
396
445
self .log .info ("Executing notebook with kernel: %s" % self .kernel_name )
397
446
for index , cell in enumerate (self .nb .cells ):
398
447
# Ignore `'execution_count' in content` as it's always 1
399
448
# when store_history is False
400
449
await self .async_execute_cell (
401
450
cell , index , execution_count = self .code_cells_executed + 1
402
451
)
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 )
404
454
self .nb .metadata ['language_info' ] = info_msg ['content' ]['language_info' ]
405
455
self .set_widgets_metadata ()
406
456
@@ -450,12 +500,12 @@ def _update_display_id(self, display_id, msg):
450
500
outputs [output_idx ]['data' ] = out ['data' ]
451
501
outputs [output_idx ]['metadata' ] = out ['metadata' ]
452
502
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 ):
454
504
if timeout is not None :
455
505
deadline = monotonic () + timeout
456
506
while True :
457
507
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 )
459
509
if msg ['parent_header' ].get ('msg_id' ) == msg_id :
460
510
if self .record_timing :
461
511
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):
474
524
timeout = max (0 , deadline - monotonic ())
475
525
except Empty :
476
526
# 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 )
479
529
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 ):
481
531
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 )
483
533
if msg ['parent_header' ].get ('msg_id' ) == parent_msg_id :
484
534
try :
485
535
# Will raise CellExecutionComplete when completed
@@ -498,39 +548,42 @@ def _get_timeout(self, cell):
498
548
499
549
return timeout
500
550
501
- async def _handle_timeout (self , timeout , cell = None ):
551
+ async def _async_handle_timeout (self , timeout , cell = None ):
502
552
self .log .error ("Timeout waiting for execute reply (%is)." % timeout )
503
553
if self .interrupt_on_timeout :
504
554
self .log .error ("Interrupting kernel" )
505
- await self .km .interrupt_kernel ( )
555
+ await await_or_block ( self .km .interrupt_kernel )
506
556
else :
507
557
raise CellTimeoutError .error_from_timeout_and_cell (
508
558
"Cell execution timed out" , timeout , cell
509
559
)
510
560
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 ):
513
563
self .log .error ("Kernel died while waiting for execute reply." )
514
564
raise DeadKernelError ("Kernel died" )
515
565
516
- async def _wait_for_reply (self , msg_id , cell = None ):
566
+ async def async_wait_for_reply (self , msg_id , cell = None ):
517
567
# wait for finish, with timeout
518
568
timeout = self ._get_timeout (cell )
519
569
cummulative_time = 0
520
- self .shell_timeout_interval = 5
521
570
while True :
522
571
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 )
524
573
except Empty :
525
- await self ._check_alive ()
574
+ await self ._async_check_alive ()
526
575
cummulative_time += self .shell_timeout_interval
527
576
if timeout and cummulative_time > timeout :
528
- await self ._handle_timeout (timeout , cell )
577
+ await self ._async_async_handle_timeout (timeout , cell )
529
578
break
530
579
else :
531
580
if msg ['parent_header' ].get ('msg_id' ) == msg_id :
532
581
return msg
533
582
583
+ wait_for_reply = run_sync (async_wait_for_reply )
584
+ # Backwards compatability naming for papermill
585
+ _wait_for_reply = wait_for_reply
586
+
534
587
def _timeout_with_deadline (self , timeout , deadline ):
535
588
if deadline is not None and deadline - monotonic () < timeout :
536
589
timeout = deadline - monotonic ()
@@ -596,7 +649,7 @@ async def async_execute_cell(self, cell, cell_index, execution_count=None, store
596
649
cell ['metadata' ]['execution' ] = {}
597
650
598
651
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 ,
600
653
cell .source , store_history = store_history , stop_on_error = not self .allow_errors
601
654
)
602
655
# We launched a code cell to execute
@@ -607,11 +660,20 @@ async def async_execute_cell(self, cell, cell_index, execution_count=None, store
607
660
self .clear_before_next_output = False
608
661
609
662
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 )
614
664
)
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
615
677
616
678
if execution_count :
617
679
cell ['execution_count' ] = execution_count
0 commit comments