diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index 90dc6648045f27..9cc3e0b60d6c9d 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -438,11 +438,17 @@ can be overridden by the local file. Move the current frame *count* (default one) levels down in the stack trace (to a newer frame). + Frames from ignored modules will be skipped. Use :pdbcmd:`ignore_module` to + ignore modules and :pdbcmd:`unignore_module` to stop ignoring them. + .. pdbcommand:: u(p) [count] Move the current frame *count* (default one) levels up in the stack trace (to an older frame). + Frames from ignored modules will be skipped. Use :pdbcmd:`ignore_module` to + ignore modules and :pdbcmd:`unignore_module` to stop ignoring them. + .. pdbcommand:: b(reak) [([filename:]lineno | function) [, condition]] With a *lineno* argument, set a break at line *lineno* in the current file. @@ -718,6 +724,47 @@ can be overridden by the local file. :pdbcmd:`interact` directs its output to the debugger's output channel rather than :data:`sys.stderr`. +.. pdbcommand:: ignore_module [module_name] + + Add a module to the list of modules to skip when stepping, continuing, or + navigating frames. When a module is ignored, the debugger will automatically + skip over frames from that module during :pdbcmd:`step`, :pdbcmd:`next`, + :pdbcmd:`continue`, :pdbcmd:`up`, and :pdbcmd:`down` commands. + + Supports wildcard patterns using glob-style matching (via :mod:`fnmatch`). + + Without *module_name*, list the currently ignored modules. + + Examples:: + + (Pdb) ignore_module threading # Skip threading module frames + (Pdb) ignore_module asyncio.* # Skip all asyncio submodules + (Pdb) ignore_module *.tests # Skip all test modules + (Pdb) ignore_module # List currently ignored modules + + Common use cases: + + - Skip framework code (``django.*``, ``flask.*``, ``asyncio.*``) + - Skip standard library modules (``threading``, ``multiprocessing``, ``logging.*``) + - Skip test framework internals (``*pytest*``, ``unittest.*``) + + .. versionadded:: 3.15 + +.. pdbcommand:: unignore_module [module_name] + + Remove a module from the list of modules to skip when stepping or navigating + frames. This will allow the debugger to step into frames from the specified + module again. + + Without *module_name*, list the currently ignored modules. + + Example:: + + (Pdb) unignore_module threading # Stop ignoring threading module frames + (Pdb) unignore_module asyncio.* # Remove the asyncio.* pattern + + .. versionadded:: 3.15 + .. _debugger-aliases: .. pdbcommand:: alias [name [command]] diff --git a/Doc/tools/extensions/pyspecific.py b/Doc/tools/extensions/pyspecific.py index f5451adb37b0b4..ff15bd1baff06e 100644 --- a/Doc/tools/extensions/pyspecific.py +++ b/Doc/tools/extensions/pyspecific.py @@ -72,7 +72,7 @@ def parse_opcode_signature(env, sig, signode): # Support for documenting pdb commands -pdbcmd_sig_re = re.compile(r'([a-z()!]+)\s*(.*)') +pdbcmd_sig_re = re.compile(r'([a-z()!_]+)\s*(.*)') # later... # pdbargs_tokens_re = re.compile(r'''[a-zA-Z]+ | # identifiers diff --git a/Lib/pdb.py b/Lib/pdb.py index f695a39332e461..c038115260b230 100644 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -521,6 +521,13 @@ def curframe_locals(self, value): # Override Bdb methods + def stop_here(self, frame): + """Override bdb's stop_here to add message when skipping ignored modules.""" + if self.skip and self.is_skipped_module(frame.f_globals.get('__name__', '')): + self.message('[... skipped 1 ignored module(s)]') + return False + return super().stop_here(frame) + def user_call(self, frame, argument_list): """This method is called when there is the remote possibility that we ever need to stop in this function.""" @@ -1774,6 +1781,8 @@ def do_up(self, arg): Move the current frame count (default one) levels up in the stack trace (to an older frame). + + Will skip frames from ignored modules. """ if self.curindex == 0: self.error('Oldest frame') @@ -1786,7 +1795,30 @@ def do_up(self, arg): if count < 0: newframe = 0 else: - newframe = max(0, self.curindex - count) + # Skip over ignored modules + counter = 0 + module_skipped = 0 + for i in range(self.curindex - 1, -1, -1): + frame = self.stack[i][0] + should_skip_module = (self.skip and + self.is_skipped_module(frame.f_globals.get('__name__', ''))) + + if should_skip_module: + module_skipped += 1 + continue + counter += 1 + if counter >= count: + break + else: + # No valid frames found + self.error('All frames above are from ignored modules. ' + 'Use "unignore_module" to allow stepping into them.') + return + + newframe = i + if module_skipped: + self.message(f'[... skipped {module_skipped} frame(s) ' + 'from ignored modules]') self._select_frame(newframe) do_u = do_up @@ -1795,6 +1827,8 @@ def do_down(self, arg): Move the current frame count (default one) levels down in the stack trace (to a newer frame). + + Will skip frames from ignored modules. """ if self.curindex + 1 == len(self.stack): self.error('Newest frame') @@ -1807,7 +1841,30 @@ def do_down(self, arg): if count < 0: newframe = len(self.stack) - 1 else: - newframe = min(len(self.stack) - 1, self.curindex + count) + # Skip over ignored modules + counter = 0 + module_skipped = 0 + for i in range(self.curindex + 1, len(self.stack)): + frame = self.stack[i][0] + should_skip_module = (self.skip and + self.is_skipped_module(frame.f_globals.get('__name__', ''))) + + if should_skip_module: + module_skipped += 1 + continue + counter += 1 + if counter >= count: + break + else: + # No valid frames found + self.error('All frames below are from ignored modules. ' + 'Use "unignore_module" to allow stepping into them.') + return + + newframe = i + if module_skipped: + self.message(f'[... skipped {module_skipped} frame(s) ' + 'from ignored modules]') self._select_frame(newframe) do_d = do_down @@ -2327,6 +2384,69 @@ def do_interact(self, arg): console.interact(banner="*pdb interact start*", exitmsg="*exit from pdb interact command*") + def _show_ignored_modules(self): + """Display currently ignored modules.""" + if self.skip: + self.message(f'Currently ignored modules: {sorted(self.skip)}') + else: + self.message('No modules are currently ignored.') + + def do_ignore_module(self, arg): + """ignore_module [module_name] + + Add a module to the list of modules to skip when stepping, + continuing, or navigating frames. When a module is ignored, + the debugger will automatically skip over frames from that + module during step, next, continue, up, and down commands. + + Supports wildcard patterns using glob-style matching: + + Usage: + ignore_module threading # Skip threading module frames + ignore_module asyncio.* # Skip all asyncio submodules + ignore_module *.tests # Skip all test modules + ignore_module # List currently ignored modules + """ + if self.skip is None: + self.skip = set() + + module_name = arg.strip() + + if not module_name: + self._show_ignored_modules() + return + + self.skip.add(module_name) + self.message(f'Ignoring module: {module_name}') + + def do_unignore_module(self, arg): + """unignore_module [module_name] + + Remove a module from the list of modules to skip when stepping + or navigating frames. This will allow the debugger to step into + frames from the specified module. + + Usage: + unignore_module threading # Stop ignoring threading module frames + unignore_module asyncio.* # Remove asyncio.* pattern + unignore_module # List currently ignored modules + """ + if self.skip is None: + self.skip = set() + + module_name = arg.strip() + + if not module_name: + self._show_ignored_modules() + return + + try: + self.skip.remove(module_name) + self.message(f'No longer ignoring module: {module_name}') + except KeyError: + self.error(f'Module {module_name} is not currently ignored') + self._show_ignored_modules() + def do_alias(self, arg): """alias [name [command]] diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 9a7d855003551a..7fbe3f977183fe 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1811,6 +1811,7 @@ def test_pdb_skip_modules(): > (4)skip_module() -> string.capwords('FOO') (Pdb) step + [... skipped 1 ignored module(s)] --Return-- > (4)skip_module()->None -> string.capwords('FOO') @@ -1884,6 +1885,7 @@ def test_pdb_skip_modules_with_callback(): > (5)skip_module() -> mod.foo_pony(callback) (Pdb) step + [... skipped 1 ignored module(s)] --Call-- > (2)callback() -> def callback(): @@ -1895,6 +1897,7 @@ def test_pdb_skip_modules_with_callback(): > (3)callback()->None -> return None (Pdb) step + [... skipped 1 ignored module(s)] --Return-- > (5)skip_module()->None -> mod.foo_pony(callback) @@ -5021,5 +5024,168 @@ def tearDown(test): return tests +def test_ignore_module(): + """Test the ignore_module and unignore_module commands. + + >>> def test_func(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + + Test basic ignore_module functionality: + + >>> with PdbTestInput([ # doctest: +ELLIPSIS + ... 'ignore_module', # List when empty + ... 'ignore_module sys', # Ignore sys module + ... 'ignore_module', # List ignored modules + ... 'ignore_module test.*', # Add wildcard pattern + ... 'ignore_module', # List again + ... 'unignore_module sys', # Remove sys + ... 'ignore_module', # List after removal + ... 'unignore_module nonexistent', # Try to remove non-existent + ... 'unignore_module test.*', # Remove pattern + ... 'ignore_module', # List when empty again + ... 'continue', + ... ]): + ... test_func() + > (2)test_func() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) ignore_module + No modules are currently ignored. + (Pdb) ignore_module sys + Ignoring module: sys + (Pdb) ignore_module + Currently ignored modules: ['sys'] + (Pdb) ignore_module test.* + Ignoring module: test.* + (Pdb) ignore_module + Currently ignored modules: ['sys', 'test.*'] + (Pdb) unignore_module sys + No longer ignoring module: sys + (Pdb) ignore_module + Currently ignored modules: ['test.*'] + (Pdb) unignore_module nonexistent + *** Module nonexistent is not currently ignored + Currently ignored modules: ['test.*'] + (Pdb) unignore_module test.* + No longer ignoring module: test.* + (Pdb) ignore_module + No modules are currently ignored. + (Pdb) continue + """ + + +# Modules for testing ignore_module with up/down navigation +ignore_test_middle = types.ModuleType('ignore_test_middle') +exec(''' +def middle_func(inner_func): + return inner_func() +''', ignore_test_middle.__dict__) + + +def test_ignore_module_navigation(): + """Test that ignore_module works with up/down commands. + + Create functions in different modules to test navigation skipping: + + >>> def outer_func(): + ... def inner_func(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... return "done" + ... return ignore_test_middle.middle_func(inner_func) + + Test navigation with ignored modules: + + >>> with PdbTestInput([ # doctest: +ELLIPSIS + ... 'up', # Go to middle_func + ... 'p "at middle_func"', + ... 'ignore_module ignore_test_middle', # Ignore the middle module + ... 'down', # Back to inner_func + ... 'p "at inner_func"', + ... 'up', # Should skip middle_func, go to outer_func + ... 'p "at outer_func"', + ... 'down', # Should skip middle_func back to inner_func + ... 'p "back at inner_func"', + ... 'continue', + ... ]): + ... outer_func() + > (3)inner_func() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) up + > (3)middle_func() + (Pdb) p "at middle_func" + 'at middle_func' + (Pdb) ignore_module ignore_test_middle + Ignoring module: ignore_test_middle + (Pdb) down + > (3)inner_func() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) p "at inner_func" + 'at inner_func' + (Pdb) up + [... skipped 1 frame(s) from ignored modules] + > (5)outer_func() + -> return ignore_test_middle.middle_func(inner_func) + (Pdb) p "at outer_func" + 'at outer_func' + (Pdb) down + [... skipped 1 frame(s) from ignored modules] + > (3)inner_func() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) p "back at inner_func" + 'back at inner_func' + (Pdb) continue + 'done' + """ + + +def test_ignore_module_stepping(): + """Test that ignore_module works with step/next commands. + + >>> def test_stepping(): + ... x = 1 + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... result = ignore_test_middle.middle_func(lambda: x + 1) + ... y = result + 1 + ... return y + + Test step command with ignored modules - should skip into ignored module + but show skip message: + + >>> with PdbTestInput([ # doctest: +ELLIPSIS + ... 'ignore_module ignore_test_middle', + ... 'step', # Step to the function call + ... 'step', # Step into call - should skip middle_func + ... 'return', # Return from lambda + ... 'step', # Step to next line + ... 'p y', + ... 'continue', + ... ]): + ... test_stepping() + > (3)test_stepping() + -> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + (Pdb) ignore_module ignore_test_middle + Ignoring module: ignore_test_middle + (Pdb) step + > (4)test_stepping() + -> result = ignore_test_middle.middle_func(lambda: x + 1) + (Pdb) step + [... skipped 1 ignored module(s)] + --Call-- + > (4)() + -> result = ignore_test_middle.middle_func(lambda: x + 1) + (Pdb) return + --Return-- + > (4)()->2 + -> result = ignore_test_middle.middle_func(lambda: x + 1) + (Pdb) step + [... skipped 1 ignored module(s)] + > (5)test_stepping() + -> y = result + 1 + (Pdb) p y + *** NameError: name 'y' is not defined + (Pdb) continue + 3 + """ + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2025-10-07-20-44-37.gh-issue-139721.N4dpE8.rst b/Misc/NEWS.d/next/Library/2025-10-07-20-44-37.gh-issue-139721.N4dpE8.rst new file mode 100644 index 00000000000000..5cb122480b46dc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-07-20-44-37.gh-issue-139721.N4dpE8.rst @@ -0,0 +1,5 @@ +Add ``ignore_module`` and ``unignore_module`` commands to :mod:`pdb` to +allow skipping frames from specified modules during debugging. When a module +is ignored, the debugger will automatically skip over its frames during +step, next, continue, up, and down commands. Supports wildcard patterns via +:mod:`fnmatch` for ignoring submodules.