diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 5bb3a58b2a103b..987fcb955087e4 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -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): diff --git a/Lib/traceback.py b/Lib/traceback.py index d7fbdae680be62..d65a6098cc621f 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -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 @@ -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() diff --git a/Misc/NEWS.d/next/Library/2021-02-06-05-34-01.bpo-43048.yCPUmo.rst b/Misc/NEWS.d/next/Library/2021-02-06-05-34-01.bpo-43048.yCPUmo.rst new file mode 100644 index 00000000000000..99f6b2bbe9e30b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-02-06-05-34-01.bpo-43048.yCPUmo.rst @@ -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.