summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/test_traceback.py151
-rw-r--r--Lib/traceback.py31
2 files changed, 175 insertions, 7 deletions
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 0c5d7c9..b43dca6 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -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:
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 0d41c34..b25a729 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -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)]