gh-111388: Add `show_group` parameter to `traceback.format_exception_only` (#111390)

This commit is contained in:
Nikita Sobolev 2023-10-27 13:11:26 +03:00 committed by GitHub
parent 6d42759c5e
commit aa732459c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 185 additions and 8 deletions

View File

@ -135,7 +135,7 @@ The module defines the following functions:
text line is not ``None``. 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 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 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 can be passed as the first argument. If *value* is provided, the first
argument is ignored in order to provide backwards compatibility. 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 .. versionchanged:: 3.10
The *etype* parameter has been renamed to *exc* and is now The *etype* parameter has been renamed to *exc* and is now
positional-only. positional-only.
@ -156,6 +160,9 @@ The module defines the following functions:
.. versionchanged:: 3.11 .. versionchanged:: 3.11
The returned list now includes any notes attached to the exception. 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) .. function:: format_exception(exc, /[, value, tb], limit=None, chain=True)

View File

@ -215,6 +215,155 @@ class TracebackCases(unittest.TestCase):
str_name = '.'.join([X.__module__, X.__qualname__]) str_name = '.'.join([X.__module__, X.__qualname__])
self.assertEqual(err[0], "%s: %s\n" % (str_name, str_value)) 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() @requires_subprocess()
def test_encoded_file(self): def test_encoded_file(self):
# Test that tracebacks are correctly printed for encoded source files: # Test that tracebacks are correctly printed for encoded source files:
@ -381,7 +530,7 @@ class TracebackCases(unittest.TestCase):
self.assertEqual( self.assertEqual(
str(inspect.signature(traceback.format_exception_only)), str(inspect.signature(traceback.format_exception_only)),
'(exc, /, value=<implicit>)') '(exc, /, value=<implicit>, *, show_group=False)')
class PurePythonExceptionFormattingMixin: class PurePythonExceptionFormattingMixin:

View File

@ -148,7 +148,7 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
return list(te.format(chain=chain)) 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. """Format the exception part of a traceback.
The return value is a list of strings, each ending in a newline. 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 contains several lines that (when printed) display detailed information
about where the syntax error occurred. Following the message, the list about where the syntax error occurred. Following the message, the list
contains the exception's ``__notes__``. 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: if value is _sentinel:
value = exc value = exc
te = TracebackException(type(value), value, None, compact=True) 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. # -- 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') valuestr = _safe_string(value, 'exception')
end_char = "\n" if insert_final_newline else ""
if value is None or not valuestr: if value is None or not valuestr:
line = "%s\n" % etype line = f"{etype}{end_char}"
else: else:
line = "%s: %s\n" % (etype, valuestr) line = f"{etype}: {valuestr}{end_char}"
return line return line
def _safe_string(value, what, func=str): def _safe_string(value, what, func=str):
@ -889,6 +894,10 @@ class TracebackException:
display detailed information about where the syntax error occurred. display detailed information about where the syntax error occurred.
Following the message, generator also yields Following the message, generator also yields
all the exception's ``__notes__``. 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 * ' ' indent = 3 * _depth * ' '
@ -904,7 +913,17 @@ class TracebackException:
stype = smod + '.' + stype stype = smod + '.' + stype
if not issubclass(self.exc_type, SyntaxError): 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: else:
yield from [indent + l for l in self._format_syntax_error(stype)] yield from [indent + l for l in self._format_syntax_error(stype)]

View File

@ -0,0 +1,2 @@
Add ``show_group`` parameter to :func:`traceback.format_exception_only`,
which allows to format :exc:`ExceptionGroup` instances.