From c37bfc5facd98f8b869da131ac7c438931ef9806 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 2 Nov 2023 20:01:52 +0100 Subject: [PATCH 1/2] jupyter-run: avoid traceback for NoSuchKernel NoSuchKernel used to raise during KernelManager instantiation, but it is now delayed. Access kernel_spec to ensure it's raised where it will be caught. Also removes a redundant warning log immediately before raising, which prevents complete handling of NoSuchError and produces unavoidable duplicate logs. --- jupyter_client/consoleapp.py | 5 ++++- jupyter_client/kernelspec.py | 1 - tests/test_runapp.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 tests/test_runapp.py diff --git a/jupyter_client/consoleapp.py b/jupyter_client/consoleapp.py index 4e9ad357..434fdf01 100644 --- a/jupyter_client/consoleapp.py +++ b/jupyter_client/consoleapp.py @@ -306,8 +306,11 @@ def init_kernel_manager(self) -> None: parent=self, data_dir=self.data_dir, ) + # access kernel_spec to ensure the NoSuchKernel error is raised + # if it's going to be + kernel_spec = self.kernel_manager.kernel_spec # noqa: F841 except NoSuchKernel: - self.log.critical("Could not find kernel %s", self.kernel_name) + self.log.critical("Could not find kernel %r", self.kernel_name) self.exit(1) # type:ignore[attr-defined] self.kernel_manager = t.cast(KernelManager, self.kernel_manager) diff --git a/jupyter_client/kernelspec.py b/jupyter_client/kernelspec.py index 41ed2bad..b903ee6d 100644 --- a/jupyter_client/kernelspec.py +++ b/jupyter_client/kernelspec.py @@ -281,7 +281,6 @@ def get_kernel_spec(self, kernel_name: str) -> KernelSpec: resource_dir = self._find_spec_directory(kernel_name.lower()) if resource_dir is None: - self.log.warning("Kernelspec name %s cannot be found!", kernel_name) raise NoSuchKernel(kernel_name) return self._get_kernel_spec_by_name(kernel_name, resource_dir) diff --git a/tests/test_runapp.py b/tests/test_runapp.py new file mode 100644 index 00000000..7f295bae --- /dev/null +++ b/tests/test_runapp.py @@ -0,0 +1,34 @@ +import sys +from subprocess import run + + +def test_runapp(tmp_path): + test_py = tmp_path / "test.py" + with test_py.open("w") as f: + f.write("print('hello')") + + p = run( + [sys.executable, "-m", "jupyter_client.runapp", str(test_py)], + capture_output=True, + text=True, + check=True, + ) + assert p.stdout.strip() == "hello" + + +def test_no_such_kernel(tmp_path): + test_py = tmp_path / "test.py" + with test_py.open("w") as f: + f.write("print('hello')") + kernel_name = "nosuchkernel" + p = run( + [sys.executable, "-m", "jupyter_client.runapp", "--kernel", kernel_name, str(test_py)], + capture_output=True, + text=True, + check=False, + ) + assert p.returncode + assert "Could not find kernel" in p.stderr + assert kernel_name in p.stderr + # shouldn't show a traceback + assert "Traceback" not in p.stderr From 3f8dd20374ecb21120b043624391cd4acda865f2 Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 23 Sep 2024 09:44:55 +0200 Subject: [PATCH 2/2] call wait_for_ready() in runapp ensures kernel is ready before running avoids lost output if iopub isn't connected yet and shutdown kernel when finished, rather than relying on the kernel shutting itself down --- jupyter_client/runapp.py | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/jupyter_client/runapp.py b/jupyter_client/runapp.py index 9ed4b154..bea5d5cf 100644 --- a/jupyter_client/runapp.py +++ b/jupyter_client/runapp.py @@ -3,10 +3,9 @@ # Distributed under the terms of the Modified BSD License. from __future__ import annotations -import queue +import atexit import signal import sys -import time import typing as t from jupyter_core.application import JupyterApp, base_aliases, base_flags @@ -73,7 +72,8 @@ def initialize(self, argv: list[str] | None = None) -> None: # type:ignore[over super().initialize(argv) JupyterConsoleApp.initialize(self) signal.signal(signal.SIGINT, self.handle_sigint) - self.init_kernel_info() + if self.kernel_manager: + atexit.register(self.kernel_manager.shutdown_kernel) def handle_sigint(self, *args: t.Any) -> None: """Handle SIGINT.""" @@ -82,28 +82,11 @@ def handle_sigint(self, *args: t.Any) -> None: else: self.log.error("Cannot interrupt kernels we didn't start.\n") - def init_kernel_info(self) -> None: - """Wait for a kernel to be ready, and store kernel info""" - timeout = self.kernel_timeout - tic = time.time() - self.kernel_client.hb_channel.unpause() - msg_id = self.kernel_client.kernel_info() - while True: - try: - reply = self.kernel_client.get_shell_msg(timeout=1) - except queue.Empty as e: - if (time.time() - tic) > timeout: - msg = "Kernel didn't respond to kernel_info_request" - raise RuntimeError(msg) from e - else: - if reply["parent_header"].get("msg_id") == msg_id: - self.kernel_info = reply["content"] - return - def start(self) -> None: """Start the application.""" self.log.debug("jupyter run: starting...") super().start() + self.kernel_client.wait_for_ready(timeout=self.kernel_timeout) if self.filenames_to_run: for filename in self.filenames_to_run: self.log.debug("jupyter run: executing `%s`", filename) @@ -112,8 +95,10 @@ def start(self) -> None: reply = self.kernel_client.execute_interactive(code, timeout=OUTPUT_TIMEOUT) return_code = 0 if reply["content"]["status"] == "ok" else 1 if return_code: - raise Exception("jupyter-run error running '%s'" % filename) + msg = f"jupyter-run error running '{filename}'" + raise Exception(msg) else: + self.log.debug("jupyter run: executing from stdin") code = sys.stdin.read() reply = self.kernel_client.execute_interactive(code, timeout=OUTPUT_TIMEOUT) return_code = 0 if reply["content"]["status"] == "ok" else 1