]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-111388: Add `show_group` parameter to `traceback.format_exception_only` (#111390)
authorNikita Sobolev <mail@sobolevn.me>
Fri, 27 Oct 2023 10:11:26 +0000 (13:11 +0300)
committerGitHub <noreply@github.com>
Fri, 27 Oct 2023 10:11:26 +0000 (11:11 +0100)
Doc/library/traceback.rst
Lib/test/test_traceback.py
Lib/traceback.py
Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst [new file with mode: 0644]

index 67ee73d4b2e1e53949fdd99b3883ab630bab6ac0..408da7fc5f0645dad4d32c460218f8f8999b80b9 100644 (file)
@@ -135,7 +135,7 @@ The module defines the following functions:
    text line is not ``None``.
 
 
-.. function:: format_exception_only(exc, /[, value])
+.. function:: format_exception_only(exc, /[, value], *, show_group=False)
 
    Format the exception part of a traceback using an exception value such as
    given by ``sys.last_value``.  The return value is a list of strings, each
@@ -149,6 +149,10 @@ The module defines the following functions:
    can be passed as the first argument.  If *value* is provided, the first
    argument is ignored in order to provide backwards compatibility.
 
+   When *show_group* is ``True``, and the exception is an instance of
+   :exc:`BaseExceptionGroup`, the nested exceptions are included as
+   well, recursively, with indentation relative to their nesting depth.
+
    .. versionchanged:: 3.10
       The *etype* parameter has been renamed to *exc* and is now
       positional-only.
@@ -156,6 +160,9 @@ The module defines the following functions:
    .. versionchanged:: 3.11
       The returned list now includes any notes attached to the exception.
 
+   .. versionchanged:: 3.13
+      *show_group* parameter was added.
+
 
 .. function:: format_exception(exc, /[, value, tb], limit=None, chain=True)
 
index 0c5d7c9c8c50d38d7c3121b5c80fd8f3f4ceb144..b43dca6f640b9ae4b823496eb25ed712de15742a 100644 (file)
@@ -215,6 +215,155 @@ class TracebackCases(unittest.TestCase):
             str_name = '.'.join([X.__module__, X.__qualname__])
         self.assertEqual(err[0], "%s: %s\n" % (str_name, str_value))
 
+    def test_format_exception_group_without_show_group(self):
+        eg = ExceptionGroup('A', [ValueError('B')])
+        err = traceback.format_exception_only(eg)
+        self.assertEqual(err, ['ExceptionGroup: A (1 sub-exception)\n'])
+
+    def test_format_exception_group(self):
+        eg = ExceptionGroup('A', [ValueError('B')])
+        err = traceback.format_exception_only(eg, show_group=True)
+        self.assertEqual(err, [
+            'ExceptionGroup: A (1 sub-exception)\n',
+            '   ValueError: B\n',
+        ])
+
+    def test_format_base_exception_group(self):
+        eg = BaseExceptionGroup('A', [BaseException('B')])
+        err = traceback.format_exception_only(eg, show_group=True)
+        self.assertEqual(err, [
+            'BaseExceptionGroup: A (1 sub-exception)\n',
+            '   BaseException: B\n',
+        ])
+
+    def test_format_exception_group_with_note(self):
+        exc = ValueError('B')
+        exc.add_note('Note')
+        eg = ExceptionGroup('A', [exc])
+        err = traceback.format_exception_only(eg, show_group=True)
+        self.assertEqual(err, [
+            'ExceptionGroup: A (1 sub-exception)\n',
+            '   ValueError: B\n',
+            '   Note\n',
+        ])
+
+    def test_format_exception_group_explicit_class(self):
+        eg = ExceptionGroup('A', [ValueError('B')])
+        err = traceback.format_exception_only(ExceptionGroup, eg, show_group=True)
+        self.assertEqual(err, [
+            'ExceptionGroup: A (1 sub-exception)\n',
+            '   ValueError: B\n',
+        ])
+
+    def test_format_exception_group_multiple_exceptions(self):
+        eg = ExceptionGroup('A', [ValueError('B'), TypeError('C')])
+        err = traceback.format_exception_only(eg, show_group=True)
+        self.assertEqual(err, [
+            'ExceptionGroup: A (2 sub-exceptions)\n',
+            '   ValueError: B\n',
+            '   TypeError: C\n',
+        ])
+
+    def test_format_exception_group_multiline_messages(self):
+        eg = ExceptionGroup('A\n1', [ValueError('B\n2')])
+        err = traceback.format_exception_only(eg, show_group=True)
+        self.assertEqual(err, [
+            'ExceptionGroup: A\n1 (1 sub-exception)\n',
+            '   ValueError: B\n',
+            '   2\n',
+        ])
+
+    def test_format_exception_group_multiline2_messages(self):
+        exc = ValueError('B\n\n2\n')
+        exc.add_note('\nC\n\n3')
+        eg = ExceptionGroup('A\n\n1\n', [exc, IndexError('D')])
+        err = traceback.format_exception_only(eg, show_group=True)
+        self.assertEqual(err, [
+            'ExceptionGroup: A\n\n1\n (2 sub-exceptions)\n',
+            '   ValueError: B\n',
+            '   \n',
+            '   2\n',
+            '   \n',
+            '   \n',  # first char of `note`
+            '   C\n',
+            '   \n',
+            '   3\n', # note ends
+            '   IndexError: D\n',
+        ])
+
+    def test_format_exception_group_syntax_error(self):
+        exc = SyntaxError("error", ("x.py", 23, None, "bad syntax"))
+        eg = ExceptionGroup('A\n1', [exc])
+        err = traceback.format_exception_only(eg, show_group=True)
+        self.assertEqual(err, [
+            'ExceptionGroup: A\n1 (1 sub-exception)\n',
+            '     File "x.py", line 23\n',
+            '       bad syntax\n',
+            '   SyntaxError: error\n',
+        ])
+
+    def test_format_exception_group_nested_with_notes(self):
+        exc = IndexError('D')
+        exc.add_note('Note\nmultiline')
+        eg = ExceptionGroup('A', [
+            ValueError('B'),
+            ExceptionGroup('C', [exc, LookupError('E')]),
+            TypeError('F'),
+        ])
+        err = traceback.format_exception_only(eg, show_group=True)
+        self.assertEqual(err, [
+            'ExceptionGroup: A (3 sub-exceptions)\n',
+            '   ValueError: B\n',
+            '   ExceptionGroup: C (2 sub-exceptions)\n',
+            '      IndexError: D\n',
+            '      Note\n',
+            '      multiline\n',
+            '      LookupError: E\n',
+            '   TypeError: F\n',
+        ])
+
+    def test_format_exception_group_with_tracebacks(self):
+        def f():
+            try:
+                1 / 0
+            except ZeroDivisionError as e:
+                return e
+
+        def g():
+            try:
+                raise TypeError('g')
+            except TypeError as e:
+                return e
+
+        eg = ExceptionGroup('A', [
+            f(),
+            ExceptionGroup('B', [g()]),
+        ])
+        err = traceback.format_exception_only(eg, show_group=True)
+        self.assertEqual(err, [
+            'ExceptionGroup: A (2 sub-exceptions)\n',
+            '   ZeroDivisionError: division by zero\n',
+            '   ExceptionGroup: B (1 sub-exception)\n',
+            '      TypeError: g\n',
+        ])
+
+    def test_format_exception_group_with_cause(self):
+        def f():
+            try:
+                try:
+                    1 / 0
+                except ZeroDivisionError:
+                    raise ValueError(0)
+            except Exception as e:
+                return e
+
+        eg = ExceptionGroup('A', [f()])
+        err = traceback.format_exception_only(eg, show_group=True)
+        self.assertEqual(err, [
+            'ExceptionGroup: A (1 sub-exception)\n',
+            '   ValueError: 0\n',
+        ])
+
     @requires_subprocess()
     def test_encoded_file(self):
         # Test that tracebacks are correctly printed for encoded source files:
@@ -381,7 +530,7 @@ class TracebackCases(unittest.TestCase):
 
         self.assertEqual(
             str(inspect.signature(traceback.format_exception_only)),
-            '(exc, /, value=<implicit>)')
+            '(exc, /, value=<implicit>, *, show_group=False)')
 
 
 class PurePythonExceptionFormattingMixin:
index 0d41c3432eda2c5a62196e428b0a7c1b6f7ff3df..b25a7291f6be510f03c77764c6cc9fa89597af85 100644 (file)
@@ -148,7 +148,7 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
     return list(te.format(chain=chain))
 
 
-def format_exception_only(exc, /, value=_sentinel):
+def format_exception_only(exc, /, value=_sentinel, *, show_group=False):
     """Format the exception part of a traceback.
 
     The return value is a list of strings, each ending in a newline.
@@ -158,21 +158,26 @@ def format_exception_only(exc, /, value=_sentinel):
     contains several lines that (when printed) display detailed information
     about where the syntax error occurred. Following the message, the list
     contains the exception's ``__notes__``.
+
+    When *show_group* is ``True``, and the exception is an instance of
+    :exc:`BaseExceptionGroup`, the nested exceptions are included as
+    well, recursively, with indentation relative to their nesting depth.
     """
     if value is _sentinel:
         value = exc
     te = TracebackException(type(value), value, None, compact=True)
-    return list(te.format_exception_only())
+    return list(te.format_exception_only(show_group=show_group))
 
 
 # -- not official API but folk probably use these two functions.
 
-def _format_final_exc_line(etype, value):
+def _format_final_exc_line(etype, value, *, insert_final_newline=True):
     valuestr = _safe_string(value, 'exception')
+    end_char = "\n" if insert_final_newline else ""
     if value is None or not valuestr:
-        line = "%s\n" % etype
+        line = f"{etype}{end_char}"
     else:
-        line = "%s: %s\n" % (etype, valuestr)
+        line = f"{etype}: {valuestr}{end_char}"
     return line
 
 def _safe_string(value, what, func=str):
@@ -889,6 +894,10 @@ class TracebackException:
         display detailed information about where the syntax error occurred.
         Following the message, generator also yields
         all the exception's ``__notes__``.
+
+        When *show_group* is ``True``, and the exception is an instance of
+        :exc:`BaseExceptionGroup`, the nested exceptions are included as
+        well, recursively, with indentation relative to their nesting depth.
         """
 
         indent = 3 * _depth * ' '
@@ -904,7 +913,17 @@ class TracebackException:
             stype = smod + '.' + stype
 
         if not issubclass(self.exc_type, SyntaxError):
-            yield indent + _format_final_exc_line(stype, self._str)
+            if _depth > 0:
+                # Nested exceptions needs correct handling of multiline messages.
+                formatted = _format_final_exc_line(
+                    stype, self._str, insert_final_newline=False,
+                ).split('\n')
+                yield from [
+                    indent + l + '\n'
+                    for l in formatted
+                ]
+            else:
+                yield _format_final_exc_line(stype, self._str)
         else:
             yield from [indent + l for l in self._format_syntax_error(stype)]
 
diff --git a/Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst b/Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst
new file mode 100644 (file)
index 0000000..3531964
--- /dev/null
@@ -0,0 +1,2 @@
+Add ``show_group`` parameter to :func:`traceback.format_exception_only`,
+which allows to format :exc:`ExceptionGroup` instances.