Skip to content

Commit ac97b3b

Browse files
committed
gh-139721: Add ignore_module/unignore_module commands to pdb
Add two new commands to pdb for skipping frames from specified modules: - ignore_module [module_name]: Add modules to skip during debugging - unignore_module [module_name]: Remove modules from skip list When a module is ignored, the debugger automatically skips over its frames during step, next, continue, up, and down commands. Supports wildcard patterns via fnmatch for flexible submodule matching.
1 parent 75b1afe commit ac97b3b

File tree

4 files changed

+280
-2
lines changed

4 files changed

+280
-2
lines changed

Doc/library/pdb.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,11 +438,17 @@ can be overridden by the local file.
438438
Move the current frame *count* (default one) levels down in the stack trace
439439
(to a newer frame).
440440

441+
Frames from ignored modules will be skipped. Use :pdbcmd:`ignore_module` to
442+
ignore modules and :pdbcmd:`unignore_module` to stop ignoring them.
443+
441444
.. pdbcommand:: u(p) [count]
442445

443446
Move the current frame *count* (default one) levels up in the stack trace (to
444447
an older frame).
445448

449+
Frames from ignored modules will be skipped. Use :pdbcmd:`ignore_module` to
450+
ignore modules and :pdbcmd:`unignore_module` to stop ignoring them.
451+
446452
.. pdbcommand:: b(reak) [([filename:]lineno | function) [, condition]]
447453

448454
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.
718724
:pdbcmd:`interact` directs its output to the debugger's
719725
output channel rather than :data:`sys.stderr`.
720726

727+
.. pdbcommand:: ignore_module [module_name]
728+
729+
Add a module to the list of modules to skip when stepping, continuing, or
730+
navigating frames. When a module is ignored, the debugger will automatically
731+
skip over frames from that module during :pdbcmd:`step`, :pdbcmd:`next`,
732+
:pdbcmd:`continue`, :pdbcmd:`up`, and :pdbcmd:`down` commands.
733+
734+
Supports wildcard patterns using glob-style matching (via :mod:`fnmatch`).
735+
736+
Without *module_name*, list the currently ignored modules.
737+
738+
Examples::
739+
740+
(Pdb) ignore_module threading # Skip threading module frames
741+
(Pdb) ignore_module asyncio.* # Skip all asyncio submodules
742+
(Pdb) ignore_module *.tests # Skip all test modules
743+
(Pdb) ignore_module # List currently ignored modules
744+
745+
Common use cases:
746+
747+
- Skip framework code (``django.*``, ``flask.*``, ``asyncio.*``)
748+
- Skip standard library modules (``threading``, ``multiprocessing``, ``logging.*``)
749+
- Skip test framework internals (``*pytest*``, ``unittest.*``)
750+
751+
.. versionadded:: 3.15
752+
753+
.. pdbcommand:: unignore_module [module_name]
754+
755+
Remove a module from the list of modules to skip when stepping or navigating
756+
frames. This will allow the debugger to step into frames from the specified
757+
module again.
758+
759+
Without *module_name*, list the currently ignored modules.
760+
761+
Example::
762+
763+
(Pdb) unignore_module threading # Stop ignoring threading module frames
764+
(Pdb) unignore_module asyncio.* # Remove the asyncio.* pattern
765+
766+
.. versionadded:: 3.15
767+
721768
.. _debugger-aliases:
722769

723770
.. pdbcommand:: alias [name [command]]

Lib/pdb.py

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,6 +1774,8 @@ def do_up(self, arg):
17741774
17751775
Move the current frame count (default one) levels up in the
17761776
stack trace (to an older frame).
1777+
1778+
Will skip frames from ignored modules.
17771779
"""
17781780
if self.curindex == 0:
17791781
self.error('Oldest frame')
@@ -1786,7 +1788,30 @@ def do_up(self, arg):
17861788
if count < 0:
17871789
newframe = 0
17881790
else:
1789-
newframe = max(0, self.curindex - count)
1791+
# Skip over ignored modules
1792+
counter = 0
1793+
module_skipped = 0
1794+
for i in range(self.curindex - 1, -1, -1):
1795+
frame = self.stack[i][0]
1796+
should_skip_module = (self.skip and
1797+
self.is_skipped_module(frame.f_globals.get('__name__', '')))
1798+
1799+
if should_skip_module:
1800+
module_skipped += 1
1801+
continue
1802+
counter += 1
1803+
if counter >= count:
1804+
break
1805+
else:
1806+
# No valid frames found
1807+
self.error('All frames above are from ignored modules. '
1808+
'Use "unignore_module" to allow stepping into them.')
1809+
return
1810+
1811+
newframe = i
1812+
if module_skipped:
1813+
self.message(f'[... skipped {module_skipped} frame(s) '
1814+
'from ignored modules]')
17901815
self._select_frame(newframe)
17911816
do_u = do_up
17921817

@@ -1795,6 +1820,8 @@ def do_down(self, arg):
17951820
17961821
Move the current frame count (default one) levels down in the
17971822
stack trace (to a newer frame).
1823+
1824+
Will skip frames from ignored modules.
17981825
"""
17991826
if self.curindex + 1 == len(self.stack):
18001827
self.error('Newest frame')
@@ -1807,7 +1834,30 @@ def do_down(self, arg):
18071834
if count < 0:
18081835
newframe = len(self.stack) - 1
18091836
else:
1810-
newframe = min(len(self.stack) - 1, self.curindex + count)
1837+
# Skip over ignored modules
1838+
counter = 0
1839+
module_skipped = 0
1840+
for i in range(self.curindex + 1, len(self.stack)):
1841+
frame = self.stack[i][0]
1842+
should_skip_module = (self.skip and
1843+
self.is_skipped_module(frame.f_globals.get('__name__', '')))
1844+
1845+
if should_skip_module:
1846+
module_skipped += 1
1847+
continue
1848+
counter += 1
1849+
if counter >= count:
1850+
break
1851+
else:
1852+
# No valid frames found
1853+
self.error('All frames below are from ignored modules. '
1854+
'Use "unignore_module" to allow stepping into them.')
1855+
return
1856+
1857+
newframe = i
1858+
if module_skipped:
1859+
self.message(f'[... skipped {module_skipped} frame(s) '
1860+
'from ignored modules]')
18111861
self._select_frame(newframe)
18121862
do_d = do_down
18131863

@@ -2327,6 +2377,69 @@ def do_interact(self, arg):
23272377
console.interact(banner="*pdb interact start*",
23282378
exitmsg="*exit from pdb interact command*")
23292379

2380+
def _show_ignored_modules(self):
2381+
"""Display currently ignored modules."""
2382+
if self.skip:
2383+
self.message(f'Currently ignored modules: {sorted(self.skip)}')
2384+
else:
2385+
self.message('No modules are currently ignored.')
2386+
2387+
def do_ignore_module(self, arg):
2388+
"""ignore_module [module_name]
2389+
2390+
Add a module to the list of modules to skip when stepping,
2391+
continuing, or navigating frames. When a module is ignored,
2392+
the debugger will automatically skip over frames from that
2393+
module during step, next, continue, up, and down commands.
2394+
2395+
Supports wildcard patterns using glob-style matching:
2396+
2397+
Usage:
2398+
ignore_module threading # Skip threading module frames
2399+
ignore_module asyncio.* # Skip all asyncio submodules
2400+
ignore_module *.tests # Skip all test modules
2401+
ignore_module # List currently ignored modules
2402+
"""
2403+
if self.skip is None:
2404+
self.skip = set()
2405+
2406+
module_name = arg.strip()
2407+
2408+
if not module_name:
2409+
self._show_ignored_modules()
2410+
return
2411+
2412+
self.skip.add(module_name)
2413+
self.message(f'Ignoring module: {module_name}')
2414+
2415+
def do_unignore_module(self, arg):
2416+
"""unignore_module [module_name]
2417+
2418+
Remove a module from the list of modules to skip when stepping
2419+
or navigating frames. This will allow the debugger to step into
2420+
frames from the specified module.
2421+
2422+
Usage:
2423+
unignore_module threading # Stop ignoring threading module frames
2424+
unignore_module asyncio.* # Remove asyncio.* pattern
2425+
unignore_module # List currently ignored modules
2426+
"""
2427+
if self.skip is None:
2428+
self.skip = set()
2429+
2430+
module_name = arg.strip()
2431+
2432+
if not module_name:
2433+
self._show_ignored_modules()
2434+
return
2435+
2436+
try:
2437+
self.skip.remove(module_name)
2438+
self.message(f'No longer ignoring module: {module_name}')
2439+
except KeyError:
2440+
self.error(f'Module {module_name} is not currently ignored')
2441+
self._show_ignored_modules()
2442+
23302443
def do_alias(self, arg):
23312444
"""alias [name [command]]
23322445

Lib/test/test_pdb.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5021,5 +5021,118 @@ def tearDown(test):
50215021
return tests
50225022

50235023

5024+
def test_ignore_module():
5025+
"""Test the ignore_module and unignore_module commands.
5026+
5027+
>>> def test_func():
5028+
... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
5029+
5030+
Test basic ignore_module functionality:
5031+
5032+
>>> with PdbTestInput([ # doctest: +ELLIPSIS
5033+
... 'ignore_module', # List when empty
5034+
... 'ignore_module sys', # Ignore sys module
5035+
... 'ignore_module', # List ignored modules
5036+
... 'ignore_module test.*', # Add wildcard pattern
5037+
... 'ignore_module', # List again
5038+
... 'unignore_module sys', # Remove sys
5039+
... 'ignore_module', # List after removal
5040+
... 'unignore_module nonexistent', # Try to remove non-existent
5041+
... 'unignore_module test.*', # Remove pattern
5042+
... 'ignore_module', # List when empty again
5043+
... 'continue',
5044+
... ]):
5045+
... test_func()
5046+
> <doctest test.test_pdb.test_ignore_module[0]>(2)test_func()
5047+
-> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
5048+
(Pdb) ignore_module
5049+
No modules are currently ignored.
5050+
(Pdb) ignore_module sys
5051+
Ignoring module: sys
5052+
(Pdb) ignore_module
5053+
Currently ignored modules: ['sys']
5054+
(Pdb) ignore_module test.*
5055+
Ignoring module: test.*
5056+
(Pdb) ignore_module
5057+
Currently ignored modules: ['sys', 'test.*']
5058+
(Pdb) unignore_module sys
5059+
No longer ignoring module: sys
5060+
(Pdb) ignore_module
5061+
Currently ignored modules: ['test.*']
5062+
(Pdb) unignore_module nonexistent
5063+
*** Module nonexistent is not currently ignored
5064+
Currently ignored modules: ['test.*']
5065+
(Pdb) unignore_module test.*
5066+
No longer ignoring module: test.*
5067+
(Pdb) ignore_module
5068+
No modules are currently ignored.
5069+
(Pdb) continue
5070+
"""
5071+
5072+
5073+
# Modules for testing ignore_module with up/down navigation
5074+
ignore_test_middle = types.ModuleType('ignore_test_middle')
5075+
exec('''
5076+
def middle_func(inner_func):
5077+
return inner_func()
5078+
''', ignore_test_middle.__dict__)
5079+
5080+
5081+
def test_ignore_module_navigation():
5082+
"""Test that ignore_module works with up/down commands.
5083+
5084+
Create functions in different modules to test navigation skipping:
5085+
5086+
>>> def outer_func():
5087+
... def inner_func():
5088+
... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
5089+
... return "done"
5090+
... return ignore_test_middle.middle_func(inner_func)
5091+
5092+
Test navigation with ignored modules:
5093+
5094+
>>> with PdbTestInput([ # doctest: +ELLIPSIS
5095+
... 'up', # Go to middle_func
5096+
... 'p "at middle_func"',
5097+
... 'ignore_module ignore_test_middle', # Ignore the middle module
5098+
... 'down', # Back to inner_func
5099+
... 'p "at inner_func"',
5100+
... 'up', # Should skip middle_func, go to outer_func
5101+
... 'p "at outer_func"',
5102+
... 'down', # Should skip middle_func back to inner_func
5103+
... 'p "back at inner_func"',
5104+
... 'continue',
5105+
... ]):
5106+
... outer_func()
5107+
> <doctest test.test_pdb.test_ignore_module_navigation[0]>(3)inner_func()
5108+
-> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
5109+
(Pdb) up
5110+
> <string>(3)middle_func()
5111+
(Pdb) p "at middle_func"
5112+
'at middle_func'
5113+
(Pdb) ignore_module ignore_test_middle
5114+
Ignoring module: ignore_test_middle
5115+
(Pdb) down
5116+
> <doctest test.test_pdb.test_ignore_module_navigation[0]>(3)inner_func()
5117+
-> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
5118+
(Pdb) p "at inner_func"
5119+
'at inner_func'
5120+
(Pdb) up
5121+
[... skipped 1 frame(s) from ignored modules]
5122+
> <doctest test.test_pdb.test_ignore_module_navigation[0]>(5)outer_func()
5123+
-> return ignore_test_middle.middle_func(inner_func)
5124+
(Pdb) p "at outer_func"
5125+
'at outer_func'
5126+
(Pdb) down
5127+
[... skipped 1 frame(s) from ignored modules]
5128+
> <doctest test.test_pdb.test_ignore_module_navigation[0]>(3)inner_func()
5129+
-> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
5130+
(Pdb) p "back at inner_func"
5131+
'back at inner_func'
5132+
(Pdb) continue
5133+
'done'
5134+
"""
5135+
5136+
50245137
if __name__ == '__main__':
50255138
unittest.main()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Add ``ignore_module`` and ``unignore_module`` commands to :mod:`pdb` to
2+
allow skipping frames from specified modules during debugging. When a module
3+
is ignored, the debugger will automatically skip over its frames during
4+
step, next, continue, up, and down commands. Supports wildcard patterns via
5+
:mod:`fnmatch` for ignoring submodules.

0 commit comments

Comments
 (0)