Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jupyter/nbclient
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1c923a202f3ade66e771911c4ab7e1845a8d718e
Choose a base ref
..
head repository: jupyter/nbclient
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: b9faf64ea3076ecf0bca7d5e641c7a20adbf4943
Choose a head ref
Showing with 60 additions and 35 deletions.
  1. +13 −1 docs/changelog.md
  2. +1 −1 nbclient/_version.py
  3. +39 −33 nbclient/client.py
  4. +7 −0 nbclient/util.py
14 changes: 13 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
# Changelog

## 0.4.0 (unreleased)
## 0.4.0

### Major Changes

- Use KernelManager's graceful shutdown rather than KILLing kernels [#64](https://github.com/jupyter/nbclient/pull/64)
- Mimic an Output widget at the frontend so that the Output widget behaves correctly [#68](https://github.com/jupyter/nbclient/pull/68)
- Nested asyncio is automatic, and works with Tornado [#71](https://github.com/jupyter/nbclient/pull/71)
- `async_execute` now has a `reset_kc` argument to control if the client is reset upon execution request [#53](https://github.com/jupyter/nbclient/pull/53)

### Fixes

- Fix `OSError: [WinError 6] The handle is invalid` for windows/python<3.7 [#77](https://github.com/jupyter/nbclient/pull/77)
- Async wapper Exceptions no longer loose thier caused exception information [#65](https://github.com/jupyter/nbclient/pull/65)
- `extra_arguments` are now configurable by config settings [#66](https://github.com/jupyter/nbclient/pull/66)

### Operational

- Cross-OS testing now run on PRs via Github Actions [#63](https://github.com/jupyter/nbclient/pull/63)

## 0.3.1

2 changes: 1 addition & 1 deletion nbclient/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version = '0.4.0a0'
version = '0.4.0'
72 changes: 39 additions & 33 deletions nbclient/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import atexit
import collections
import datetime
import enum
import base64
import signal
from textwrap import dedent
@@ -25,23 +24,14 @@
CellExecutionComplete,
CellExecutionError
)
from .util import run_sync, ensure_async
from .util import run_sync, ensure_async, run_hook
from .output_widget import OutputWidget


def timestamp():
return datetime.datetime.utcnow().isoformat() + 'Z'


class ExecutionState(enum.Enum):
IDLE = 0
STARTING_UP = 1
STARTING_KERNEL_CLIENT = 2
EXECUTING = 3
CLEANING_UP = 4
COMPLETE = 5


class NotebookClient(LoggingConfigurable):
"""
Encompasses a Client for executing cells in a notebook
@@ -233,6 +223,35 @@ class NotebookClient(LoggingConfigurable):

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

on_kernel_create = Any(
default_value=None,
allow_none=True,
help="""A callable which executes when the kernel is created.""",
).tag(config=True)

on_cell_start = Any(
default_value=None,
allow_none=True,
help="""A callable which executes before a cell is executed.""",
).tag(config=True)

on_cell_complete = Any(
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.
"""
),
).tag(config=True)

on_cell_error = Any(
default_value=None,
allow_none=True,
help="""A callable which executes when a cell execution results in an error.""",
).tag(config=True)

@default('kernel_manager_class')
def _kernel_manager_class_default(self):
"""Use a dynamic default to avoid importing jupyter_client at startup"""
@@ -308,9 +327,6 @@ def __init__(self, nb, km=None, **kw):
'jupyter.widget': self.on_comm_open_jupyter_widget
}

def _update_state(self, new_state):
self.state_history.append((new_state, timestamp()))

def reset_execution_trackers(self):
"""Resets any per-execution trackers.
"""
@@ -323,10 +339,6 @@ def reset_execution_trackers(self):
self.output_hook_stack = collections.defaultdict(list)
# our front-end mimicing Output widgets
self.comm_objects = {}
self.state_history = []
self._update_state(ExecutionState.IDLE)
self.current_cell = None
self.current_cell_index = -1

def start_kernel_manager(self):
"""Creates a new kernel manager.
@@ -349,7 +361,6 @@ def start_kernel_manager(self):
return self.km

async def _async_cleanup_kernel(self):
self._update_state(ExecutionState.CLEANING_UP)
now = self.shutdown_kernel == "immediate"
try:
# Queue the manager to kill the process, and recover gracefully if it's already dead.
@@ -385,8 +396,6 @@ async def async_start_new_kernel_client(self, **kwargs):
kernel_id : string-ized version 4 uuid
The id of the started kernel.
"""

self._update_state(ExecutionState.STARTING_KERNEL_CLIENT)
resource_path = self.resources.get('metadata', {}).get('path') or None
if resource_path and 'cwd' not in kwargs:
kwargs["cwd"] = resource_path
@@ -398,6 +407,7 @@ async def async_start_new_kernel_client(self, **kwargs):

kernel_id = await ensure_async(self.km.start_kernel(extra_arguments=self.extra_arguments,
**kwargs))
run_hook(self.on_kernel_create, kernel_id)

# if self.km is not a KernelManager, it's probably a MultiKernelManager
try:
@@ -519,10 +529,8 @@ async def async_execute(self, reset_kc=False, **kwargs):
if reset_kc and self.km:
await self._async_cleanup_kernel()
self.reset_execution_trackers()
self._update_state(ExecutionState.STARTING_UP)

async with self.async_setup_kernel(**kwargs):
self._update_state(ExecutionState.EXECUTING)
self.log.info("Executing notebook with kernel: %s" % self.kernel_name)
for index, cell in enumerate(self.nb.cells):
# Ignore `'execution_count' in content` as it's always 1
@@ -534,8 +542,7 @@ async def async_execute(self, reset_kc=False, **kwargs):
info_msg = await self.async_wait_for_reply(msg_id)
self.nb.metadata['language_info'] = info_msg['content']['language_info']
self.set_widgets_metadata()
self._update_state(ExecutionState.COMPLETE)
self._update_state(ExecutionState.IDLE)

return self.nb

execute = run_sync(async_execute)
@@ -684,14 +691,15 @@ def _passed_deadline(self, deadline):
return True
return False

def _check_raise_for_error(self, cell, exec_reply):
def _check_raise_for_error(self, cell, cell_index, exec_reply):
cell_allows_errors = self.allow_errors or "raises-exception" in cell.metadata.get(
"tags", []
)

if self.force_raise_errors or not cell_allows_errors:
if (exec_reply is not None) and exec_reply['content']['status'] == 'error':
raise CellExecutionError.from_cell_and_msg(cell, exec_reply['content'])
if (exec_reply is not None) and exec_reply['content']['status'] == 'error':
run_hook(self.on_cell_error, cell, cell_index)
if self.force_raise_errors or not cell_allows_errors:
raise CellExecutionError.from_cell_and_msg(cell, exec_reply['content'])

async def async_execute_cell(self, cell, cell_index, execution_count=None, store_history=True):
"""
@@ -734,10 +742,8 @@ async def async_execute_cell(self, cell, cell_index, execution_count=None, store
if self.record_timing and 'execution' not in cell['metadata']:
cell['metadata']['execution'] = {}

self.current_cell = cell
self.current_cell_index = cell_index

self.log.debug("Executing cell:\n%s", cell.source)
run_hook(self.on_cell_start, cell, cell_index)
parent_msg_id = await ensure_async(
self.kc.execute(
cell.source,
@@ -770,7 +776,7 @@ async def async_execute_cell(self, cell, cell_index, execution_count=None, store

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

7 changes: 7 additions & 0 deletions nbclient/util.py
Original file line number Diff line number Diff line change
@@ -89,3 +89,10 @@ async def ensure_async(obj):
return result
# obj doesn't need to be awaited
return obj


def run_hook(hook, *args):
if hook is None:
return
loop = asyncio.get_event_loop()
loop.call_soon(hook, *args)