Skip to content

Commit b411883

Browse files
committed
Rebased with master and added tests
1 parent f5b4636 commit b411883

File tree

2 files changed

+105
-19
lines changed

2 files changed

+105
-19
lines changed

nbclient/client.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
DeadKernelError,
2626
)
2727
from .output_widget import OutputWidget
28-
from .util import ensure_async, run_sync, run_hook
28+
from .util import ensure_async, run_hook, run_sync
2929

3030

3131
def timestamp() -> str:
@@ -248,40 +248,47 @@ class NotebookClient(LoggingConfigurable):
248248
on_execution_start: t.Optional[t.Callable] = Any(
249249
default_value=None,
250250
allow_none=True,
251-
help=dedent("""
251+
help=dedent(
252+
"""
252253
Called after the kernel manager and kernel client are setup, and cells
253254
are about to execute.
254-
Called with kwargs `kernel_id`.
255-
"""),
255+
"""
256+
),
256257
).tag(config=True)
257258

258259
on_cell_start: t.Optional[t.Callable] = Any(
259260
default_value=None,
260261
allow_none=True,
261-
help=dedent("""
262+
help=dedent(
263+
"""
262264
A callable which executes before a cell is executed.
263265
Called with kwargs `cell`, and `cell_index`.
264-
"""),
266+
"""
267+
),
265268
).tag(config=True)
266269

267270
on_cell_complete: t.Optional[t.Callable] = Any(
268271
default_value=None,
269272
allow_none=True,
270-
help=dedent("""
273+
help=dedent(
274+
"""
271275
A callable which executes after a cell execution is complete. It is
272276
called even when a cell results in a failure.
273277
Called with kwargs `cell`, and `cell_index`.
274-
"""),
278+
"""
279+
),
275280
).tag(config=True)
276281

277282
on_cell_error: t.Optional[t.Callable] = Any(
278283
default_value=None,
279284
allow_none=True,
280-
help=dedent("""
285+
help=dedent(
286+
"""
281287
A callable which executes when a cell execution results in an error.
282288
This is executed even if errors are suppressed with `cell_allows_errors`.
283289
Called with kwargs `cell`, and `cell_index`.
284-
"""),
290+
"""
291+
),
285292
).tag(config=True)
286293

287294
@default('kernel_manager_class')
@@ -465,7 +472,7 @@ async def async_start_new_kernel_client(self) -> KernelClient:
465472
await self._async_cleanup_kernel()
466473
raise
467474
self.kc.allow_stdin = False
468-
run_hook(sself.on_execution_start)
475+
run_hook(self.on_execution_start)
469476
return self.kc
470477

471478
start_new_kernel_client = run_sync(async_start_new_kernel_client)
@@ -770,10 +777,8 @@ def _passed_deadline(self, deadline: int) -> bool:
770777
return False
771778

772779
def _check_raise_for_error(
773-
self,
774-
cell: NotebookNode,
775-
cell_index: int,
776-
exec_reply: t.Optional[t.Dict]) -> None:
780+
self, cell: NotebookNode, cell_index: int, exec_reply: t.Optional[t.Dict]
781+
) -> None:
777782

778783
if exec_reply is None:
779784
return None
@@ -787,11 +792,9 @@ def _check_raise_for_error(
787792
or exec_reply_content.get('ename') in self.allow_error_names
788793
or "raises-exception" in cell.metadata.get("tags", [])
789794
)
790-
791-
if (exec_reply is not None) and exec_reply['content']['status'] == 'error':
795+
if not cell_allows_errors:
792796
run_hook(self.on_cell_error, cell=cell, cell_index=cell_index)
793-
if self.force_raise_errors or not cell_allows_errors:
794-
raise CellExecutionError.from_cell_and_msg(cell, exec_reply['content'])
797+
raise CellExecutionError.from_cell_and_msg(cell, exec_reply_content)
795798

796799
async def async_execute_cell(
797800
self,

nbclient/tests/test_client.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,45 @@ def test_widgets(self):
737737
assert 'version_major' in wdata
738738
assert 'version_minor' in wdata
739739

740+
def test_execution_hook(self):
741+
filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb')
742+
with open(filename) as f:
743+
input_nb = nbformat.read(f, 4)
744+
cell_hook = MagicMock()
745+
execution_hook = MagicMock()
746+
747+
executor = NotebookClient(
748+
input_nb,
749+
resources=NBClientTestsBase().build_resources(),
750+
on_cell_start=cell_hook,
751+
on_cell_complete=cell_hook,
752+
on_cell_error=cell_hook,
753+
on_execution_start=execution_hook,
754+
)
755+
executor.execute()
756+
execution_hook.assert_called_once()
757+
assert cell_hook.call_count == 2
758+
759+
def test_error_execution_hook_error(self):
760+
filename = os.path.join(current_dir, 'files', 'Error.ipynb')
761+
with open(filename) as f:
762+
input_nb = nbformat.read(f, 4)
763+
cell_hook = MagicMock()
764+
execution_hook = MagicMock()
765+
766+
executor = NotebookClient(
767+
input_nb,
768+
resources=NBClientTestsBase().build_resources(),
769+
on_cell_start=cell_hook,
770+
on_cell_complete=cell_hook,
771+
on_cell_error=cell_hook,
772+
on_execution_start=execution_hook,
773+
)
774+
with pytest.raises(CellExecutionError):
775+
executor.execute()
776+
execution_hook.assert_called_once()
777+
assert cell_hook.call_count == 3
778+
740779

741780
class TestRunCell(NBClientTestsBase):
742781
"""Contains test functions for NotebookClient.execute_cell"""
@@ -1520,3 +1559,47 @@ def test_no_source(self, executor, cell_mock, message_mock):
15201559
assert message_mock.call_count == 0
15211560
# Should also consume the message stream
15221561
assert cell_mock.outputs == []
1562+
1563+
@prepare_cell_mocks()
1564+
def test_cell_hooks(self, executor, cell_mock, message_mock):
1565+
hook1, hook2, hook3, hook4 = MagicMock(), MagicMock(), MagicMock(), MagicMock()
1566+
executor.on_cell_start = hook1
1567+
executor.on_cell_complete = hook2
1568+
executor.on_cell_error = hook3
1569+
executor.on_execution_start = hook4
1570+
executor.execute_cell(cell_mock, 0)
1571+
assert hook1.call_count == 1
1572+
assert hook2.call_count == 1
1573+
assert hook3.call_count == 0
1574+
assert hook4.call_count == 0
1575+
hook1.assert_called_once_with(cell=cell_mock, cell_index=0)
1576+
hook2.assert_called_once_with(cell=cell_mock, cell_index=0)
1577+
1578+
@prepare_cell_mocks(
1579+
{
1580+
'msg_type': 'error',
1581+
'header': {'msg_type': 'error'},
1582+
'content': {'ename': 'foo', 'evalue': 'bar', 'traceback': ['Boom']},
1583+
},
1584+
reply_msg={
1585+
'msg_type': 'execute_reply',
1586+
'header': {'msg_type': 'execute_reply'},
1587+
# ERROR
1588+
'content': {'status': 'error'},
1589+
},
1590+
)
1591+
def test_error_cell_hooks(self, executor, cell_mock, message_mock):
1592+
hook1, hook2, hook3, hook4 = MagicMock(), MagicMock(), MagicMock(), MagicMock()
1593+
executor.on_cell_start = hook1
1594+
executor.on_cell_complete = hook2
1595+
executor.on_cell_error = hook3
1596+
executor.on_execution_start = hook4
1597+
with self.assertRaises(CellExecutionError):
1598+
executor.execute_cell(cell_mock, 0)
1599+
assert hook1.call_count == 1
1600+
assert hook2.call_count == 1
1601+
assert hook3.call_count == 1
1602+
assert hook4.call_count == 0
1603+
hook1.assert_called_once_with(cell=cell_mock, cell_index=0)
1604+
hook2.assert_called_once_with(cell=cell_mock, cell_index=0)
1605+
hook3.assert_called_once_with(cell=cell_mock, cell_index=0)

0 commit comments

Comments
 (0)