Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitf75cefd

Browse files
authored
gh-106670: Allow Pdb to move between chained exceptions (#106676)
1 parent242bef4 commitf75cefd

File tree

5 files changed

+526
-20
lines changed

5 files changed

+526
-20
lines changed

‎Doc/library/pdb.rst

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ slightly different way:
175175

176176
..function::pm()
177177

178-
Enter post-mortem debugging of thetraceback found in
179-
:data:`sys.last_traceback`.
178+
Enter post-mortem debugging of theexception found in
179+
:data:`sys.last_exc`.
180180

181181

182182
The ``run*`` functions and:func:`set_trace` are aliases for instantiating the
@@ -639,6 +639,55 @@ can be overridden by the local file.
639639

640640
Print the return value for the last return of the current function.
641641

642+
..pdbcommand::exceptions [excnumber]
643+
644+
List or jump between chained exceptions.
645+
646+
When using ``pdb.pm()`` or ``Pdb.post_mortem(...)`` with a chained exception
647+
instead of a traceback, it allows the user to move between the
648+
chained exceptions using ``exceptions`` command to list exceptions, and
649+
``exception <number>`` to switch to that exception.
650+
651+
652+
Example::
653+
654+
def out():
655+
try:
656+
middle()
657+
except Exception as e:
658+
raise ValueError("reraise middle() error") from e
659+
660+
def middle():
661+
try:
662+
return inner(0)
663+
except Exception as e:
664+
raise ValueError("Middle fail")
665+
666+
def inner(x):
667+
1 / x
668+
669+
out()
670+
671+
calling ``pdb.pm()`` will allow to move between exceptions::
672+
673+
> example.py(5)out()
674+
-> raise ValueError("reraise middle() error") from e
675+
676+
(Pdb) exceptions
677+
0 ZeroDivisionError('division by zero')
678+
1 ValueError('Middle fail')
679+
> 2 ValueError('reraise middle() error')
680+
681+
(Pdb) exceptions 0
682+
> example.py(16)inner()
683+
-> 1 / x
684+
685+
(Pdb) up
686+
> example.py(10)middle()
687+
-> return inner(0)
688+
689+
..versionadded::3.13
690+
642691
..rubric::Footnotes
643692

644693
.. [1]Whether a frame is considered to originate in a certain module

‎Doc/whatsnew/3.13.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ pathlib
158158
:meth:`~pathlib.Path.is_dir`.
159159
(Contributed by Barney Gale in:gh:`77609` and:gh:`105793`.)
160160

161+
pdb
162+
---
163+
164+
* Add ability to move between chained exceptions during post mortem debugging in:func:`~pdb.pm` using
165+
the new ``exceptions [exc_number]`` command for Pdb. (Contributed by Matthias
166+
Bussonnier in:gh:`106676`.)
167+
161168
sqlite3
162169
-------
163170

‎Lib/pdb.py

Lines changed: 124 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
importtraceback
8686
importlinecache
8787

88+
fromcontextlibimportcontextmanager
8889
fromtypingimportUnion
8990

9091

@@ -205,10 +206,15 @@ def namespace(self):
205206
# line_prefix = ': ' # Use this to get the old situation back
206207
line_prefix='\n-> '# Probably a better default
207208

208-
classPdb(bdb.Bdb,cmd.Cmd):
209209

210+
211+
classPdb(bdb.Bdb,cmd.Cmd):
210212
_previous_sigint_handler=None
211213

214+
# Limit the maximum depth of chained exceptions, we should be handling cycles,
215+
# but in case there are recursions, we stop at 999.
216+
MAX_CHAINED_EXCEPTION_DEPTH=999
217+
212218
def__init__(self,completekey='tab',stdin=None,stdout=None,skip=None,
213219
nosigint=False,readrc=True):
214220
bdb.Bdb.__init__(self,skip=skip)
@@ -256,6 +262,9 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
256262
self.commands_bnum=None# The breakpoint number for which we are
257263
# defining a list
258264

265+
self._chained_exceptions=tuple()
266+
self._chained_exception_index=0
267+
259268
defsigint_handler(self,signum,frame):
260269
ifself.allow_kbdint:
261270
raiseKeyboardInterrupt
@@ -414,7 +423,64 @@ def preloop(self):
414423
self.message('display %s: %r [old: %r]'%
415424
(expr,newvalue,oldvalue))
416425

417-
definteraction(self,frame,traceback):
426+
def_get_tb_and_exceptions(self,tb_or_exc):
427+
"""
428+
Given a tracecack or an exception, return a tuple of chained exceptions
429+
and current traceback to inspect.
430+
431+
This will deal with selecting the right ``__cause__`` or ``__context__``
432+
as well as handling cycles, and return a flattened list of exceptions we
433+
can jump to with do_exceptions.
434+
435+
"""
436+
_exceptions= []
437+
ifisinstance(tb_or_exc,BaseException):
438+
traceback,current=tb_or_exc.__traceback__,tb_or_exc
439+
440+
whilecurrentisnotNone:
441+
ifcurrentin_exceptions:
442+
break
443+
_exceptions.append(current)
444+
ifcurrent.__cause__isnotNone:
445+
current=current.__cause__
446+
elif (
447+
current.__context__isnotNoneandnotcurrent.__suppress_context__
448+
):
449+
current=current.__context__
450+
451+
iflen(_exceptions)>=self.MAX_CHAINED_EXCEPTION_DEPTH:
452+
self.message(
453+
f"More than{self.MAX_CHAINED_EXCEPTION_DEPTH}"
454+
" chained exceptions found, not all exceptions"
455+
"will be browsable with `exceptions`."
456+
)
457+
break
458+
else:
459+
traceback=tb_or_exc
460+
returntuple(reversed(_exceptions)),traceback
461+
462+
@contextmanager
463+
def_hold_exceptions(self,exceptions):
464+
"""
465+
Context manager to ensure proper cleaning of exceptions references
466+
467+
When given a chained exception instead of a traceback,
468+
pdb may hold references to many objects which may leak memory.
469+
470+
We use this context manager to make sure everything is properly cleaned
471+
472+
"""
473+
try:
474+
self._chained_exceptions=exceptions
475+
self._chained_exception_index=len(exceptions)-1
476+
yield
477+
finally:
478+
# we can't put those in forget as otherwise they would
479+
# be cleared on exception change
480+
self._chained_exceptions=tuple()
481+
self._chained_exception_index=0
482+
483+
definteraction(self,frame,tb_or_exc):
418484
# Restore the previous signal handler at the Pdb prompt.
419485
ifPdb._previous_sigint_handler:
420486
try:
@@ -423,14 +489,17 @@ def interaction(self, frame, traceback):
423489
pass
424490
else:
425491
Pdb._previous_sigint_handler=None
426-
ifself.setup(frame,traceback):
427-
# no interaction desired at this time (happens if .pdbrc contains
428-
# a command like "continue")
492+
493+
_chained_exceptions,tb=self._get_tb_and_exceptions(tb_or_exc)
494+
withself._hold_exceptions(_chained_exceptions):
495+
ifself.setup(frame,tb):
496+
# no interaction desired at this time (happens if .pdbrc contains
497+
# a command like "continue")
498+
self.forget()
499+
return
500+
self.print_stack_entry(self.stack[self.curindex])
501+
self._cmdloop()
429502
self.forget()
430-
return
431-
self.print_stack_entry(self.stack[self.curindex])
432-
self._cmdloop()
433-
self.forget()
434503

435504
defdisplayhook(self,obj):
436505
"""Custom displayhook for the exec in default(), which prevents
@@ -1073,6 +1142,44 @@ def _select_frame(self, number):
10731142
self.print_stack_entry(self.stack[self.curindex])
10741143
self.lineno=None
10751144

1145+
defdo_exceptions(self,arg):
1146+
"""exceptions [number]
1147+
1148+
List or change current exception in an exception chain.
1149+
1150+
Without arguments, list all the current exception in the exception
1151+
chain. Exceptions will be numbered, with the current exception indicated
1152+
with an arrow.
1153+
1154+
If given an integer as argument, switch to the exception at that index.
1155+
"""
1156+
ifnotself._chained_exceptions:
1157+
self.message(
1158+
"Did not find chained exceptions. To move between"
1159+
" exceptions, pdb/post_mortem must be given an exception"
1160+
" object rather than a traceback."
1161+
)
1162+
return
1163+
ifnotarg:
1164+
forix,excinenumerate(self._chained_exceptions):
1165+
prompt=">"ifix==self._chained_exception_indexelse" "
1166+
rep=repr(exc)
1167+
iflen(rep)>80:
1168+
rep=rep[:77]+"..."
1169+
self.message(f"{prompt}{ix:>3}{rep}")
1170+
else:
1171+
try:
1172+
number=int(arg)
1173+
exceptValueError:
1174+
self.error("Argument must be an integer")
1175+
return
1176+
if0<=number<len(self._chained_exceptions):
1177+
self._chained_exception_index=number
1178+
self.setup(None,self._chained_exceptions[number].__traceback__)
1179+
self.print_stack_entry(self.stack[self.curindex])
1180+
else:
1181+
self.error("No exception with that number")
1182+
10761183
defdo_up(self,arg):
10771184
"""u(p) [count]
10781185
@@ -1890,11 +1997,15 @@ def set_trace(*, header=None):
18901997
# Post-Mortem interface
18911998

18921999
defpost_mortem(t=None):
1893-
"""Enter post-mortem debugging of the given *traceback* object.
2000+
"""Enter post-mortem debugging of the given *traceback*, or *exception*
2001+
object.
18942002
18952003
If no traceback is given, it uses the one of the exception that is
18962004
currently being handled (an exception must be being handled if the
18972005
default is to be used).
2006+
2007+
If `t` is an exception object, the `exceptions` command makes it possible to
2008+
list and inspect its chained exceptions (if any).
18982009
"""
18992010
# handling the default
19002011
iftisNone:
@@ -1911,12 +2022,8 @@ def post_mortem(t=None):
19112022
p.interaction(None,t)
19122023

19132024
defpm():
1914-
"""Enter post-mortem debugging of the traceback found in sys.last_traceback."""
1915-
ifhasattr(sys,'last_exc'):
1916-
tb=sys.last_exc.__traceback__
1917-
else:
1918-
tb=sys.last_traceback
1919-
post_mortem(tb)
2025+
"""Enter post-mortem debugging of the traceback found in sys.last_exc."""
2026+
post_mortem(sys.last_exc)
19202027

19212028

19222029
# Main program for testing
@@ -1996,8 +2103,7 @@ def main():
19962103
traceback.print_exc()
19972104
print("Uncaught exception. Entering post mortem debugging")
19982105
print("Running 'cont' or 'step' will restart the program")
1999-
t=e.__traceback__
2000-
pdb.interaction(None,t)
2106+
pdb.interaction(None,e)
20012107
print("Post mortem debugger finished. The "+target+
20022108
" will be restarted")
20032109

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp