Skip to content

Commit 4f46196

Browse files
committed
Rebased with master and added tests
1 parent d28ce02 commit 4f46196

File tree

3 files changed

+110
-20
lines changed

3 files changed

+110
-20
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: 87 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,51 @@ 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+
async def test_cell_hooks(self, executor, cell_mock, message_mock):
1565+
hook1, hook2, hook3, hook4 = MagicMock(), MagicMock(), MagicMock(), MagicMock()
1566+
tasks = [hook1, hook2, hook3, hook4]
1567+
executor.on_cell_start = hook1
1568+
executor.on_cell_complete = hook2
1569+
executor.on_cell_error = hook3
1570+
executor.on_execution_start = hook4
1571+
executor.execute_cell(cell_mock, 0)
1572+
await asyncio.gather(*tasks)
1573+
assert hook1.call_count == 1
1574+
assert hook2.call_count == 1
1575+
assert hook3.call_count == 0
1576+
assert hook4.call_count == 0
1577+
hook1.assert_called_once_with(cell=cell_mock, cell_index=0)
1578+
hook2.assert_called_once_with(cell=cell_mock, cell_index=0)
1579+
1580+
@prepare_cell_mocks(
1581+
{
1582+
'msg_type': 'error',
1583+
'header': {'msg_type': 'error'},
1584+
'content': {'ename': 'foo', 'evalue': 'bar', 'traceback': ['Boom']},
1585+
},
1586+
reply_msg={
1587+
'msg_type': 'execute_reply',
1588+
'header': {'msg_type': 'execute_reply'},
1589+
# ERROR
1590+
'content': {'status': 'error'},
1591+
},
1592+
)
1593+
async def test_error_cell_hooks(self, executor, cell_mock, message_mock):
1594+
hook1, hook2, hook3, hook4 = MagicMock(), MagicMock(), MagicMock(), MagicMock()
1595+
tasks = [hook1, hook2, hook3, hook4]
1596+
executor.on_cell_start = hook1
1597+
executor.on_cell_complete = hook2
1598+
executor.on_cell_error = hook3
1599+
executor.on_execution_start = hook4
1600+
with self.assertRaises(CellExecutionError):
1601+
executor.execute_cell(cell_mock, 0)
1602+
await asyncio.gather(*tasks)
1603+
assert hook1.call_count == 1
1604+
assert hook2.call_count == 1
1605+
assert hook3.call_count == 1
1606+
assert hook4.call_count == 0
1607+
hook1.assert_called_once_with(cell=cell_mock, cell_index=0)
1608+
hook2.assert_called_once_with(cell=cell_mock, cell_index=0)
1609+
hook3.assert_called_once_with(cell=cell_mock, cell_index=0)

nbclient/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import asyncio
77
import inspect
88
import sys
9-
from typing import Any, Awaitable, Callable, Optional, Union
109
from functools import partial
10+
from typing import Any, Awaitable, Callable, Optional, Union
1111

1212

1313
def check_ipython() -> None:

0 commit comments

Comments
 (0)