[Python-checkins] bpo-45292: [PEP 654] Update traceback display code to work with exception groups (GH-29207)

iritkatriel webhook-mailer at python.org
Fri Nov 5 05:39:51 EDT 2021


https://github.com/python/cpython/commit/3509b26c916707363c71a1df040855e395cf4817
commit: 3509b26c916707363c71a1df040855e395cf4817
branch: main
author: Irit Katriel <1055913+iritkatriel at users.noreply.github.com>
committer: iritkatriel <1055913+iritkatriel at users.noreply.github.com>
date: 2021-11-05T09:39:18Z
summary:

bpo-45292: [PEP 654] Update traceback display code to work with exception groups (GH-29207)

files:
M Include/internal/pycore_traceback.h
M Lib/test/test_traceback.py
M Lib/traceback.py
M Misc/NEWS.d/next/Core and Builtins/2021-09-26-18-18-50.bpo-45292.aX5HVr.rst
M Python/pythonrun.c
M Python/traceback.c

diff --git a/Include/internal/pycore_traceback.h b/Include/internal/pycore_traceback.h
index c01a47639d5e3..84dbe27044fd3 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 1c7db9d3d4737..d88851ddda431 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -987,6 +987,35 @@ def __eq__(self, other):
             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 @@ def __eq__(self, other):
 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 @@ def get_exception(self, exception_or_callable):
         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 @@ def __str__(self):
         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 @@ def f():
              ''])
 
 
+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 568f3ff28c29b..97caa1372f478 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -4,6 +4,7 @@
 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 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
             _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 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
                         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 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
                         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 @@ def _format_syntax_error(self, stype):
         msg = self.msg or "<no detail available>"
         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 @@ def format(self, *, chain=True):
         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 @@ def format(self, *, chain=True):
 
                 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 ee48b6d5105c5..55ca14f2259f0 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 6cecef9793228..2c0950ee17e8a 100644
--- a/Python/pythonrun.c
+++ b/Python/pythonrun.c
@@ -8,6 +8,8 @@
 
 /* TODO: Cull includes following phase split */
 
+#include <stdbool.h>
+
 #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("<unknown>", 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("<unknown>", 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(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(PyObject *f, PyObject *value, PyObject *seen)
+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 22a0922c255b3..67f995a759980 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 @@ _Py_FindSourceFile(PyObject *filename, char* namebuf, size_t namelen, PyObject *
     return result;
 }
 
+/* Writes indent spaces. Returns 0 on success and non-zero on failure.
+ */
+int
+_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_DisplaySourceLine(PyObject *f, PyObject *filename, int lineno, int indent, int *truncation, PyObject **line)
+_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.
 



More information about the Python-checkins mailing list