Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions ipykernel/kernelbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from traitlets.traitlets import (
Any,
Bool,
CaselessStrEnum,
Dict,
Float,
Instance,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions tests/test_kernel_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import os
import signal
import sys
import warnings

import pytest
Expand Down Expand Up @@ -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