Show More
@@ -108,6 +108,7 b' import re' | |||||
108 | import os |
|
108 | import os | |
109 |
|
109 | |||
110 | from IPython import get_ipython |
|
110 | from IPython import get_ipython | |
|
111 | from contextlib import contextmanager | |||
111 | from IPython.utils import PyColorize |
|
112 | from IPython.utils import PyColorize | |
112 | from IPython.utils import coloransi, py3compat |
|
113 | from IPython.utils import coloransi, py3compat | |
113 | from IPython.core.excolors import exception_colors |
|
114 | from IPython.core.excolors import exception_colors | |
@@ -127,6 +128,11 b' from pdb import Pdb as OldPdb' | |||||
127 | DEBUGGERSKIP = "__debuggerskip__" |
|
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 | def make_arrow(pad): |
|
136 | def make_arrow(pad): | |
131 | """generate the leading arrow in front of traceback or debugger""" |
|
137 | """generate the leading arrow in front of traceback or debugger""" | |
132 | if pad >= 2: |
|
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 | default_predicates = { |
|
197 | default_predicates = { | |
189 | "tbhide": True, |
|
198 | "tbhide": True, | |
190 | "readonly": False, |
|
199 | "readonly": False, | |
@@ -281,6 +290,10 b' class Pdb(OldPdb):' | |||||
281 | # list of predicates we use to skip frames |
|
290 | # list of predicates we use to skip frames | |
282 | self._predicates = self.default_predicates |
|
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 | def set_colors(self, scheme): |
|
298 | def set_colors(self, scheme): | |
286 | """Shorthand access to the color table scheme selector method.""" |
|
299 | """Shorthand access to the color table scheme selector method.""" | |
@@ -330,9 +343,106 b' class Pdb(OldPdb):' | |||||
330 | ip_hide = [h if i > ip_start[0] else True for (i, h) in enumerate(ip_hide)] |
|
343 | ip_hide = [h if i > ip_start[0] else True for (i, h) in enumerate(ip_hide)] | |
331 | return ip_hide |
|
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 | """ | |||
334 | try: |
|
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): | |||
|
437 | try: | |||
|
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: | |||
335 | OldPdb.interaction(self, frame, traceback) |
|
444 | OldPdb.interaction(self, frame, traceback) | |
|
445 | ||||
336 | except KeyboardInterrupt: |
|
446 | except KeyboardInterrupt: | |
337 | self.stdout.write("\n" + self.shell.get_exception_only()) |
|
447 | self.stdout.write("\n" + self.shell.get_exception_only()) | |
338 |
|
448 |
@@ -1246,6 +1246,12 b' class VerboseTB(TBTools):' | |||||
1246 | if etb and etb.tb_next: |
|
1246 | if etb and etb.tb_next: | |
1247 | etb = etb.tb_next |
|
1247 | etb = etb.tb_next | |
1248 | self.pdb.botframe = etb.tb_frame |
|
1248 | self.pdb.botframe = etb.tb_frame | |
|
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: | |||
1249 | self.pdb.interaction(None, etb) |
|
1255 | self.pdb.interaction(None, etb) | |
1250 |
|
1256 | |||
1251 | if hasattr(self, 'tb'): |
|
1257 | if hasattr(self, 'tb'): |
@@ -68,8 +68,10 b' def test_debug_magic_passes_through_generators():' | |||||
68 | child.expect_exact('----> 1 for x in gen:') |
|
68 | child.expect_exact('----> 1 for x in gen:') | |
69 |
|
69 | |||
70 | child.expect(ipdb_prompt) |
|
70 | child.expect(ipdb_prompt) | |
71 |
child.sendline( |
|
71 | child.sendline("u") | |
72 |
child.expect_exact( |
|
72 | child.expect_exact( | |
|
73 | "*** all frames above hidden, use `skip_hidden False` to get get into those." | |||
|
74 | ) | |||
73 |
|
75 | |||
74 | child.expect(ipdb_prompt) |
|
76 | child.expect(ipdb_prompt) | |
75 | child.sendline('exit') |
|
77 | child.sendline('exit') |
@@ -1,6 +1,23 b'' | |||||
1 | ============ |
|
1 | ============ | |
2 | 8.x Series |
|
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 | .. _version 8.14: |
|
21 | .. _version 8.14: | |
5 |
|
22 | |||
6 | IPython 8.14 |
|
23 | IPython 8.14 |
General Comments 0
You need to be logged in to leave comments.
Login now