Skip to content

Commit

Permalink
bpo-42848: remove recursion from TracebackException (GH-24158)
Browse files Browse the repository at this point in the history
  • Loading branch information
iritkatriel authored and adorilson committed Mar 11, 2021
1 parent a6304bd commit 5649d06
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 46 deletions.
25 changes: 25 additions & 0 deletions Lib/test/test_traceback.py
Expand Up @@ -1148,6 +1148,31 @@ def test_context(self):
self.assertEqual(exc_info[0], exc.exc_type)
self.assertEqual(str(exc_info[1]), str(exc))

def test_long_context_chain(self):
def f():
try:
1/0
except:
f()

try:
f()
except RecursionError:
exc_info = sys.exc_info()
else:
self.fail("Exception not raised")

te = traceback.TracebackException(*exc_info)
res = list(te.format())

# many ZeroDiv errors followed by the RecursionError
self.assertGreater(len(res), sys.getrecursionlimit())
self.assertGreater(
len([l for l in res if 'ZeroDivisionError:' in l]),
sys.getrecursionlimit() * 0.5)
self.assertIn(
"RecursionError: maximum recursion depth exceeded", res[-1])

def test_no_refs_to_exception_and_traceback_objects(self):
try:
1/0
Expand Down
115 changes: 69 additions & 46 deletions Lib/traceback.py
Expand Up @@ -481,39 +481,10 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
# permit backwards compat with the existing API, otherwise we
# need stub thunk objects just to glue it together.
# Handle loops in __cause__ or __context__.
is_recursive_call = _seen is not None
if _seen is None:
_seen = set()
_seen.add(id(exc_value))
# Gracefully handle (the way Python 2.4 and earlier did) the case of
# being called with no type or value (None, None, None).
if (exc_value and exc_value.__cause__ is not None
and id(exc_value.__cause__) not in _seen):
cause = TracebackException(
type(exc_value.__cause__),
exc_value.__cause__,
exc_value.__cause__.__traceback__,
limit=limit,
lookup_lines=False,
capture_locals=capture_locals,
_seen=_seen)
else:
cause = None
if (exc_value and exc_value.__context__ is not None
and id(exc_value.__context__) not in _seen):
context = TracebackException(
type(exc_value.__context__),
exc_value.__context__,
exc_value.__context__.__traceback__,
limit=limit,
lookup_lines=False,
capture_locals=capture_locals,
_seen=_seen)
else:
context = None
self.__cause__ = cause
self.__context__ = context
self.__suppress_context__ = \
exc_value.__suppress_context__ if exc_value else False
# TODO: locals.
self.stack = StackSummary.extract(
walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines,
Expand All @@ -532,6 +503,45 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
self.msg = exc_value.msg
if lookup_lines:
self._load_lines()
self.__suppress_context__ = \
exc_value.__suppress_context__ if exc_value else False

# Convert __cause__ and __context__ to `TracebackExceptions`s, use a
# queue to avoid recursion (only the top-level call gets _seen == None)
if not is_recursive_call:
queue = [(self, exc_value)]
while queue:
te, e = queue.pop()
if (e and e.__cause__ is not None
and id(e.__cause__) not in _seen):
cause = TracebackException(
type(e.__cause__),
e.__cause__,
e.__cause__.__traceback__,
limit=limit,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
_seen=_seen)
else:
cause = None
if (e and e.__context__ is not None
and id(e.__context__) not in _seen):
context = TracebackException(
type(e.__context__),
e.__context__,
e.__context__.__traceback__,
limit=limit,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
_seen=_seen)
else:
context = None
te.__cause__ = cause
te.__context__ = context
if cause:
queue.append((te.__cause__, e.__cause__))
if context:
queue.append((te.__context__, e.__context__))

@classmethod
def from_exception(cls, exc, *args, **kwargs):
Expand All @@ -542,10 +552,6 @@ def _load_lines(self):
"""Private API. force all lines in the stack to be loaded."""
for frame in self.stack:
frame.line
if self.__context__:
self.__context__._load_lines()
if self.__cause__:
self.__cause__._load_lines()

def __eq__(self, other):
if isinstance(other, TracebackException):
Expand Down Expand Up @@ -622,15 +628,32 @@ def format(self, *, chain=True):
The message indicating which exception occurred is always the last
string in the output.
"""
if chain:
if self.__cause__ is not None:
yield from self.__cause__.format(chain=chain)
yield _cause_message
elif (self.__context__ is not None and
not self.__suppress_context__):
yield from self.__context__.format(chain=chain)
yield _context_message
if self.stack:
yield 'Traceback (most recent call last):\n'
yield from self.stack.format()
yield from self.format_exception_only()

output = []
exc = self
while exc:
if chain:
if exc.__cause__ is not None:
chained_msg = _cause_message
chained_exc = exc.__cause__
elif (exc.__context__ is not None and
not exc.__suppress_context__):
chained_msg = _context_message
chained_exc = exc.__context__
else:
chained_msg = None
chained_exc = None

output.append((chained_msg, exc))
exc = chained_exc
else:
output.append((None, exc))
exc = None

for msg, exc in reversed(output):
if msg is not None:
yield msg
if exc.stack:
yield 'Traceback (most recent call last):\n'
yield from exc.stack.format()
yield from exc.format_exception_only()
@@ -0,0 +1 @@
Removed recursion from :class:`~traceback.TracebackException` to allow it to handle long exception chains.

0 comments on commit 5649d06

Please sign in to comment.