|
108 | 108 | import os
|
109 | 109 |
|
110 | 110 | from IPython import get_ipython
|
| 111 | +from contextlib import contextmanager |
111 | 112 | from IPython.utils import PyColorize
|
112 | 113 | from IPython.utils import coloransi, py3compat
|
113 | 114 | from IPython.core.excolors import exception_colors
|
|
127 | 128 | DEBUGGERSKIP = "__debuggerskip__"
|
128 | 129 |
|
129 | 130 |
|
| 131 | +# this has been implemented in Pdb in Python 3.13 (https://github.com/python/cpython/pull/106676 |
| 132 | +# on lower python versions, we backported the feature. |
| 133 | +CHAIN_EXCEPTIONS = sys.version_info < (3, 13) |
| 134 | + |
| 135 | + |
130 | 136 | def make_arrow(pad):
|
131 | 137 | """generate the leading arrow in front of traceback or debugger"""
|
132 | 138 | if pad >= 2:
|
@@ -185,6 +191,9 @@ class Pdb(OldPdb):
|
185 | 191 |
|
186 | 192 | """
|
187 | 193 |
|
| 194 | + if CHAIN_EXCEPTIONS: |
| 195 | + MAX_CHAINED_EXCEPTION_DEPTH = 999 |
| 196 | + |
188 | 197 | default_predicates = {
|
189 | 198 | "tbhide": True,
|
190 | 199 | "readonly": False,
|
@@ -281,6 +290,10 @@ def __init__(self, completekey=None, stdin=None, stdout=None, context=5, **kwarg
|
281 | 290 | # list of predicates we use to skip frames
|
282 | 291 | self._predicates = self.default_predicates
|
283 | 292 |
|
| 293 | + if CHAIN_EXCEPTIONS: |
| 294 | + self._chained_exceptions = tuple() |
| 295 | + self._chained_exception_index = 0 |
| 296 | + |
284 | 297 | #
|
285 | 298 | def set_colors(self, scheme):
|
286 | 299 | """Shorthand access to the color table scheme selector method."""
|
@@ -330,12 +343,110 @@ def hidden_frames(self, stack):
|
330 | 343 | ip_hide = [h if i > ip_start[0] else True for (i, h) in enumerate(ip_hide)]
|
331 | 344 | return ip_hide
|
332 | 345 |
|
333 |
| - def interaction(self, frame, traceback): |
| 346 | + if CHAIN_EXCEPTIONS: |
| 347 | + |
| 348 | + def _get_tb_and_exceptions(self, tb_or_exc): |
| 349 | + """ |
| 350 | + Given a tracecack or an exception, return a tuple of chained exceptions |
| 351 | + and current traceback to inspect. |
| 352 | + This will deal with selecting the right ``__cause__`` or ``__context__`` |
| 353 | + as well as handling cycles, and return a flattened list of exceptions we |
| 354 | + can jump to with do_exceptions. |
| 355 | + """ |
| 356 | + _exceptions = [] |
| 357 | + if isinstance(tb_or_exc, BaseException): |
| 358 | + traceback, current = tb_or_exc.__traceback__, tb_or_exc |
| 359 | + |
| 360 | + while current is not None: |
| 361 | + if current in _exceptions: |
| 362 | + break |
| 363 | + _exceptions.append(current) |
| 364 | + if current.__cause__ is not None: |
| 365 | + current = current.__cause__ |
| 366 | + elif ( |
| 367 | + current.__context__ is not None |
| 368 | + and not current.__suppress_context__ |
| 369 | + ): |
| 370 | + current = current.__context__ |
| 371 | + |
| 372 | + if len(_exceptions) >= self.MAX_CHAINED_EXCEPTION_DEPTH: |
| 373 | + self.message( |
| 374 | + f"More than {self.MAX_CHAINED_EXCEPTION_DEPTH}" |
| 375 | + " chained exceptions found, not all exceptions" |
| 376 | + "will be browsable with `exceptions`." |
| 377 | + ) |
| 378 | + break |
| 379 | + else: |
| 380 | + traceback = tb_or_exc |
| 381 | + return tuple(reversed(_exceptions)), traceback |
| 382 | + |
| 383 | + @contextmanager |
| 384 | + def _hold_exceptions(self, exceptions): |
| 385 | + """ |
| 386 | + Context manager to ensure proper cleaning of exceptions references |
| 387 | + When given a chained exception instead of a traceback, |
| 388 | + pdb may hold references to many objects which may leak memory. |
| 389 | + We use this context manager to make sure everything is properly cleaned |
| 390 | + """ |
| 391 | + try: |
| 392 | + self._chained_exceptions = exceptions |
| 393 | + self._chained_exception_index = len(exceptions) - 1 |
| 394 | + yield |
| 395 | + finally: |
| 396 | + # we can't put those in forget as otherwise they would |
| 397 | + # be cleared on exception change |
| 398 | + self._chained_exceptions = tuple() |
| 399 | + self._chained_exception_index = 0 |
| 400 | + |
| 401 | + def do_exceptions(self, arg): |
| 402 | + """exceptions [number] |
| 403 | + List or change current exception in an exception chain. |
| 404 | + Without arguments, list all the current exception in the exception |
| 405 | + chain. Exceptions will be numbered, with the current exception indicated |
| 406 | + with an arrow. |
| 407 | + If given an integer as argument, switch to the exception at that index. |
| 408 | + """ |
| 409 | + if not self._chained_exceptions: |
| 410 | + self.message( |
| 411 | + "Did not find chained exceptions. To move between" |
| 412 | + " exceptions, pdb/post_mortem must be given an exception" |
| 413 | + " object rather than a traceback." |
| 414 | + ) |
| 415 | + return |
| 416 | + if not arg: |
| 417 | + for ix, exc in enumerate(self._chained_exceptions): |
| 418 | + prompt = ">" if ix == self._chained_exception_index else " " |
| 419 | + rep = repr(exc) |
| 420 | + if len(rep) > 80: |
| 421 | + rep = rep[:77] + "..." |
| 422 | + self.message(f"{prompt} {ix:>3} {rep}") |
| 423 | + else: |
| 424 | + try: |
| 425 | + number = int(arg) |
| 426 | + except ValueError: |
| 427 | + self.error("Argument must be an integer") |
| 428 | + return |
| 429 | + if 0 <= number < len(self._chained_exceptions): |
| 430 | + self._chained_exception_index = number |
| 431 | + self.setup(None, self._chained_exceptions[number].__traceback__) |
| 432 | + self.print_stack_entry(self.stack[self.curindex]) |
| 433 | + else: |
| 434 | + self.error("No exception with that number") |
| 435 | + |
| 436 | + def interaction(self, frame, tb_or_exc): |
334 | 437 | try:
|
335 |
| - OldPdb.interaction(self, frame, traceback) |
| 438 | + if CHAIN_EXCEPTIONS: |
| 439 | + # this context manager is part of interaction in 3.13 |
| 440 | + _chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc) |
| 441 | + with self._hold_exceptions(_chained_exceptions): |
| 442 | + OldPdb.interaction(self, frame, tb) |
| 443 | + else: |
| 444 | + OldPdb.interaction(self, frame, traceback) |
| 445 | + |
336 | 446 | except KeyboardInterrupt:
|
337 | 447 | self.stdout.write("\n" + self.shell.get_exception_only())
|
338 | 448 |
|
| 449 | + |
339 | 450 | def precmd(self, line):
|
340 | 451 | """Perform useful escapes on the command before it is executed."""
|
341 | 452 |
|
|
0 commit comments