8585import traceback
8686import linecache
8787
88+ from contextlib import contextmanager
8889from typing import Union
8990
9091
@@ -205,10 +206,15 @@ def namespace(self):
205206# line_prefix = ': ' # Use this to get the old situation back
206207line_prefix = '\n -> ' # Probably a better default
207208
208- class Pdb (bdb .Bdb , cmd .Cmd ):
209209
210+
211+ class Pdb (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 def sigint_handler (self , signum , frame ):
260269 if self .allow_kbdint :
261270 raise KeyboardInterrupt
@@ -414,7 +423,64 @@ def preloop(self):
414423 self .message ('display %s: %r [old: %r]' %
415424 (expr , newvalue , oldvalue ))
416425
417- def interaction (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+ if isinstance (tb_or_exc , BaseException ):
438+ traceback , current = tb_or_exc .__traceback__ , tb_or_exc
439+
440+ while current is not None :
441+ if current in _exceptions :
442+ break
443+ _exceptions .append (current )
444+ if current .__cause__ is not None :
445+ current = current .__cause__
446+ elif (
447+ current .__context__ is not None and not current .__suppress_context__
448+ ):
449+ current = current .__context__
450+
451+ if len (_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+ return tuple (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+ def interaction (self , frame , tb_or_exc ):
418484 # Restore the previous signal handler at the Pdb prompt.
419485 if Pdb ._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- if self .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+ with self ._hold_exceptions (_chained_exceptions ):
495+ if self .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 def displayhook (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+ def do_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+ if not self ._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+ if not arg :
1164+ for ix , exc in enumerate (self ._chained_exceptions ):
1165+ prompt = ">" if ix == self ._chained_exception_index else " "
1166+ rep = repr (exc )
1167+ if len (rep ) > 80 :
1168+ rep = rep [:77 ] + "..."
1169+ self .message (f"{ prompt } { ix :>3} { rep } " )
1170+ else :
1171+ try :
1172+ number = int (arg )
1173+ except ValueError :
1174+ self .error ("Argument must be an integer" )
1175+ return
1176+ if 0 <= 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 def do_up (self , arg ):
10771184 """u(p) [count]
10781185
@@ -1890,11 +1997,15 @@ def set_trace(*, header=None):
18901997# Post-Mortem interface
18911998
18921999def post_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 if t is None :
@@ -1911,12 +2022,8 @@ def post_mortem(t=None):
19112022 p .interaction (None , t )
19122023
19132024def pm ():
1914- """Enter post-mortem debugging of the traceback found in sys.last_traceback."""
1915- if hasattr (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