85
85
import traceback
86
86
import linecache
87
87
88
+ from contextlib import contextmanager
88
89
from typing import Union
89
90
90
91
@@ -205,10 +206,15 @@ def namespace(self):
205
206
# line_prefix = ': ' # Use this to get the old situation back
206
207
line_prefix = '\n -> ' # Probably a better default
207
208
208
- class Pdb (bdb .Bdb , cmd .Cmd ):
209
209
210
+
211
+ class Pdb (bdb .Bdb , cmd .Cmd ):
210
212
_previous_sigint_handler = None
211
213
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
+
212
218
def __init__ (self , completekey = 'tab' , stdin = None , stdout = None , skip = None ,
213
219
nosigint = False , readrc = True ):
214
220
bdb .Bdb .__init__ (self , skip = skip )
@@ -256,6 +262,9 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
256
262
self .commands_bnum = None # The breakpoint number for which we are
257
263
# defining a list
258
264
265
+ self ._chained_exceptions = tuple ()
266
+ self ._chained_exception_index = 0
267
+
259
268
def sigint_handler (self , signum , frame ):
260
269
if self .allow_kbdint :
261
270
raise KeyboardInterrupt
@@ -414,7 +423,64 @@ def preloop(self):
414
423
self .message ('display %s: %r [old: %r]' %
415
424
(expr , newvalue , oldvalue ))
416
425
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 ):
418
484
# Restore the previous signal handler at the Pdb prompt.
419
485
if Pdb ._previous_sigint_handler :
420
486
try :
@@ -423,14 +489,17 @@ def interaction(self, frame, traceback):
423
489
pass
424
490
else :
425
491
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 ()
429
502
self .forget ()
430
- return
431
- self .print_stack_entry (self .stack [self .curindex ])
432
- self ._cmdloop ()
433
- self .forget ()
434
503
435
504
def displayhook (self , obj ):
436
505
"""Custom displayhook for the exec in default(), which prevents
@@ -1073,6 +1142,44 @@ def _select_frame(self, number):
1073
1142
self .print_stack_entry (self .stack [self .curindex ])
1074
1143
self .lineno = None
1075
1144
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
+
1076
1183
def do_up (self , arg ):
1077
1184
"""u(p) [count]
1078
1185
@@ -1890,11 +1997,15 @@ def set_trace(*, header=None):
1890
1997
# Post-Mortem interface
1891
1998
1892
1999
def 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.
1894
2002
1895
2003
If no traceback is given, it uses the one of the exception that is
1896
2004
currently being handled (an exception must be being handled if the
1897
2005
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).
1898
2009
"""
1899
2010
# handling the default
1900
2011
if t is None :
@@ -1911,12 +2022,8 @@ def post_mortem(t=None):
1911
2022
p .interaction (None , t )
1912
2023
1913
2024
def 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 )
1920
2027
1921
2028
1922
2029
# Main program for testing
@@ -1996,8 +2103,7 @@ def main():
1996
2103
traceback .print_exc ()
1997
2104
print ("Uncaught exception. Entering post mortem debugging" )
1998
2105
print ("Running 'cont' or 'step' will restart the program" )
1999
- t = e .__traceback__
2000
- pdb .interaction (None , t )
2106
+ pdb .interaction (None , e )
2001
2107
print ("Post mortem debugger finished. The " + target +
2002
2108
" will be restarted" )
2003
2109
0 commit comments