Skip to content

bpo-42848: remove recursion from TracebackException #24158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Removed recursion from :class:`~traceback.TracebackException` to allow it to handle long exception chains.