mirror of https://github.com/python/cpython
gh-111388: Add `show_group` parameter to `traceback.format_exception_only` (#111390)
This commit is contained in:
parent
6d42759c5e
commit
aa732459c5
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Add ``show_group`` parameter to :func:`traceback.format_exception_only`,
|
||||||
|
which allows to format :exc:`ExceptionGroup` instances.
|
Loading…
Reference in New Issue