Skip to content

Commit 01465b8

Browse files
CiprianAntonpre-commit-ci[bot]davidbrochart
authored
Make sure kernel is cleaned up in case an error occurred while starting kernel client (#234)
* Make sure kernel is cleaned up in case an error ocurred while starting kernel client * Add test * Run pre-commit * Include kernel id in log * Fix [no-untyped-def] warning * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Pin traitlets>=5.2.2 Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: David Brochart <[email protected]>
1 parent c77a735 commit 01465b8

File tree

4 files changed

+57
-14
lines changed

4 files changed

+57
-14
lines changed

nbclient/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ class NbClientApp(JupyterApp):
3535
An application used to execute notebook files (``*.ipynb``)
3636
"""
3737

38-
version = __version__
38+
version = Unicode(__version__)
3939
name = 'jupyter-execute'
4040
aliases = nbclient_aliases
4141
flags = nbclient_flags
4242

43-
description = Unicode("An application used to execute notebook files (*.ipynb)")
43+
description = "An application used to execute notebook files (*.ipynb)"
4444
notebooks = List([], help="Path of notebooks to convert").tag(config=True)
4545
timeout: int = Integer(
4646
None,

nbclient/client.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from .util import ensure_async, run_hook, run_sync
4040

4141

42-
def timestamp(msg: t.Optional[Dict] = None) -> str:
42+
def timestamp(msg: t.Optional[t.Dict] = None) -> str:
4343
if msg and 'header' in msg: # The test mocks don't provide a header, so tolerate that
4444
msg_header = msg['header']
4545
if 'date' in msg_header and isinstance(msg_header['date'], datetime.datetime):
@@ -288,7 +288,9 @@ class NotebookClient(LoggingConfigurable):
288288
""",
289289
).tag(config=True)
290290

291-
kernel_manager_class: KernelManager = Type(config=True, help='The kernel manager class to use.')
291+
kernel_manager_class = Type(
292+
config=True, klass=KernelManager, help='The kernel manager class to use.'
293+
)
292294

293295
on_notebook_start: t.Optional[t.Callable] = Callable(
294296
default_value=None,
@@ -390,7 +392,7 @@ def _kernel_manager_class_default(self) -> t.Type[KernelManager]:
390392

391393
return AsyncKernelManager
392394

393-
_display_id_map: t.Dict[str, t.Dict] = Dict(
395+
_display_id_map = Dict(
394396
help=dedent(
395397
"""
396398
mapping of locations of outputs with a given display_id
@@ -423,7 +425,7 @@ def _kernel_manager_class_default(self) -> t.Type[KernelManager]:
423425
""",
424426
).tag(config=True)
425427

426-
resources: t.Dict = Dict(
428+
resources = Dict(
427429
help=dedent(
428430
"""
429431
Additional resources used in the conversion process. For example,
@@ -557,11 +559,16 @@ async def async_start_new_kernel_client(self) -> KernelClient:
557559
Kernel client as created by the kernel manager ``km``.
558560
"""
559561
assert self.km is not None
560-
self.kc = self.km.client()
561-
await ensure_async(self.kc.start_channels()) # type:ignore[func-returns-value]
562562
try:
563-
await ensure_async(self.kc.wait_for_ready(timeout=self.startup_timeout))
564-
except RuntimeError:
563+
self.kc = self.km.client()
564+
await ensure_async(self.kc.start_channels()) # type:ignore[func-returns-value]
565+
await ensure_async(self.kc.wait_for_ready(timeout=self.startup_timeout)) # type:ignore
566+
except Exception as e:
567+
self.log.error(
568+
"Error occurred while starting new kernel client for kernel {}: {}".format(
569+
self.km.kernel_id, str(e)
570+
)
571+
)
565572
await self._async_cleanup_kernel()
566573
raise
567574
self.kc.allow_stdin = False
@@ -846,7 +853,7 @@ async def _async_handle_timeout(
846853

847854
async def _async_check_alive(self) -> None:
848855
assert self.kc is not None
849-
if not await ensure_async(self.kc.is_alive()):
856+
if not await ensure_async(self.kc.is_alive()): # type:ignore
850857
self.log.error("Kernel died while waiting for execute reply.")
851858
raise DeadKernelError("Kernel died")
852859

nbclient/tests/test_client.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import nbformat
1616
import pytest
1717
import xmltodict
18-
from jupyter_client import KernelManager
18+
from jupyter_client import KernelClient, KernelManager
1919
from jupyter_client.kernelspec import KernelSpecManager
2020
from nbconvert.filters import strip_ansi
2121
from nbformat import NotebookNode
@@ -211,7 +211,11 @@ def test_mock_wrapper(self):
211211
cell_mock = NotebookNode(
212212
source='"foo" = "bar"', metadata={}, cell_type='code', outputs=[]
213213
)
214-
executor = NotebookClient({}) # type:ignore
214+
215+
class NotebookClientWithParentID(NotebookClient):
216+
parent_id: str
217+
218+
executor = NotebookClientWithParentID({}) # type:ignore
215219
executor.nb = {'cells': [cell_mock]} # type:ignore
216220

217221
# self.kc.iopub_channel.get_msg => message_mock.side_effect[i]
@@ -508,6 +512,38 @@ def test_start_new_kernel_history_file_setting():
508512
kc.stop_channels()
509513

510514

515+
def test_start_new_kernel_client_cleans_up_kernel_on_failure():
516+
class FakeClient(KernelClient):
517+
def start_channels(
518+
self,
519+
shell: bool = True,
520+
iopub: bool = True,
521+
stdin: bool = True,
522+
hb: bool = True,
523+
control: bool = True,
524+
) -> None:
525+
raise Exception("Any error")
526+
527+
def stop_channels(self) -> None:
528+
pass
529+
530+
nb = nbformat.v4.new_notebook()
531+
km = KernelManager()
532+
km.client_factory = FakeClient
533+
executor = NotebookClient(nb, km=km)
534+
executor.start_new_kernel()
535+
assert km.has_kernel
536+
assert executor.km is not None
537+
538+
with pytest.raises(Exception) as err:
539+
executor.start_new_kernel_client()
540+
541+
assert str(err.value.args[0]) == "Any error"
542+
assert executor.kc is None
543+
assert executor.km is None
544+
assert not km.has_kernel
545+
546+
511547
class TestExecute(NBClientTestsBase):
512548
"""Contains test functions for execute.py"""
513549

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
jupyter_client>=6.1.5
22
nbformat>=5.0
33
nest_asyncio
4-
traitlets>=5.0.0
4+
traitlets>=5.2.2

0 commit comments

Comments
 (0)