diff --git a/ipykernel/debugger.py b/ipykernel/debugger.py index cfa976107..24fe92849 100644 --- a/ipykernel/debugger.py +++ b/ipykernel/debugger.py @@ -326,7 +326,15 @@ class Debugger: ] def __init__( - self, log, debugpy_stream, event_callback, shell_socket, session, just_my_code=True + self, + log, + debugpy_stream, + event_callback, + shell_socket, + session, + kernel_modules, + just_my_code=False, + filter_internal_frames=True, ): """Initialize the debugger.""" self.log = log @@ -335,7 +343,9 @@ def __init__( self.session = session self.is_started = False self.event_callback = event_callback + self.kernel_modules = kernel_modules self.just_my_code = just_my_code + self.filter_internal_frames = filter_internal_frames self.stopped_queue: Queue[t.Any] = Queue() self.started_debug_handlers = {} @@ -498,25 +508,7 @@ async def source(self, message): async def stackTrace(self, message): """Handle a stack trace message.""" - reply = await self._forward_message(message) - # The stackFrames array can have the following content: - # { frames from the notebook} - # ... - # { 'id': xxx, 'name': '', ... } <= this is the first frame of the code from the notebook - # { frames from ipykernel } - # ... - # {'id': yyy, 'name': '', ... } <= this is the first frame of ipykernel code - # or only the frames from the notebook. - # We want to remove all the frames from ipykernel when they are present. - try: - sf_list = reply["body"]["stackFrames"] - module_idx = len(sf_list) - next( - i for i, v in enumerate(reversed(sf_list), 1) if v["name"] == "" and i != 1 - ) - reply["body"]["stackFrames"] = reply["body"]["stackFrames"][: module_idx + 1] - except StopIteration: - pass - return reply + return await self._forward_message(message) def accept_variable(self, variable_name): """Accept a variable by name.""" @@ -574,6 +566,12 @@ async def attach(self, message): # Set debugOptions for breakpoints in python standard library source. if not self.just_my_code: message["arguments"]["debugOptions"] = ["DebugStdLib"] + + # Dynamic skip rules (computed at kernel startup) + if self.filter_internal_frames: + rules = [{"path": path, "include": False} for path in self.kernel_modules] + message["arguments"]["rules"] = rules + return await self._forward_message(message) async def configurationDone(self, message): diff --git a/ipykernel/ipkernel.py b/ipykernel/ipkernel.py index e1505a9a7..aa22bbb0f 100644 --- a/ipykernel/ipkernel.py +++ b/ipykernel/ipkernel.py @@ -119,6 +119,10 @@ def __init__(self, **kwargs): from .debugger import _is_debugpy_available + self._kernel_modules = [ + m.__file__ for m in sys.modules.values() if hasattr(m, "__file__") and m.__file__ + ] + # Initialize the Debugger if _is_debugpy_available: self.debugger = self.debugger_class( @@ -127,7 +131,9 @@ def __init__(self, **kwargs): self._publish_debug_event, self.debug_shell_socket, self.session, + self._kernel_modules, self.debug_just_my_code, + self.filter_internal_frames, ) # Initialize the InteractiveShell subclass diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 52d40b952..7fa1fb9dc 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -179,11 +179,18 @@ def _default_ident(self): # The ipykernel source is in the call stack, so the user # has to manipulate the step-over and step-into in a wize way. debug_just_my_code = Bool( - True, + False, help="""Set to False if you want to debug python standard and dependent libraries. """, ).tag(config=True) + # Experimental option to filter internal frames from the stack trace and stepping. + filter_internal_frames = Bool( + True, + help="""Set to False if you want to debug kernel modules. + """, + ).tag(config=True) + # track associations with current request # Private interface