From 131f3ec6104c11c7d9f8b1c4b68bed7939587519 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Sun, 9 Aug 2026 10:06:45 +0200 Subject: [PATCH] Add an advertise_debugger option so kernel_info need not import debugpy Deferring the debugger construction does not on its own keep debugpy out of a kernel: `kernel_info` imports `.debugger` to fill in `supported_features`, and every frontend sends a kernel_info_request at startup -- jupyter_client's `wait_for_ready` re-sends it until it gets a reply, on start and on every restart. Left alone, the import is merely moved from initialisation into the first shell request. Add an `advertise_debugger` trait, auto|true|false, defaulting to auto so nothing changes by default. "auto" asks the debugger module as before; "true" and "false" answer kernel_info without importing anything, which lets a deployment that already knows whether debugpy is available keep it out of the process entirely. The option only controls what is advertised: setting it to "true" where debugpy is unavailable does not make debug requests work. --- ipykernel/kernelbase.py | 36 ++++++++++++++++++++++++++++++++++-- tests/test_kernel_direct.py | 23 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 073ce01d1..f6017bdb2 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -44,6 +44,7 @@ from traitlets.traitlets import ( Any, Bool, + CaselessStrEnum, Dict, Float, Instance, @@ -203,6 +204,24 @@ def _default_ident(self): """, ).tag(config=True) + advertise_debugger = CaselessStrEnum( + ["auto", "true", "false"], + default_value="auto", + help="""Whether to advertise "debugger" in the kernel_info_reply's + supported_features. + + "auto" (the default) imports the debugger module to find out whether + debugpy is actually usable. That import is not cheap, and every + frontend sends a kernel_info_request at startup, so a deployment that + already knows the answer can set this to "true" or "false" to answer + kernel_info without touching debugpy at all. + + Note that this only controls what is advertised. Setting it to "true" + where debugpy is unavailable does not make debugging work: debug + requests will still fail. + """, + ).tag(config=True) + # track associations with current request # Private interface @@ -1001,13 +1020,26 @@ async def connect_request(self, stream, ident, parent): self.log.debug("%s", msg) @property - def kernel_info(self): + def _debugger_advertised(self) -> bool: + """Whether to list "debugger" in kernel_info's supported_features. + + Only the "auto" setting has to import the debugger module (and so + debugpy) to answer; see the ``advertise_debugger`` trait. + """ + if self.advertise_debugger == "true": + return True + if self.advertise_debugger == "false": + return False from .debugger import _is_debugpy_available + return _is_debugpy_available + + @property + def kernel_info(self): supported_features: list[str] = [] if self._supports_kernel_subshells: supported_features.append("kernel subshells") - if _is_debugpy_available: + if self._debugger_advertised: supported_features.append("debugger") return { diff --git a/tests/test_kernel_direct.py b/tests/test_kernel_direct.py index 146ae3ead..78bc29568 100644 --- a/tests/test_kernel_direct.py +++ b/tests/test_kernel_direct.py @@ -5,6 +5,7 @@ import os import signal +import sys import warnings import pytest @@ -187,3 +188,25 @@ async def test_send_interrupt_children(kernel): # async def test_direct_usage_request(kernel): # reply = await kernel.test_control_message("usage_request", {}) # assert reply['header']['msg_type'] == 'usage_reply' + + +@pytest.mark.parametrize( + ("setting", "advertised"), + [("true", True), ("false", False)], +) +def test_advertise_debugger_explicit(kernel, monkeypatch, setting, advertised): + """ "true"/"false" answer kernel_info without importing the debugger.""" + # Poisoning the entry makes any `from .debugger import ...` raise. + monkeypatch.setitem(sys.modules, "ipykernel.debugger", None) + kernel.advertise_debugger = setting + + assert ("debugger" in kernel.kernel_info["supported_features"]) is advertised + + +def test_advertise_debugger_auto(kernel): + """ "auto" (the default) reports what the debugger module says.""" + from ipykernel.debugger import _is_debugpy_available + + assert kernel.advertise_debugger == "auto" + features = kernel.kernel_info["supported_features"] + assert ("debugger" in features) is _is_debugpy_available