Skip to content

Commit 327589f

Browse files
authored
Implement kernel-side callstack filtering for internal frames (#1481)
This PR introduces a kernel-side mechanism to filter internal kernel module frames from the debugger callstack. These frames currently appear when stepping (e.g. pressing “Next”), causing the debugger to stop deep inside IPython internals and creating long, noisy callstacks. This PR also flips `debug_just_my_code` to `False` by default to allow users to debug external sources when needed, and introduces a kernel-side filtering layer that hides internal kernel frames unless explicitly disabled. Without filtering: https://github.com/user-attachments/assets/49686fd2-c99c-4e30-9c67-012050c29284 With filtering enabled: https://github.com/user-attachments/assets/94b8ece3-5dd5-408e-b0c8-57b9d1f2c240
2 parents 5b9f05b + a97f48a commit 327589f

File tree

3 files changed

+32
-21
lines changed

3 files changed

+32
-21
lines changed

ipykernel/debugger.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,15 @@ class Debugger:
326326
]
327327

328328
def __init__(
329-
self, log, debugpy_stream, event_callback, shell_socket, session, just_my_code=True
329+
self,
330+
log,
331+
debugpy_stream,
332+
event_callback,
333+
shell_socket,
334+
session,
335+
kernel_modules,
336+
just_my_code=False,
337+
filter_internal_frames=True,
330338
):
331339
"""Initialize the debugger."""
332340
self.log = log
@@ -335,7 +343,9 @@ def __init__(
335343
self.session = session
336344
self.is_started = False
337345
self.event_callback = event_callback
346+
self.kernel_modules = kernel_modules
338347
self.just_my_code = just_my_code
348+
self.filter_internal_frames = filter_internal_frames
339349
self.stopped_queue: Queue[t.Any] = Queue()
340350

341351
self.started_debug_handlers = {}
@@ -498,25 +508,7 @@ async def source(self, message):
498508

499509
async def stackTrace(self, message):
500510
"""Handle a stack trace message."""
501-
reply = await self._forward_message(message)
502-
# The stackFrames array can have the following content:
503-
# { frames from the notebook}
504-
# ...
505-
# { 'id': xxx, 'name': '<module>', ... } <= this is the first frame of the code from the notebook
506-
# { frames from ipykernel }
507-
# ...
508-
# {'id': yyy, 'name': '<module>', ... } <= this is the first frame of ipykernel code
509-
# or only the frames from the notebook.
510-
# We want to remove all the frames from ipykernel when they are present.
511-
try:
512-
sf_list = reply["body"]["stackFrames"]
513-
module_idx = len(sf_list) - next(
514-
i for i, v in enumerate(reversed(sf_list), 1) if v["name"] == "<module>" and i != 1
515-
)
516-
reply["body"]["stackFrames"] = reply["body"]["stackFrames"][: module_idx + 1]
517-
except StopIteration:
518-
pass
519-
return reply
511+
return await self._forward_message(message)
520512

521513
def accept_variable(self, variable_name):
522514
"""Accept a variable by name."""
@@ -574,6 +566,12 @@ async def attach(self, message):
574566
# Set debugOptions for breakpoints in python standard library source.
575567
if not self.just_my_code:
576568
message["arguments"]["debugOptions"] = ["DebugStdLib"]
569+
570+
# Dynamic skip rules (computed at kernel startup)
571+
if self.filter_internal_frames:
572+
rules = [{"path": path, "include": False} for path in self.kernel_modules]
573+
message["arguments"]["rules"] = rules
574+
577575
return await self._forward_message(message)
578576

579577
async def configurationDone(self, message):

ipykernel/ipkernel.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ def __init__(self, **kwargs):
119119

120120
from .debugger import _is_debugpy_available
121121

122+
self._kernel_modules = [
123+
m.__file__ for m in sys.modules.values() if hasattr(m, "__file__") and m.__file__
124+
]
125+
122126
# Initialize the Debugger
123127
if _is_debugpy_available:
124128
self.debugger = self.debugger_class(
@@ -127,7 +131,9 @@ def __init__(self, **kwargs):
127131
self._publish_debug_event,
128132
self.debug_shell_socket,
129133
self.session,
134+
self._kernel_modules,
130135
self.debug_just_my_code,
136+
self.filter_internal_frames,
131137
)
132138

133139
# Initialize the InteractiveShell subclass

ipykernel/kernelbase.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,18 @@ def _default_ident(self):
179179
# The ipykernel source is in the call stack, so the user
180180
# has to manipulate the step-over and step-into in a wize way.
181181
debug_just_my_code = Bool(
182-
True,
182+
False,
183183
help="""Set to False if you want to debug python standard and dependent libraries.
184184
""",
185185
).tag(config=True)
186186

187+
# Experimental option to filter internal frames from the stack trace and stepping.
188+
filter_internal_frames = Bool(
189+
True,
190+
help="""Set to False if you want to debug kernel modules.
191+
""",
192+
).tag(config=True)
193+
187194
# track associations with current request
188195
# Private interface
189196

0 commit comments

Comments
 (0)