diff --git a/Include/internal/pycore_traceback.h b/Include/internal/pycore_traceback.h index c01a47639d5..84dbe27044f 100644 --- a/Include/internal/pycore_traceback.h +++ b/Include/internal/pycore_traceback.h @@ -87,6 +87,17 @@ PyAPI_FUNC(PyObject*) _PyTraceBack_FromFrame( PyObject *tb_next, PyFrameObject *frame); +#define EXCEPTION_TB_HEADER "Traceback (most recent call last):\n" +#define EXCEPTION_GROUP_TB_HEADER "Exception Group Traceback (most recent call last):\n" + +/* Write the traceback tb to file f. Prefix each line with + indent spaces followed by the margin (if it is not NULL). */ +PyAPI_FUNC(int) _PyTraceBack_Print_Indented( + PyObject *tb, int indent, const char* margin, + const char *header_margin, const char *header, PyObject *f); +PyAPI_FUNC(int) _Py_WriteIndentedMargin(int, const char*, PyObject *); +PyAPI_FUNC(int) _Py_WriteIndent(int, PyObject *); + #ifdef __cplusplus } #endif diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 1c7db9d3d47..d88851ddda4 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -987,6 +987,35 @@ class TracebackFormatTests(unittest.TestCase): self.assertIn('UnhashableException: ex2', tb[4]) self.assertIn('UnhashableException: ex1', tb[12]) + def deep_eg(self): + e = TypeError(1) + for i in range(2000): + e = ExceptionGroup('eg', [e]) + return e + + @cpython_only + def test_exception_group_deep_recursion_capi(self): + from _testcapi import exception_print + LIMIT = 75 + eg = self.deep_eg() + with captured_output("stderr") as stderr_f: + with support.infinite_recursion(max_depth=LIMIT): + exception_print(eg) + output = stderr_f.getvalue() + self.assertIn('ExceptionGroup', output) + self.assertLessEqual(output.count('ExceptionGroup'), LIMIT) + + def test_exception_group_deep_recursion_traceback(self): + LIMIT = 75 + eg = self.deep_eg() + with captured_output("stderr") as stderr_f: + with support.infinite_recursion(max_depth=LIMIT): + traceback.print_exception(type(eg), eg, eg.__traceback__) + output = stderr_f.getvalue() + self.assertIn('ExceptionGroup', output) + self.assertLessEqual(output.count('ExceptionGroup'), LIMIT) + + cause_message = ( "\nThe above exception was the direct cause " "of the following exception:\n\n") @@ -998,7 +1027,6 @@ context_message = ( boundaries = re.compile( '(%s|%s)' % (re.escape(cause_message), re.escape(context_message))) - class BaseExceptionReportingTests: def get_exception(self, exception_or_callable): @@ -1009,6 +1037,8 @@ class BaseExceptionReportingTests: except Exception as e: return e + callable_line = get_exception.__code__.co_firstlineno + 4 + def zero_div(self): 1/0 # In zero_div @@ -1234,6 +1264,298 @@ class BaseExceptionReportingTests: self.assertEqual(err, f"{str_name}: {str_value}\n") + # #### Exception Groups #### + + def test_exception_group_basic(self): + def exc(): + raise ExceptionGroup("eg", [ValueError(1), TypeError(2)]) + + expected = ( + f' + Exception Group Traceback (most recent call last):\n' + f' | File "{__file__}", line {self.callable_line}, in get_exception\n' + f' | exception_or_callable()\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\n' + f' | raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | ExceptionGroup: eg\n' + f' +-+---------------- 1 ----------------\n' + f' | ValueError: 1\n' + f' +---------------- 2 ----------------\n' + f' | TypeError: 2\n' + f' +------------------------------------\n') + + report = self.get_report(exc) + self.assertEqual(report, expected) + + def test_exception_group_cause(self): + def exc(): + EG = ExceptionGroup + try: + raise EG("eg1", [ValueError(1), TypeError(2)]) + except Exception as e: + raise EG("eg2", [ValueError(3), TypeError(4)]) from e + + expected = (f' + Exception Group Traceback (most recent call last):\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 3}, in exc\n' + f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | ExceptionGroup: eg1\n' + f' +-+---------------- 1 ----------------\n' + f' | ValueError: 1\n' + f' +---------------- 2 ----------------\n' + f' | TypeError: 2\n' + f' +------------------------------------\n' + f'\n' + f'The above exception was the direct cause of the following exception:\n' + f'\n' + f' + Exception Group Traceback (most recent call last):\n' + f' | File "{__file__}", line {self.callable_line}, in get_exception\n' + f' | exception_or_callable()\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' + f' | raise EG("eg2", [ValueError(3), TypeError(4)]) from e\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | ExceptionGroup: eg2\n' + f' +-+---------------- 1 ----------------\n' + f' | ValueError: 3\n' + f' +---------------- 2 ----------------\n' + f' | TypeError: 4\n' + f' +------------------------------------\n') + + report = self.get_report(exc) + self.assertEqual(report, expected) + + def test_exception_group_context_with_context(self): + def exc(): + EG = ExceptionGroup + try: + try: + raise EG("eg1", [ValueError(1), TypeError(2)]) + except: + raise EG("eg2", [ValueError(3), TypeError(4)]) + except: + raise ImportError(5) + + expected = ( + f' + Exception Group Traceback (most recent call last):\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 4}, in exc\n' + f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | ExceptionGroup: eg1\n' + f' +-+---------------- 1 ----------------\n' + f' | ValueError: 1\n' + f' +---------------- 2 ----------------\n' + f' | TypeError: 2\n' + f' +------------------------------------\n' + f'\n' + f'During handling of the above exception, another exception occurred:\n' + f'\n' + f' + Exception Group Traceback (most recent call last):\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 6}, in exc\n' + f' | raise EG("eg2", [ValueError(3), TypeError(4)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | ExceptionGroup: eg2\n' + f' +-+---------------- 1 ----------------\n' + f' | ValueError: 3\n' + f' +---------------- 2 ----------------\n' + f' | TypeError: 4\n' + f' +------------------------------------\n' + f'\n' + f'During handling of the above exception, another exception occurred:\n' + f'\n' + f'Traceback (most recent call last):\n' + f' File "{__file__}", line {self.callable_line}, in get_exception\n' + f' exception_or_callable()\n' + f' ^^^^^^^^^^^^^^^^^^^^^^^\n' + f' File "{__file__}", line {exc.__code__.co_firstlineno + 8}, in exc\n' + f' raise ImportError(5)\n' + f' ^^^^^^^^^^^^^^^^^^^^\n' + f'ImportError: 5\n') + + report = self.get_report(exc) + self.assertEqual(report, expected) + + def test_exception_group_nested(self): + def exc(): + EG = ExceptionGroup + VE = ValueError + TE = TypeError + try: + try: + raise EG("nested", [TE(2), TE(3)]) + except Exception as e: + exc = e + raise EG("eg", [VE(1), exc, VE(4)]) + except: + raise EG("top", [VE(5)]) + + expected = (f' + Exception Group Traceback (most recent call last):\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 9}, in exc\n' + f' | raise EG("eg", [VE(1), exc, VE(4)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | ExceptionGroup: eg\n' + f' +-+---------------- 1 ----------------\n' + f' | ValueError: 1\n' + f' +---------------- 2 ----------------\n' + f' | Exception Group Traceback (most recent call last):\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 6}, in exc\n' + f' | raise EG("nested", [TE(2), TE(3)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | ExceptionGroup: nested\n' + f' +-+---------------- 1 ----------------\n' + f' | TypeError: 2\n' + f' +---------------- 2 ----------------\n' + f' | TypeError: 3\n' + f' +------------------------------------\n' + f' +---------------- 3 ----------------\n' + f' | ValueError: 4\n' + f' +------------------------------------\n' + f'\n' + f'During handling of the above exception, another exception occurred:\n' + f'\n' + f' + Exception Group Traceback (most recent call last):\n' + f' | File "{__file__}", line {self.callable_line}, in get_exception\n' + f' | exception_or_callable()\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 11}, in exc\n' + f' | raise EG("top", [VE(5)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | ExceptionGroup: top\n' + f' +-+---------------- 1 ----------------\n' + f' | ValueError: 5\n' + f' +------------------------------------\n') + + report = self.get_report(exc) + self.assertEqual(report, expected) + + def test_exception_group_width_limit(self): + excs = [] + for i in range(1000): + excs.append(ValueError(i)) + eg = ExceptionGroup('eg', excs) + + expected = (' | ExceptionGroup: eg\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 0\n' + ' +---------------- 2 ----------------\n' + ' | ValueError: 1\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: 2\n' + ' +---------------- 4 ----------------\n' + ' | ValueError: 3\n' + ' +---------------- 5 ----------------\n' + ' | ValueError: 4\n' + ' +---------------- 6 ----------------\n' + ' | ValueError: 5\n' + ' +---------------- 7 ----------------\n' + ' | ValueError: 6\n' + ' +---------------- 8 ----------------\n' + ' | ValueError: 7\n' + ' +---------------- 9 ----------------\n' + ' | ValueError: 8\n' + ' +---------------- 10 ----------------\n' + ' | ValueError: 9\n' + ' +---------------- 11 ----------------\n' + ' | ValueError: 10\n' + ' +---------------- 12 ----------------\n' + ' | ValueError: 11\n' + ' +---------------- 13 ----------------\n' + ' | ValueError: 12\n' + ' +---------------- 14 ----------------\n' + ' | ValueError: 13\n' + ' +---------------- 15 ----------------\n' + ' | ValueError: 14\n' + ' +---------------- ... ----------------\n' + ' | and 985 more exceptions\n' + ' +------------------------------------\n') + + report = self.get_report(eg) + self.assertEqual(report, expected) + + def test_exception_group_depth_limit(self): + exc = TypeError('bad type') + for i in range(1000): + exc = ExceptionGroup( + f'eg{i}', + [ValueError(i), exc, ValueError(-i)]) + + expected = (' | ExceptionGroup: eg999\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 999\n' + ' +---------------- 2 ----------------\n' + ' | ExceptionGroup: eg998\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 998\n' + ' +---------------- 2 ----------------\n' + ' | ExceptionGroup: eg997\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 997\n' + ' +---------------- 2 ----------------\n' + ' | ExceptionGroup: eg996\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 996\n' + ' +---------------- 2 ----------------\n' + ' | ExceptionGroup: eg995\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 995\n' + ' +---------------- 2 ----------------\n' + ' | ExceptionGroup: eg994\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 994\n' + ' +---------------- 2 ----------------\n' + ' | ExceptionGroup: eg993\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 993\n' + ' +---------------- 2 ----------------\n' + ' | ExceptionGroup: eg992\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 992\n' + ' +---------------- 2 ----------------\n' + ' | ExceptionGroup: eg991\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 991\n' + ' +---------------- 2 ----------------\n' + ' | ExceptionGroup: eg990\n' + ' +-+---------------- 1 ----------------\n' + ' | ValueError: 990\n' + ' +---------------- 2 ----------------\n' + ' | ... (max_group_depth is 10)\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: -990\n' + ' +------------------------------------\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: -991\n' + ' +------------------------------------\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: -992\n' + ' +------------------------------------\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: -993\n' + ' +------------------------------------\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: -994\n' + ' +------------------------------------\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: -995\n' + ' +------------------------------------\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: -996\n' + ' +------------------------------------\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: -997\n' + ' +------------------------------------\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: -998\n' + ' +------------------------------------\n' + ' +---------------- 3 ----------------\n' + ' | ValueError: -999\n' + ' +------------------------------------\n') + + report = self.get_report(exc) + self.assertEqual(report, expected) + + class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): # # This checks reporting through the 'traceback' module, with both @@ -1913,6 +2235,197 @@ class TestTracebackException(unittest.TestCase): '']) +class TestTracebackException_ExceptionGroups(unittest.TestCase): + def setUp(self): + super().setUp() + self.eg_info = self._get_exception_group() + + def _get_exception_group(self): + def f(): + 1/0 + + def g(v): + raise ValueError(v) + + self.lno_f = f.__code__.co_firstlineno + self.lno_g = g.__code__.co_firstlineno + + try: + try: + try: + f() + except Exception as e: + exc1 = e + try: + g(42) + except Exception as e: + exc2 = e + raise ExceptionGroup("eg1", [exc1, exc2]) + except ExceptionGroup as e: + exc3 = e + try: + g(24) + except Exception as e: + exc4 = e + raise ExceptionGroup("eg2", [exc3, exc4]) + except ExceptionGroup: + return sys.exc_info() + self.fail('Exception Not Raised') + + def test_exception_group_construction(self): + eg_info = self.eg_info + teg1 = traceback.TracebackException(*eg_info) + teg2 = traceback.TracebackException.from_exception(eg_info[1]) + self.assertIsNot(teg1, teg2) + self.assertEqual(teg1, teg2) + + def test_exception_group_format_exception_only(self): + teg = traceback.TracebackException(*self.eg_info) + formatted = ''.join(teg.format_exception_only()).split('\n') + expected = "ExceptionGroup: eg2\n".split('\n') + + self.assertEqual(formatted, expected) + + def test_exception_group_format(self): + teg = traceback.TracebackException(*self.eg_info) + + formatted = ''.join(teg.format()).split('\n') + lno_f = self.lno_f + lno_g = self.lno_g + + expected = [ + f' + Exception Group Traceback (most recent call last):', + f' | File "{__file__}", line {lno_g+23}, in _get_exception_group', + f' | raise ExceptionGroup("eg2", [exc3, exc4])', + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^', + f' | ExceptionGroup: eg2', + f' +-+---------------- 1 ----------------', + f' | Exception Group Traceback (most recent call last):', + f' | File "{__file__}", line {lno_g+16}, in _get_exception_group', + f' | raise ExceptionGroup("eg1", [exc1, exc2])', + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^', + f' | ExceptionGroup: eg1', + f' +-+---------------- 1 ----------------', + f' | Traceback (most recent call last):', + f' | File "{__file__}", line {lno_g+9}, in _get_exception_group', + f' | f()', + f' | ^^^', + f' | File "{__file__}", line {lno_f+1}, in f', + f' | 1/0', + f' | ~^~', + f' | ZeroDivisionError: division by zero', + f' +---------------- 2 ----------------', + f' | Traceback (most recent call last):', + f' | File "{__file__}", line {lno_g+13}, in _get_exception_group', + f' | g(42)', + f' | ^^^^^', + f' | File "{__file__}", line {lno_g+1}, in g', + f' | raise ValueError(v)', + f' | ^^^^^^^^^^^^^^^^^^^', + f' | ValueError: 42', + f' +------------------------------------', + f' +---------------- 2 ----------------', + f' | Traceback (most recent call last):', + f' | File "{__file__}", line {lno_g+20}, in _get_exception_group', + f' | g(24)', + f' | ^^^^^', + f' | File "{__file__}", line {lno_g+1}, in g', + f' | raise ValueError(v)', + f' | ^^^^^^^^^^^^^^^^^^^', + f' | ValueError: 24', + f' +------------------------------------', + f''] + + self.assertEqual(formatted, expected) + + def test_max_group_width(self): + excs1 = [] + excs2 = [] + for i in range(3): + excs1.append(ValueError(i)) + for i in range(10): + excs2.append(TypeError(i)) + + EG = ExceptionGroup + eg = EG('eg', [EG('eg1', excs1), EG('eg2', excs2)]) + + teg = traceback.TracebackException.from_exception(eg, max_group_width=2) + formatted = ''.join(teg.format()).split('\n') + + expected = [ + f' | ExceptionGroup: eg', + f' +-+---------------- 1 ----------------', + f' | ExceptionGroup: eg1', + f' +-+---------------- 1 ----------------', + f' | ValueError: 0', + f' +---------------- 2 ----------------', + f' | ValueError: 1', + f' +---------------- ... ----------------', + f' | and 1 more exception', + f' +------------------------------------', + f' +---------------- 2 ----------------', + f' | ExceptionGroup: eg2', + f' +-+---------------- 1 ----------------', + f' | TypeError: 0', + f' +---------------- 2 ----------------', + f' | TypeError: 1', + f' +---------------- ... ----------------', + f' | and 8 more exceptions', + f' +------------------------------------', + f''] + + self.assertEqual(formatted, expected) + + def test_max_group_depth(self): + exc = TypeError('bad type') + for i in range(3): + exc = ExceptionGroup('exc', [ValueError(-i), exc, ValueError(i)]) + + teg = traceback.TracebackException.from_exception(exc, max_group_depth=2) + formatted = ''.join(teg.format()).split('\n') + + expected = [ + f' | ExceptionGroup: exc', + f' +-+---------------- 1 ----------------', + f' | ValueError: -2', + f' +---------------- 2 ----------------', + f' | ExceptionGroup: exc', + f' +-+---------------- 1 ----------------', + f' | ValueError: -1', + f' +---------------- 2 ----------------', + f' | ... (max_group_depth is 2)', + f' +---------------- 3 ----------------', + f' | ValueError: 1', + f' +------------------------------------', + f' +---------------- 3 ----------------', + f' | ValueError: 2', + f' +------------------------------------', + f''] + + self.assertEqual(formatted, expected) + + def test_comparison(self): + try: + raise self.eg_info[1] + except ExceptionGroup: + exc_info = sys.exc_info() + for _ in range(5): + try: + raise exc_info[1] + except: + exc_info = sys.exc_info() + exc = traceback.TracebackException(*exc_info) + exc2 = traceback.TracebackException(*exc_info) + exc3 = traceback.TracebackException(*exc_info, limit=300) + ne = traceback.TracebackException(*exc_info, limit=3) + self.assertIsNot(exc, exc2) + self.assertEqual(exc, exc2) + self.assertEqual(exc, exc3) + self.assertNotEqual(exc, ne) + self.assertNotEqual(exc, object()) + self.assertEqual(exc, ALWAYS_EQ) + + class MiscTest(unittest.TestCase): def test_all(self): diff --git a/Lib/traceback.py b/Lib/traceback.py index 568f3ff28c2..97caa1372f4 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -4,6 +4,7 @@ import collections import itertools import linecache import sys +import textwrap from contextlib import suppress __all__ = ['extract_stack', 'extract_tb', 'format_exception', @@ -601,6 +602,29 @@ def _extract_caret_anchors_from_line_segment(segment): return None +class _ExceptionPrintContext: + def __init__(self): + self.seen = set() + self.exception_group_depth = 0 + self.need_close = False + + def indent(self): + return ' ' * (2 * self.exception_group_depth) + + def emit(self, text_gen, margin_char=None): + if margin_char is None: + margin_char = '|' + indent_str = self.indent() + if self.exception_group_depth: + indent_str += margin_char + ' ' + + if isinstance(text_gen, str): + yield textwrap.indent(text_gen, indent_str, lambda line: True) + else: + for text in text_gen: + yield textwrap.indent(text, indent_str, lambda line: True) + + class TracebackException: """An exception ready for rendering. @@ -608,6 +632,11 @@ class TracebackException: to this intermediary form to ensure that no references are held, while still being able to fully print or format it. + max_group_width and max_group_depth control the formatting of exception + groups. The depth refers to the nesting level of the group, and the width + refers to the size of a single exception group's exceptions array. The + formatted output is truncated when either limit is exceeded. + Use `from_exception` to create TracebackException instances from exception objects, or the constructor to create TracebackException instances from individual components. @@ -635,7 +664,7 @@ class TracebackException: def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, compact=False, - _seen=None): + max_group_width=15, max_group_depth=10, _seen=None): # NB: we need to accept exc_traceback, exc_value, exc_traceback to # permit backwards compat with the existing API, otherwise we # need stub thunk objects just to glue it together. @@ -645,7 +674,9 @@ class TracebackException: _seen = set() _seen.add(id(exc_value)) - # TODO: locals. + self.max_group_width = max_group_width + self.max_group_depth = max_group_depth + self.stack = StackSummary._extract_from_extended_frame_gen( _walk_tb_with_full_positions(exc_traceback), limit=limit, lookup_lines=lookup_lines, @@ -685,6 +716,8 @@ class TracebackException: limit=limit, lookup_lines=lookup_lines, capture_locals=capture_locals, + max_group_width=max_group_width, + max_group_depth=max_group_depth, _seen=_seen) else: cause = None @@ -704,15 +737,38 @@ class TracebackException: limit=limit, lookup_lines=lookup_lines, capture_locals=capture_locals, + max_group_width=max_group_width, + max_group_depth=max_group_depth, _seen=_seen) else: context = None + + if e and isinstance(e, BaseExceptionGroup): + exceptions = [] + for exc in e.exceptions: + texc = TracebackException( + type(exc), + exc, + exc.__traceback__, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + max_group_width=max_group_width, + max_group_depth=max_group_depth, + _seen=_seen) + exceptions.append(texc) + else: + exceptions = None + te.__cause__ = cause te.__context__ = context + te.exceptions = exceptions if cause: queue.append((te.__cause__, e.__cause__)) if context: queue.append((te.__context__, e.__context__)) + if exceptions: + queue.extend(zip(te.exceptions, e.exceptions)) @classmethod def from_exception(cls, exc, *args, **kwargs): @@ -795,7 +851,7 @@ class TracebackException: msg = self.msg or "" yield "{}: {}{}\n".format(stype, msg, filename_suffix) - def format(self, *, chain=True): + def format(self, *, chain=True, _ctx=None): """Format the exception. If chain is not *True*, *__cause__* and *__context__* will not be formatted. @@ -808,10 +864,13 @@ class TracebackException: string in the output. """ + if _ctx is None: + _ctx = _ExceptionPrintContext() + output = [] exc = self - while exc: - if chain: + if chain: + while exc: if exc.__cause__ is not None: chained_msg = _cause_message chained_exc = exc.__cause__ @@ -825,17 +884,73 @@ class TracebackException: output.append((chained_msg, exc)) exc = chained_exc - else: - output.append((None, exc)) - exc = None + else: + output.append((None, exc)) 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() + yield from _ctx.emit(msg) + if exc.exceptions is None: + if exc.stack: + yield from _ctx.emit('Traceback (most recent call last):\n') + yield from _ctx.emit(exc.stack.format()) + yield from _ctx.emit(exc.format_exception_only()) + elif _ctx.exception_group_depth > self.max_group_depth: + # exception group, but depth exceeds limit + yield from _ctx.emit( + f"... (max_group_depth is {self.max_group_depth})\n") + else: + # format exception group + is_toplevel = (_ctx.exception_group_depth == 0) + if is_toplevel: + _ctx.exception_group_depth += 1 + + if exc.stack: + yield from _ctx.emit( + 'Exception Group Traceback (most recent call last):\n', + margin_char = '+' if is_toplevel else None) + yield from _ctx.emit(exc.stack.format()) + + yield from _ctx.emit(exc.format_exception_only()) + num_excs = len(exc.exceptions) + if num_excs <= self.max_group_width: + n = num_excs + else: + n = self.max_group_width + 1 + _ctx.need_close = False + for i in range(n): + last_exc = (i == n-1) + if last_exc: + # The closing frame may be added by a recursive call + _ctx.need_close = True + + if self.max_group_width is not None: + truncated = (i >= self.max_group_width) + else: + truncated = False + title = f'{i+1}' if not truncated else '...' + yield (_ctx.indent() + + ('+-' if i==0 else ' ') + + f'+---------------- {title} ----------------\n') + _ctx.exception_group_depth += 1 + if not truncated: + yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx) + else: + remaining = num_excs - self.max_group_width + plural = 's' if remaining > 1 else '' + yield from _ctx.emit( + f"and {remaining} more exception{plural}\n") + + if last_exc and _ctx.need_close: + yield (_ctx.indent() + + "+------------------------------------\n") + _ctx.need_close = False + _ctx.exception_group_depth -= 1 + + if is_toplevel: + assert _ctx.exception_group_depth == 1 + _ctx.exception_group_depth = 0 + def print(self, *, file=None, chain=True): """Print the result of self.format(chain=chain) to 'file'.""" diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst b/Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst index ee48b6d5105..55ca14f2259 100644 --- a/Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst +++ b/Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst @@ -1 +1 @@ -Implement :pep:`654` Add :class:`ExceptionGroup` and :class:`BaseExceptionGroup`. +Implement :pep:`654`. Add :class:`ExceptionGroup` and :class:`BaseExceptionGroup`. Update traceback display code. diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 6cecef97932..2c0950ee17e 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -8,6 +8,8 @@ /* TODO: Cull includes following phase split */ +#include + #include "Python.h" #include "pycore_ast.h" // PyAST_mod2obj @@ -19,6 +21,7 @@ #include "pycore_pylifecycle.h" // _Py_UnhandledKeyboardInterrupt #include "pycore_pystate.h" // _PyInterpreterState_GET() #include "pycore_sysmodule.h" // _PySys_Audit() +#include "pycore_traceback.h" // _PyTraceBack_Print_Indented() #include "token.h" // INDENT #include "errcode.h" // E_EOF @@ -886,19 +889,50 @@ PyErr_Print(void) PyErr_PrintEx(1); } +struct exception_print_context +{ + PyObject *file; + PyObject *seen; // Prevent cycles in recursion + int exception_group_depth; // nesting level of current exception group + bool need_close; // Need a closing bottom frame + int max_group_width; // Maximum number of children of each EG + int max_group_depth; // Maximum nesting level of EGs +}; + +#define EXC_MARGIN(ctx) ((ctx)->exception_group_depth ? "| " : "") +#define EXC_INDENT(ctx) (2 * (ctx)->exception_group_depth) + +static int +write_indented_margin(struct exception_print_context *ctx, PyObject *f) +{ + return _Py_WriteIndentedMargin(EXC_INDENT(ctx), EXC_MARGIN(ctx), f); +} + static void -print_exception(PyObject *f, PyObject *value) +print_exception(struct exception_print_context *ctx, PyObject *value) { int err = 0; PyObject *type, *tb, *tmp; + PyObject *f = ctx->file; + _Py_IDENTIFIER(print_file_and_line); if (!PyExceptionInstance_Check(value)) { - err = PyFile_WriteString("TypeError: print_exception(): Exception expected for value, ", f); - err += PyFile_WriteString(Py_TYPE(value)->tp_name, f); - err += PyFile_WriteString(" found\n", f); - if (err) + if (err == 0) { + err = _Py_WriteIndent(EXC_INDENT(ctx), f); + } + if (err == 0) { + err = PyFile_WriteString("TypeError: print_exception(): Exception expected for value, ", f); + } + if (err == 0) { + err = PyFile_WriteString(Py_TYPE(value)->tp_name, f); + } + if (err == 0) { + err = PyFile_WriteString(" found\n", f); + } + if (err != 0) { PyErr_Clear(); + } return; } @@ -906,8 +940,18 @@ print_exception(PyObject *f, PyObject *value) fflush(stdout); type = (PyObject *) Py_TYPE(value); tb = PyException_GetTraceback(value); - if (tb && tb != Py_None) - err = PyTraceBack_Print(tb, f); + if (tb && tb != Py_None) { + const char *header = EXCEPTION_TB_HEADER; + const char *header_margin = EXC_MARGIN(ctx); + if (_PyBaseExceptionGroup_Check(value)) { + header = EXCEPTION_GROUP_TB_HEADER; + if (ctx->exception_group_depth == 1) { + header_margin = "+ "; + } + } + err = _PyTraceBack_Print_Indented( + tb, EXC_INDENT(ctx), EXC_MARGIN(ctx), header_margin, header, f); + } if (err == 0 && (err = _PyObject_LookupAttrId(value, &PyId_print_file_and_line, &tmp)) > 0) { @@ -917,8 +961,9 @@ print_exception(PyObject *f, PyObject *value) Py_DECREF(tmp); if (!parse_syntax_error(value, &message, &filename, &lineno, &offset, - &end_lineno, &end_offset, &text)) + &end_lineno, &end_offset, &text)) { PyErr_Clear(); + } else { PyObject *line; @@ -929,7 +974,10 @@ print_exception(PyObject *f, PyObject *value) filename, lineno); Py_DECREF(filename); if (line != NULL) { - PyFile_WriteObject(line, f, Py_PRINT_RAW); + err = write_indented_margin(ctx, f); + if (err == 0) { + err = PyFile_WriteObject(line, f, Py_PRINT_RAW); + } Py_DECREF(line); } @@ -958,7 +1006,7 @@ print_exception(PyObject *f, PyObject *value) err = -1; } } - if (err) { + if (err != 0) { /* Don't do anything else */ } else { @@ -967,21 +1015,26 @@ print_exception(PyObject *f, PyObject *value) _Py_IDENTIFIER(__module__); assert(PyExceptionClass_Check(type)); - modulename = _PyObject_GetAttrId(type, &PyId___module__); - if (modulename == NULL || !PyUnicode_Check(modulename)) - { - Py_XDECREF(modulename); - PyErr_Clear(); - err = PyFile_WriteString("", f); - } - else { - if (!_PyUnicode_EqualToASCIIId(modulename, &PyId_builtins) && - !_PyUnicode_EqualToASCIIId(modulename, &PyId___main__)) + err = write_indented_margin(ctx, f); + if (err == 0) { + modulename = _PyObject_GetAttrId(type, &PyId___module__); + if (modulename == NULL || !PyUnicode_Check(modulename)) { - err = PyFile_WriteObject(modulename, f, Py_PRINT_RAW); - err += PyFile_WriteString(".", f); + Py_XDECREF(modulename); + PyErr_Clear(); + err = PyFile_WriteString("", f); + } + else { + if (!_PyUnicode_EqualToASCIIId(modulename, &PyId_builtins) && + !_PyUnicode_EqualToASCIIId(modulename, &PyId___main__)) + { + err = PyFile_WriteObject(modulename, f, Py_PRINT_RAW); + if (err == 0) { + err = PyFile_WriteString(".", f); + } + } + Py_DECREF(modulename); } - Py_DECREF(modulename); } if (err == 0) { PyObject* qualname = PyType_GetQualName((PyTypeObject *)type); @@ -1039,26 +1092,67 @@ print_exception(PyObject *f, PyObject *value) } static const char cause_message[] = - "\nThe above exception was the direct cause " - "of the following exception:\n\n"; + "The above exception was the direct cause " + "of the following exception:\n"; static const char context_message[] = - "\nDuring handling of the above exception, " - "another exception occurred:\n\n"; + "During handling of the above exception, " + "another exception occurred:\n"; static void -print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen) +print_exception_recursive(struct exception_print_context*, PyObject*); + +static int +print_chained(struct exception_print_context* ctx, PyObject *value, + const char * message, const char *tag) +{ + PyObject *f = ctx->file; + bool need_close = ctx->need_close; + + int err = Py_EnterRecursiveCall(" in print_chained"); + if (err == 0) { + print_exception_recursive(ctx, value); + Py_LeaveRecursiveCall(); + + if (err == 0) { + err = write_indented_margin(ctx, f); + } + if (err == 0) { + err = PyFile_WriteString("\n", f); + } + if (err == 0) { + err = write_indented_margin(ctx, f); + } + if (err == 0) { + err = PyFile_WriteString(message, f); + } + if (err == 0) { + err = write_indented_margin(ctx, f); + } + if (err == 0) { + err = PyFile_WriteString("\n", f); + } + } + + ctx->need_close = need_close; + + return err; +} + +static void +print_exception_recursive(struct exception_print_context* ctx, PyObject *value) { int err = 0, res; PyObject *cause, *context; - if (seen != NULL) { + if (ctx->seen != NULL) { /* Exception chaining */ PyObject *value_id = PyLong_FromVoidPtr(value); - if (value_id == NULL || PySet_Add(seen, value_id) == -1) + if (value_id == NULL || PySet_Add(ctx->seen, value_id) == -1) PyErr_Clear(); else if (PyExceptionInstance_Check(value)) { PyObject *check_id = NULL; + cause = PyException_GetCause(value); context = PyException_GetContext(value); if (cause) { @@ -1066,16 +1160,13 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen) if (check_id == NULL) { res = -1; } else { - res = PySet_Contains(seen, check_id); + res = PySet_Contains(ctx->seen, check_id); Py_DECREF(check_id); } if (res == -1) PyErr_Clear(); if (res == 0) { - print_exception_recursive( - f, cause, seen); - err |= PyFile_WriteString( - cause_message, f); + err = print_chained(ctx, cause, cause_message, "cause"); } } else if (context && @@ -1084,16 +1175,13 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen) if (check_id == NULL) { res = -1; } else { - res = PySet_Contains(seen, check_id); + res = PySet_Contains(ctx->seen, check_id); Py_DECREF(check_id); } if (res == -1) PyErr_Clear(); if (res == 0) { - print_exception_recursive( - f, context, seen); - err |= PyFile_WriteString( - context_message, f); + err = print_chained(ctx, context, context_message, "context"); } } Py_XDECREF(context); @@ -1101,17 +1189,146 @@ print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen) } Py_XDECREF(value_id); } - print_exception(f, value); + if (err) { + /* don't do anything else */ + } + else if (!_PyBaseExceptionGroup_Check(value)) { + print_exception(ctx, value); + } + else if (ctx->exception_group_depth > ctx->max_group_depth) { + /* exception group but depth exceeds limit */ + + PyObject *line = PyUnicode_FromFormat( + "... (max_group_depth is %d)\n", ctx->max_group_depth); + + if (line) { + PyObject *f = ctx->file; + if (err == 0) { + err = write_indented_margin(ctx, f); + } + if (err == 0) { + err = PyFile_WriteObject(line, f, Py_PRINT_RAW); + } + Py_DECREF(line); + } + else { + err = -1; + } + } + else { + /* format exception group */ + + if (ctx->exception_group_depth == 0) { + ctx->exception_group_depth += 1; + } + print_exception(ctx, value); + + PyObject *excs = ((PyBaseExceptionGroupObject *)value)->excs; + assert(excs && PyTuple_Check(excs)); + Py_ssize_t num_excs = PyTuple_GET_SIZE(excs); + assert(num_excs > 0); + Py_ssize_t n; + if (num_excs <= ctx->max_group_width) { + n = num_excs; + } + else { + n = ctx->max_group_width + 1; + } + + PyObject *f = ctx->file; + + ctx->need_close = false; + for (Py_ssize_t i = 0; i < n; i++) { + int last_exc = (i == n - 1); + if (last_exc) { + // The closing frame may be added in a recursive call + ctx->need_close = true; + } + PyObject *line; + bool truncated = (i >= ctx->max_group_width); + if (!truncated) { + line = PyUnicode_FromFormat( + "%s+---------------- %zd ----------------\n", + (i == 0) ? "+-" : " ", i + 1); + } + else { + line = PyUnicode_FromFormat( + "%s+---------------- ... ----------------\n", + (i == 0) ? "+-" : " "); + } + + if (line) { + if (err == 0) { + err = _Py_WriteIndent(EXC_INDENT(ctx), f); + } + if (err == 0) { + err = PyFile_WriteObject(line, f, Py_PRINT_RAW); + } + Py_DECREF(line); + } + else { + err = -1; + } + + if (err == 0) { + ctx->exception_group_depth += 1; + PyObject *exc = PyTuple_GET_ITEM(excs, i); + + if (!truncated) { + if (!Py_EnterRecursiveCall(" in print_exception_recursive")) { + print_exception_recursive(ctx, exc); + Py_LeaveRecursiveCall(); + } + else { + err = -1; + } + } + else { + Py_ssize_t excs_remaining = num_excs - ctx->max_group_width; + PyObject *line = PyUnicode_FromFormat( + "and %zd more exception%s\n", + excs_remaining, excs_remaining > 1 ? "s" : ""); + + if (line) { + if (err == 0) { + err = write_indented_margin(ctx, f); + } + if (err == 0) { + err = PyFile_WriteObject(line, f, Py_PRINT_RAW); + } + Py_DECREF(line); + } + else { + err = -1; + } + } + + if (err == 0 && last_exc && ctx->need_close) { + err = _Py_WriteIndent(EXC_INDENT(ctx), f); + if (err == 0) { + err = PyFile_WriteString( + "+------------------------------------\n", f); + } + ctx->need_close = false; + } + ctx->exception_group_depth -= 1; + } + } + if (ctx->exception_group_depth == 1) { + ctx->exception_group_depth -= 1; + } + } if (err != 0) PyErr_Clear(); } +#define PyErr_MAX_GROUP_WIDTH 15 +#define PyErr_MAX_GROUP_DEPTH 10 + void _PyErr_Display(PyObject *file, PyObject *exception, PyObject *value, PyObject *tb) { assert(file != NULL && file != Py_None); - - PyObject *seen; if (PyExceptionInstance_Check(value) && tb != NULL && PyTraceBack_Check(tb)) { /* Put the traceback on the exception, otherwise it won't get @@ -1123,15 +1340,21 @@ _PyErr_Display(PyObject *file, PyObject *exception, PyObject *value, PyObject *t Py_DECREF(cur_tb); } + struct exception_print_context ctx; + ctx.file = file; + ctx.exception_group_depth = 0; + ctx.max_group_width = PyErr_MAX_GROUP_WIDTH; + ctx.max_group_depth = PyErr_MAX_GROUP_DEPTH; + /* We choose to ignore seen being possibly NULL, and report at least the main exception (it could be a MemoryError). */ - seen = PySet_New(NULL); - if (seen == NULL) { + ctx.seen = PySet_New(NULL); + if (ctx.seen == NULL) { PyErr_Clear(); } - print_exception_recursive(file, value, seen); - Py_XDECREF(seen); + print_exception_recursive(&ctx, value); + Py_XDECREF(ctx.seen); /* Call file.flush() */ PyObject *res = _PyObject_CallMethodIdNoArgs(file, &PyId_flush); diff --git a/Python/traceback.c b/Python/traceback.c index 22a0922c255..67f995a7599 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -14,6 +14,7 @@ #include "pycore_pyarena.h" // _PyArena_Free() #include "pycore_pyerrors.h" // _PyErr_Fetch() #include "pycore_pystate.h" // _PyThreadState_GET() +#include "pycore_traceback.h" // EXCEPTION_TB_HEADER #include "../Parser/pegen.h" // _PyPegen_byte_offset_to_character_offset() #include "structmember.h" // PyMemberDef #include "osdefs.h" // SEP @@ -379,8 +380,44 @@ finally: return result; } +/* Writes indent spaces. Returns 0 on success and non-zero on failure. + */ int -_Py_DisplaySourceLine(PyObject *f, PyObject *filename, int lineno, int indent, int *truncation, PyObject **line) +_Py_WriteIndent(int indent, PyObject *f) +{ + int err = 0; + char buf[11] = " "; + assert(strlen(buf) == 10); + while (indent > 0) { + if (indent < 10) { + buf[indent] = '\0'; + } + err = PyFile_WriteString(buf, f); + if (err != 0) { + return err; + } + indent -= 10; + } + return 0; +} + +/* Writes indent spaces, followed by the margin if it is not `\0`. + Returns 0 on success and non-zero on failure. + */ +int +_Py_WriteIndentedMargin(int indent, const char *margin, PyObject *f) +{ + int err = _Py_WriteIndent(indent, f); + if (err == 0 && margin) { + err = PyFile_WriteString(margin, f); + } + return err; +} + +static int +display_source_line_with_margin(PyObject *f, PyObject *filename, int lineno, int indent, + int margin_indent, const char *margin, + int *truncation, PyObject **line) { int err = 0; int fd; @@ -508,27 +545,33 @@ _Py_DisplaySourceLine(PyObject *f, PyObject *filename, int lineno, int indent, i *truncation = i - indent; } + if (err == 0) { + err = _Py_WriteIndentedMargin(margin_indent, margin, f); + } /* Write some spaces before the line */ - strcpy(buf, " "); - assert (strlen(buf) == 10); - while (indent > 0) { - if (indent < 10) - buf[indent] = '\0'; - err = PyFile_WriteString(buf, f); - if (err != 0) - break; - indent -= 10; + if (err == 0) { + err = _Py_WriteIndent(indent, f); } /* finally display the line */ - if (err == 0) + if (err == 0) { err = PyFile_WriteObject(lineobj, f, Py_PRINT_RAW); + } Py_DECREF(lineobj); - if (err == 0) + if (err == 0) { err = PyFile_WriteString("\n", f); + } return err; } +int +_Py_DisplaySourceLine(PyObject *f, PyObject *filename, int lineno, int indent, + int *truncation, PyObject **line) +{ + return display_source_line_with_margin(f, filename, lineno, indent, 0, + NULL, truncation, line); +} + /* AST based Traceback Specialization * * When displaying a new traceback line, for certain syntactical constructs @@ -697,7 +740,7 @@ print_error_location_carets(PyObject *f, int offset, Py_ssize_t start_offset, Py static int tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int lineno, - PyFrameObject *frame, PyObject *name) + PyFrameObject *frame, PyObject *name, int margin_indent, const char *margin) { int err; PyObject *line; @@ -708,15 +751,20 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen filename, lineno, name); if (line == NULL) return -1; - err = PyFile_WriteObject(line, f, Py_PRINT_RAW); + err = _Py_WriteIndentedMargin(margin_indent, margin, f); + if (err == 0) { + err = PyFile_WriteObject(line, f, Py_PRINT_RAW); + } Py_DECREF(line); if (err != 0) return err; int truncation = _TRACEBACK_SOURCE_LINE_INDENT; PyObject* source_line = NULL; - if (_Py_DisplaySourceLine(f, filename, lineno, _TRACEBACK_SOURCE_LINE_INDENT, - &truncation, &source_line) != 0 || !source_line) { + int rc = display_source_line_with_margin( + f, filename, lineno, _TRACEBACK_SOURCE_LINE_INDENT, + margin_indent, margin, &truncation, &source_line); + if (rc != 0 || !source_line) { /* ignore errors since we can't report them, can we? */ err = ignore_source_errors(); goto done; @@ -801,9 +849,12 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen end_offset = i + 1; } - err = print_error_location_carets(f, truncation, start_offset, end_offset, - right_start_offset, left_end_offset, - primary_error_char, secondary_error_char); + err = _Py_WriteIndentedMargin(margin_indent, margin, f); + if (err == 0) { + err = print_error_location_carets(f, truncation, start_offset, end_offset, + right_start_offset, left_end_offset, + primary_error_char, secondary_error_char); + } done: Py_XDECREF(source_line); @@ -830,7 +881,8 @@ tb_print_line_repeated(PyObject *f, long cnt) } static int -tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit) +tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit, + int indent, const char *margin) { int err = 0; Py_ssize_t depth = 0; @@ -864,7 +916,7 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit) cnt++; if (err == 0 && cnt <= TB_RECURSIVE_CUTOFF) { err = tb_displayline(tb, f, code->co_filename, tb->tb_lineno, - tb->tb_frame, code->co_name); + tb->tb_frame, code->co_name, indent, margin); if (err == 0) { err = PyErr_CheckSignals(); } @@ -881,7 +933,8 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit) #define PyTraceBack_LIMIT 1000 int -PyTraceBack_Print(PyObject *v, PyObject *f) +_PyTraceBack_Print_Indented(PyObject *v, int indent, const char *margin, + const char *header_margin, const char *header, PyObject *f) { int err; PyObject *limitv; @@ -904,12 +957,27 @@ PyTraceBack_Print(PyObject *v, PyObject *f) return 0; } } - err = PyFile_WriteString("Traceback (most recent call last):\n", f); - if (!err) - err = tb_printinternal((PyTracebackObject *)v, f, limit); + err = _Py_WriteIndentedMargin(indent, header_margin, f); + if (err == 0) { + err = PyFile_WriteString(header, f); + } + if (err == 0) { + err = tb_printinternal((PyTracebackObject *)v, f, limit, indent, margin); + } return err; } +int +PyTraceBack_Print(PyObject *v, PyObject *f) +{ + int indent = 0; + const char *margin = NULL; + const char *header_margin = NULL; + const char *header = EXCEPTION_TB_HEADER; + + return _PyTraceBack_Print_Indented(v, indent, margin, header_margin, header, f); +} + /* Format an integer in range [0; 0xffffffff] to decimal and write it into the file fd.