##// END OF EJS Templates
Add support for chained exceptions....
Matthias Bussonnier -
Show More
@@ -108,6 +108,7 b' import re'
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,6 +128,11 b' from pdb import Pdb as OldPdb'
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 b' 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 b' class Pdb(OldPdb):'
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,9 +343,106 b' class Pdb(OldPdb):'
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
@@ -1246,7 +1246,13 b' class VerboseTB(TBTools):'
1246 1246 if etb and etb.tb_next:
1247 1247 etb = etb.tb_next
1248 1248 self.pdb.botframe = etb.tb_frame
1249 self.pdb.interaction(None, etb)
1249 # last_value should be deprecated, but last-exc sometimme not set
1250 # please check why later and remove the getattr.
1251 exc = sys.last_value if sys.version_info < (3, 12) else getattr(sys, "last_exc", sys.last_value) # type: ignore[attr-defined]
1252 if exc:
1253 self.pdb.interaction(None, exc)
1254 else:
1255 self.pdb.interaction(None, etb)
1250 1256
1251 1257 if hasattr(self, 'tb'):
1252 1258 del self.tb
@@ -68,8 +68,10 b' def test_debug_magic_passes_through_generators():'
68 68 child.expect_exact('----> 1 for x in gen:')
69 69
70 70 child.expect(ipdb_prompt)
71 child.sendline('u')
72 child.expect_exact('*** Oldest frame')
71 child.sendline("u")
72 child.expect_exact(
73 "*** all frames above hidden, use `skip_hidden False` to get get into those."
74 )
73 75
74 76 child.expect(ipdb_prompt)
75 77 child.sendline('exit')
@@ -1,6 +1,23 b''
1 1 ============
2 2 8.x Series
3 3 ============
4
5 .. _version 8.15:
6
7 IPython 8.15
8 ------------
9
10 Medium release of IPython after a couple of month hiatus, and a bit off-schedule.
11
12 The main change is the addition of the ability to move between chained
13 exceptions when using IPdb, this feature was also contributed to upstream Pdb
14 and is thus native to CPython in Python 3.13+ Though ipdb should support this
15 feature in older version of Python. I invite you to look at the `CPython changes
16 and docs <https://github.com/python/cpython/pull/106676>`_ for more details.
17
18 I also want o thanks the `D.E. Shaw group <https://www.deshaw.com/>`_ for
19 suggesting and funding this feature.
20
4 21 .. _version 8.14:
5 22
6 23 IPython 8.14
General Comments 0
You need to be logged in to leave comments. Login now