8000 bpo-42848: remove recursion from TracebackException (GH-24158) · python/cpython@6dfd173 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6dfd173

Browse files
authored
bpo-42848: remove recursion from TracebackException (GH-24158)
1 parent 0f66498 commit 6dfd173

File tree

3 files changed

+95
-46
lines changed

3 files changed

+95
-46
lines changed

Lib/test/test_traceback.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,31 @@ def test_context(self):
11481148
self.assertEqual(exc_info[0], exc.exc_type)
11491149
self.assertEqual(str(exc_info[1]), str(exc))
11501150

1151+
def test_long_context_chain(self):
1152+
def f():
1153+
try:
1154+
1/0
1155+
except:
1156+
f()
1157+
1158+
try:
1159+
f()
1160+
except RecursionError:
1161+
exc_info = sys.exc_info()
1162+
else:
1163+
self.fail("Exception not raised")
1164+
1165+
te = traceback.TracebackException(*exc_info)
1166+
res = list(te.format())
1167+
1168+
# many ZeroDiv errors followed by the RecursionError
1169+
self.assertGreater(len(res), sys.getrecursionlimit())
1170+
self.assertGreater(
1171+
len([l for l in res if 'ZeroDivisionError:' in l]),
1172+
sys.getrecursionlimit() * 0.5)
1173+
self.assertIn(
1174+
"RecursionError: maximum recursion depth exceeded", res[-1])
1175+
11511176
def test_no_refs_to_exception_and_traceback_objects(self):
11521177
try:
11531178
1/0

Lib/traceback.py

Lines changed: 69 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -481,39 +481,10 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
481481
# permit backwards compat with the existing API, otherwise we
482482
# need stub thunk objects just to glue it together.
483483
# Handle loops in __cause__ or __context__.
484+
is_recursive_call = _seen is not None
484485
if _seen is None:
485486
_seen = set()
486487
_seen.add(id(exc_value))
487-
# Gracefully handle (the way Python 2.4 and earlier did) the case of
488-
# being called with no type or value (None, None, None).
489-
if (exc_value and exc_value.__cause__ is not None
490-
and id(exc_value.__cause__) not in _seen):
491-
cause = TracebackException(
492-
type(exc_value.__cause__),
493-
exc_value.__cause__,
494-
exc_value.__cause__.__traceback__,
495-
limit=limit,
496-
lookup_lines=False,
497-
capture_locals=capture_locals,
498-
_seen=_seen)
499-
else:
500-
cause = None
501-
if (exc_value and exc_value.__context__ is not None
502-
and id(exc_value.__context__) not in _seen):
503-
context = TracebackException(
504-
type(exc_value.__context__),
505-
exc_value.__context__,
506-
exc_value.__context__.__traceback__,
507-
limit=limit,
508-
lookup_lines=False,
509-
capture_locals=capture_locals,
510-
_seen=_seen)
511-
else:
512-
context = None
513-
self.__cause__ = cause
514-
self.__context__ = context
515-
self.__suppress_context__ = \
516-
exc_value.__suppress_context__ if exc_value else False
517488
# TODO: locals.
518489
self.stack = StackSummary.extract(
519490
walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines,
@@ -532,6 +503,45 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
532503
self.msg = exc_value.msg
533504
if lookup_lines:
534505
self._load_lines()
506+
self.__suppress_context__ = \
507+
exc_value.__suppress_context__ if exc_value else False
508+
509+
# Convert __cause__ and __context__ to `TracebackExceptions`s, use a
510+
# queue to avoid recursion (only the top-level call gets _seen == None)
511+
if not is_recursive_call:
512+
queue = [(self, exc_value)]
513+
while queue:
514+
te, e = queue.pop()
515+
if (e and e.__cause__ is not None
516+
and id(e.__cause__) not in _seen):
517+
cause = TracebackException(
518+
type(e.__cause__),
519+
e.__cause__,
520+
e.__cause__.__traceback__,
521+
limit=limit,
522+
lookup_lines=lookup_lines,
523+
capture_locals=capture_locals,
524+
_seen=_seen)
525+
else:
526+
cause = None
527+
if (e and e.__context__ is not None
528+
and id(e.__context__) not in _seen):
529+
context = TracebackException(
530+
type(e.__context__),
531+
e.__context__,
532+
e.__context__.__traceback__,
533+
limit=limit,
534+
lookup_lines=lookup_lines,
535+
capture_locals=capture_locals,
536+
_seen=_seen)
537+
else:
538+
context = None
539+
te.__cause__ = cause
540+
te.__context__ = context
541+
if cause:
542+
queue.append((te.__cause__, e.__cause__))
543+
if context:
544+
queue.append((te.__context__, e.__context__))
535545

536546
@classmethod
537547
def from_exception(cls, exc, *args, **kwargs):
@@ -542,10 +552,6 @@ def _load_lines(self):
542552
"""Private API. force all lines in the stack to be loaded."""
543553
for frame in self.stack:
544554
frame.line
545-
if self.__context__:
546-
self.__context__._load_lines()
547-
if self.__cause__:
548-
self.__cause__._load_lines()
549555

550556
def __eq__(self, other):
551557
if isinstance(other, TracebackException):
@@ -622,15 +628,32 @@ def format(self, *, chain=True):
622628
The message indicating which exception occurred is always the last
623629
string in the output.
624630
"""
625-
if chain:
626-
if self.__cause__ is not None:
627-
yield from self.__cause__.format(chain=chain)
628-
yield _cause_message
629-
elif (self.__context__ is not None and
630-
not self.__suppress_context__):
631-
yield from self.__context__.format(chain=chain)
632-
yield _context_message
633-
if self.stack:
634-
yield 'Traceback (most recent call last):\n'
635-
yield from self.stack.format()
636-
yield from self.format_exception_only()
631+
632+
output = []
633+
exc = self
634+
while exc:
635+
if chain:
636+
if exc.__cause__ is not None:
637+
chained_msg = _cause_message
638+
chained_exc = exc.__cause__
639+
elif (exc.__context__ is not None and
640+
not exc.__suppress_context__):
641+
chained_msg = _context_message
642+
chained_exc = exc.__context__
643+
else:
644+
chained_msg = None
645+
chained_exc = None
646+
647+
output.append((chained_msg, exc))
648+
exc = chained_exc
649+
else:
650+
output.append((None, exc))
651+
exc = None
652+
653+
for msg, exc in reversed(output):
654+
if msg is not None:
655+
yield msg
656+
if exc.stack:
657+
yield 'Traceback (most recent call last):\n'
658+
yield from exc.stack.format()
659+
yield from exc.format_exception_only()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Removed recursion from :class:`~traceback.TracebackException` to allow it to handle long exception chains.

0 commit comments

Comments
 (0)
0