Skip to content
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
37 changes: 37 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,43 @@ def test_base_exception(self):
lst = traceback.format_exception_only(e.__class__, e)
self.assertEqual(lst, ['KeyboardInterrupt\n'])

def test_traceback_context_recursionerror(self):
# Test that for long traceback chains traceback does not itself
# raise a recursion error while printing (Issue43048)

# Calling f() creates a stack-overflowing __context__ chain.
def f():
try:
raise ValueError('hello')
except ValueError:
f()

try:
f()
except RecursionError:
exc_info = sys.exc_info()

traceback.format_exception(exc_info[0], exc_info[1], exc_info[2])

def test_traceback_cause_recursionerror(self):
# Same as test_traceback_context_recursionerror, but with
# a __cause__ chain.

def f():
e = None
try:
f()
except Exception as exc:
e = exc
raise Exception from e

try:
f()
except Exception:
exc_info = sys.exc_info()

traceback.format_exception(exc_info[0], exc_info[1], exc_info[2])

def test_format_exception_only_bad__str__(self):
class X(Exception):
def __str__(self):
Expand Down
57 changes: 35 additions & 22 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,29 +476,38 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
_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:
self._truncated = False
try:
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
except RecursionError:
# The recursive call to the constructors above
# may result in a stack overflow for long exception chains,
# so we must truncate.
self._truncated = True
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
Expand Down Expand Up @@ -620,6 +629,10 @@ def format(self, *, chain=True):
not self.__suppress_context__):
yield from self.__context__.format(chain=chain)
yield _context_message
if self._truncated:
yield (
'Chained exceptions have been truncated to avoid '
'stack overflow in traceback formatting:\n')
if self.stack:
yield 'Traceback (most recent call last):\n'
yield from self.stack.format()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Handle `RecursionError` in :class:`~traceback.TracebackException`'s constructor, so that long exceptions chains are truncated instead of causing traceback formatting to fail.