Skip to content

gh-111388: Add show_group to traceback.format_exception_only #111390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Doc/library/traceback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -149,13 +149,20 @@ 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.

.. 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)

Expand Down
151 changes: 150 additions & 1 deletion Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,155 @@ def __str__(self):
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:
Expand Down Expand Up @@ -381,7 +530,7 @@ def test_signatures(self):

self.assertEqual(
str(inspect.signature(traceback.format_exception_only)),
'(exc, /, value=<implicit>)')
'(exc, /, value=<implicit>, *, show_group=False)')


class PurePythonExceptionFormattingMixin:
Expand Down
31 changes: 25 additions & 6 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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):
Expand Down Expand Up @@ -889,6 +894,10 @@ def format_exception_only(self, *, show_group=False, _depth=0):
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 * ' '
Expand All @@ -904,7 +913,17 @@ def format_exception_only(self, *, show_group=False, _depth=0):
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this fix need to be backported to 3.12/311?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because 3.12 does not have show_group / _depth at all:

cpython/Lib/traceback.py

Lines 865 to 892 in 307ca78

def format_exception_only(self):
"""Format the exception part of the traceback.
The return value is a generator of strings, each ending in a newline.
Generator yields the exception message.
For :exc:`SyntaxError` exceptions, it
also yields (before the exception message)
several lines that (when printed)
display detailed information about where the syntax error occurred.
Following the message, generator also yields
all the exception's ``__notes__``.
"""
if self.exc_type is None:
yield _format_final_exc_line(None, self._str)
return
stype = self.exc_type.__qualname__
smod = self.exc_type.__module__
if smod not in ("__main__", "builtins"):
if not isinstance(smod, str):
smod = "<unknown>"
stype = smod + '.' + stype
if not issubclass(self.exc_type, SyntaxError):
yield _format_final_exc_line(stype, self._str)
else:
yield from self._format_syntax_error(stype)

And without it, the result is identical with and without this fix.

else:
yield from [indent + l for l in self._format_syntax_error(stype)]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``show_group`` parameter to :func:`traceback.format_exception_only`,
which allows to format :exc:`ExceptionGroup` instances.