From 76f56b34d49cb8125ac7e860d9b5040c87c6c98b Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sat, 25 Jul 2026 06:30:13 +0200 Subject: [PATCH 1/4] enh: jupuyter --- code_sandboxes/__version__.py | 2 +- code_sandboxes/jupyter_sandbox.py | 44 ++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/code_sandboxes/__version__.py b/code_sandboxes/__version__.py index f0787df..dcc0a6f 100644 --- a/code_sandboxes/__version__.py +++ b/code_sandboxes/__version__.py @@ -3,4 +3,4 @@ """Code Sandboxes.""" -__version__ = "0.0.19" +__version__ = "0.0.20" diff --git a/code_sandboxes/jupyter_sandbox.py b/code_sandboxes/jupyter_sandbox.py index 922215f..65b2121 100644 --- a/code_sandboxes/jupyter_sandbox.py +++ b/code_sandboxes/jupyter_sandbox.py @@ -60,6 +60,10 @@ def __init__( port: int = DEFAULT_PORT, python_executable: str | None = None, separate_process: bool = True, + kernel_id: str | None = None, + kernel_path: str | None = None, + client_kwargs: dict | None = None, + reuse_kernel: bool = True, **kwargs, ): super().__init__(config) @@ -90,6 +94,13 @@ def __init__( self._workdir_tmp: str | None = None self._extra_kwargs = kwargs self._owns_server = server_url is None + # Explicit kernel to connect to. When None and ``reuse_kernel`` is True + # the sandbox reuses a pre-warmed kernel on the server; when None and + # ``reuse_kernel`` is False a brand-new kernel is created. + self._kernel_id = kernel_id + self._kernel_path = kernel_path + self._client_kwargs = client_kwargs + self._reuse_kernel = reuse_kernel @classmethod def list_environments(cls) -> list[SandboxEnvironment]: @@ -339,15 +350,25 @@ def start(self) -> None: self._wait_for_server(timeout=self.config.timeout or DEFAULT_STARTUP_TIMEOUT) - # Try to reuse an existing pre-warmed kernel instead of creating a new one. - # Jupyter runtimes pre-warm a kernel at startup; connecting to it avoids - # unnecessary kernel proliferation (3 kernels → 2, or 2 → 1). - kernel_id = self._find_existing_kernel() + # Decide which kernel to connect to: + # - an explicit kernel_id always wins; + # - otherwise, when reuse is enabled, reuse a pre-warmed kernel (Jupyter + # runtimes pre-warm one at startup) to avoid kernel proliferation; + # - otherwise connect with no id so the client starts a brand-new kernel. + if self._kernel_id is not None: + kernel_id = self._kernel_id + elif self._reuse_kernel: + kernel_id = self._find_existing_kernel() + else: + kernel_id = None self._client = KernelClient( - server_url=self._server_url, token=self._token, kernel_id=kernel_id + server_url=self._server_url, + token=self._token, + kernel_id=kernel_id, + client_kwargs=self._client_kwargs or None, ) - self._client.start() + self._client.start(path=self._kernel_path) self._default_context = self.create_context("default") self._info = SandboxInfo( @@ -361,6 +382,17 @@ def start(self) -> None: ) self._started = True + @property + def kernel_client(self): + """The underlying ``jupyter_kernel_client.KernelClient``. + + Exposed so callers that need the full low-level kernel API (for + example streaming execution via ``execute_interactive``) can delegate + to the same client the sandbox uses internally. ``None`` until + :meth:`start` has been called. + """ + return self._client + def _setup_tool_caller(self) -> None: """Keep tool calling on the client side for Jupyter sandboxes.""" return From 482e37f156979706df809413f6335f5b47cf83cf Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sat, 25 Jul 2026 06:43:48 +0200 Subject: [PATCH 2/4] lint --- code_sandboxes/jupyter_sandbox.py | 8 +- tests/test_jupyter.py | 133 ++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/code_sandboxes/jupyter_sandbox.py b/code_sandboxes/jupyter_sandbox.py index 65b2121..ad9ea36 100644 --- a/code_sandboxes/jupyter_sandbox.py +++ b/code_sandboxes/jupyter_sandbox.py @@ -94,9 +94,10 @@ def __init__( self._workdir_tmp: str | None = None self._extra_kwargs = kwargs self._owns_server = server_url is None - # Explicit kernel to connect to. When None and ``reuse_kernel`` is True - # the sandbox reuses a pre-warmed kernel on the server; when None and - # ``reuse_kernel`` is False a brand-new kernel is created. + # Explicit kernel to connect to. When ``kernel_id`` is None and + # ``reuse_kernel`` is True, the sandbox attempts to reuse a pre-warmed + # kernel on the server; when ``kernel_id`` is None and + # ``reuse_kernel`` is False, a brand-new kernel is created. self._kernel_id = kernel_id self._kernel_path = kernel_path self._client_kwargs = client_kwargs @@ -368,6 +369,7 @@ def start(self) -> None: kernel_id=kernel_id, client_kwargs=self._client_kwargs or None, ) + self._client.start(path=self._kernel_path) self._default_context = self.create_context("default") diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index 59f26f3..a5128d9 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -5,6 +5,8 @@ """jupyter sandbox tests.""" import os +import sys +import types from pathlib import Path import pytest @@ -13,6 +15,137 @@ from code_sandboxes.models import SandboxConfig +def test_explicit_kernel_id_wins_over_reuse(monkeypatch): + """Explicit kernel_id takes precedence even when reuse is enabled.""" + + captured: dict[str, object] = {} + + class _KernelClientStub: + def __init__(self, server_url, token, kernel_id, client_kwargs=None): + captured["server_url"] = server_url + captured["token"] = token + captured["kernel_id"] = kernel_id + captured["client_kwargs"] = client_kwargs + + def start(self, path=None): + captured["path"] = path + + def stop(self): + return None + + monkeypatch.setitem( + sys.modules, + "jupyter_kernel_client", + types.SimpleNamespace(KernelClient=_KernelClientStub), + ) + + sandbox = JupyterSandbox( + server_url="http://localhost:8888", + token="tok", + kernel_id="explicit-kernel", + reuse_kernel=True, + ) + + monkeypatch.setattr(sandbox, "_wait_for_server", lambda timeout=None: None) + + def _should_not_be_called(): + raise AssertionError("_find_existing_kernel should not be called with explicit kernel_id") + + monkeypatch.setattr(sandbox, "_find_existing_kernel", _should_not_be_called) + + sandbox.start() + try: + assert captured["kernel_id"] == "explicit-kernel" + finally: + sandbox.stop() + + +def test_reuse_kernel_false_forces_new_kernel(monkeypatch): + """When reuse_kernel is False and no kernel_id is provided, connect with kernel_id=None.""" + + captured: dict[str, object] = {} + + class _KernelClientStub: + def __init__(self, server_url, token, kernel_id, client_kwargs=None): + captured["kernel_id"] = kernel_id + + def start(self, path=None): + return None + + def stop(self): + return None + + monkeypatch.setitem( + sys.modules, + "jupyter_kernel_client", + types.SimpleNamespace(KernelClient=_KernelClientStub), + ) + + sandbox = JupyterSandbox( + server_url="http://localhost:8888", + token="tok", + kernel_id=None, + reuse_kernel=False, + ) + + monkeypatch.setattr(sandbox, "_wait_for_server", lambda timeout=None: None) + + def _should_not_be_called(): + raise AssertionError("_find_existing_kernel should not be called when reuse_kernel=False") + + monkeypatch.setattr(sandbox, "_find_existing_kernel", _should_not_be_called) + + sandbox.start() + try: + assert captured["kernel_id"] is None + finally: + sandbox.stop() + + +def test_kernel_client_forwards_client_kwargs(monkeypatch): + """JupyterSandbox forwards client_kwargs to KernelClient.""" + + captured: dict[str, object] = {} + + class _KernelClientStub: + def __init__(self, server_url, token, kernel_id, client_kwargs=None): + captured["server_url"] = server_url + captured["token"] = token + captured["kernel_id"] = kernel_id + captured["client_kwargs"] = client_kwargs + + def start(self, path=None): + captured["start_path"] = path + + def stop(self): + return None + + monkeypatch.setitem( + sys.modules, + "jupyter_kernel_client", + types.SimpleNamespace(KernelClient=_KernelClientStub), + ) + + sandbox = JupyterSandbox( + server_url="http://localhost:8888", + token="tok", + kernel_id="kernel-1", + kernel_path="/tmp/notebook.ipynb", + client_kwargs={"reconnect_interval": 5}, + reuse_kernel=False, + ) + + monkeypatch.setattr(sandbox, "_wait_for_server", lambda timeout=None: None) + + sandbox.start() + try: + assert captured["kernel_id"] == "kernel-1" + assert captured.get("client_kwargs") == {"reconnect_interval": 5} + assert captured.get("start_path") == "/tmp/notebook.ipynb" + finally: + sandbox.stop() + + class TestJupyterSandbox: """Tests for JupyterSandbox.""" From 3588eebce7d5ec75fd297d4d425dc69df5a1d89e Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sat, 25 Jul 2026 06:46:25 +0200 Subject: [PATCH 3/4] lint --- tests/test_jupyter.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index a5128d9..e7d0e4d 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -41,7 +41,6 @@ def stop(self): sandbox = JupyterSandbox( server_url="http://localhost:8888", - token="tok", kernel_id="explicit-kernel", reuse_kernel=True, ) @@ -83,7 +82,6 @@ def stop(self): sandbox = JupyterSandbox( server_url="http://localhost:8888", - token="tok", kernel_id=None, reuse_kernel=False, ) @@ -102,7 +100,7 @@ def _should_not_be_called(): sandbox.stop() -def test_kernel_client_forwards_client_kwargs(monkeypatch): +def test_kernel_client_forwards_client_kwargs(monkeypatch, tmp_path: Path): """JupyterSandbox forwards client_kwargs to KernelClient.""" captured: dict[str, object] = {} @@ -126,11 +124,12 @@ def stop(self): types.SimpleNamespace(KernelClient=_KernelClientStub), ) + notebook_path = str(tmp_path / "notebook.ipynb") + sandbox = JupyterSandbox( server_url="http://localhost:8888", - token="tok", kernel_id="kernel-1", - kernel_path="/tmp/notebook.ipynb", + kernel_path=notebook_path, client_kwargs={"reconnect_interval": 5}, reuse_kernel=False, ) @@ -141,7 +140,7 @@ def stop(self): try: assert captured["kernel_id"] == "kernel-1" assert captured.get("client_kwargs") == {"reconnect_interval": 5} - assert captured.get("start_path") == "/tmp/notebook.ipynb" + assert captured.get("start_path") == notebook_path finally: sandbox.stop() From e185826adf9f7a0fd320105a508d8b747d9322c9 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sat, 25 Jul 2026 08:15:32 +0200 Subject: [PATCH 4/4] bump --- code_sandboxes/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_sandboxes/__version__.py b/code_sandboxes/__version__.py index dcc0a6f..d9bd733 100644 --- a/code_sandboxes/__version__.py +++ b/code_sandboxes/__version__.py @@ -3,4 +3,4 @@ """Code Sandboxes.""" -__version__ = "0.0.20" +__version__ = "0.0.21"