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( |
|
|
72 |
child.expect_exact( |
|
|
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