bpo-42848: remove recursion from TracebackException (GH-24158)

This commit is contained in:
Irit Katriel 2021-01-12 22:14:27 +00:00 committed by GitHub
parent 0f66498fd8
commit 6dfd1734f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 95 additions and 46 deletions

View File

@ -1148,6 +1148,31 @@ class TestTracebackException(unittest.TestCase):
self.assertEqual(exc_info[0], exc.exc_type) self.assertEqual(exc_info[0], exc.exc_type)
self.assertEqual(str(exc_info[1]), str(exc)) 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): def test_no_refs_to_exception_and_traceback_objects(self):
try: try:
1/0 1/0

View File

@ -481,39 +481,10 @@ class TracebackException:
# permit backwards compat with the existing API, otherwise we # permit backwards compat with the existing API, otherwise we
# need stub thunk objects just to glue it together. # need stub thunk objects just to glue it together.
# Handle loops in __cause__ or __context__. # Handle loops in __cause__ or __context__.
is_recursive_call = _seen is not None
if _seen is None: if _seen is None:
_seen = set() _seen = set()
_seen.add(id(exc_value)) _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. # TODO: locals.
self.stack = StackSummary.extract( self.stack = StackSummary.extract(
walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines, walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines,
@ -532,6 +503,45 @@ class TracebackException:
self.msg = exc_value.msg self.msg = exc_value.msg
if lookup_lines: if lookup_lines:
self._load_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 @classmethod
def from_exception(cls, exc, *args, **kwargs): def from_exception(cls, exc, *args, **kwargs):
@ -542,10 +552,6 @@ class TracebackException:
"""Private API. force all lines in the stack to be loaded.""" """Private API. force all lines in the stack to be loaded."""
for frame in self.stack: for frame in self.stack:
frame.line frame.line
if self.__context__:
self.__context__._load_lines()
if self.__cause__:
self.__cause__._load_lines()
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, TracebackException): if isinstance(other, TracebackException):
@ -622,15 +628,32 @@ class TracebackException:
The message indicating which exception occurred is always the last The message indicating which exception occurred is always the last
string in the output. string in the output.
""" """
output = []
exc = self
while exc:
if chain: if chain:
if self.__cause__ is not None: if exc.__cause__ is not None:
yield from self.__cause__.format(chain=chain) chained_msg = _cause_message
yield _cause_message chained_exc = exc.__cause__
elif (self.__context__ is not None and elif (exc.__context__ is not None and
not self.__suppress_context__): not exc.__suppress_context__):
yield from self.__context__.format(chain=chain) chained_msg = _context_message
yield _context_message chained_exc = exc.__context__
if self.stack: 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 'Traceback (most recent call last):\n'
yield from self.stack.format() yield from exc.stack.format()
yield from self.format_exception_only() yield from exc.format_exception_only()

View File

@ -0,0 +1 @@
Removed recursion from :class:`~traceback.TracebackException` to allow it to handle long exception chains.