Skip to content

Commit aa73245

Browse files
authored
pythongh-111388: Add show_group parameter to traceback.format_exception_only (python#111390)
1 parent 6d42759 commit aa73245

File tree

4 files changed

+185
-8
lines changed

4 files changed

+185
-8
lines changed

Doc/library/traceback.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ The module defines the following functions:
135135
text line is not ``None``.
136136

137137

138-
.. function:: format_exception_only(exc, /[, value])
138+
.. function:: format_exception_only(exc, /[, value], *, show_group=False)
139139

140140
Format the exception part of a traceback using an exception value such as
141141
given by ``sys.last_value``. The return value is a list of strings, each
@@ -149,13 +149,20 @@ The module defines the following functions:
149149
can be passed as the first argument. If *value* is provided, the first
150150
argument is ignored in order to provide backwards compatibility.
151151

152+
When *show_group* is ``True``, and the exception is an instance of
153+
:exc:`BaseExceptionGroup`, the nested exceptions are included as
154+
well, recursively, with indentation relative to their nesting depth.
155+
152156
.. versionchanged:: 3.10
153157
The *etype* parameter has been renamed to *exc* and is now
154158
positional-only.
155159

156160
.. versionchanged:: 3.11
157161
The returned list now includes any notes attached to the exception.
158162

163+
.. versionchanged:: 3.13
164+
*show_group* parameter was added.
165+
159166

160167
.. function:: format_exception(exc, /[, value, tb], limit=None, chain=True)
161168

Lib/test/test_traceback.py

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,155 @@ def __str__(self):
215215
str_name = '.'.join([X.__module__, X.__qualname__])
216216
self.assertEqual(err[0], "%s: %s\n" % (str_name, str_value))
217217

218+
def test_format_exception_group_without_show_group(self):
219+
eg = ExceptionGroup('A', [ValueError('B')])
220+
err = traceback.format_exception_only(eg)
221+
self.assertEqual(err, ['ExceptionGroup: A (1 sub-exception)\n'])
222+
223+
def test_format_exception_group(self):
224+
eg = ExceptionGroup('A', [ValueError('B')])
225+
err = traceback.format_exception_only(eg, show_group=True)
226+
self.assertEqual(err, [
227+
'ExceptionGroup: A (1 sub-exception)\n',
228+
' ValueError: B\n',
229+
])
230+
231+
def test_format_base_exception_group(self):
232+
eg = BaseExceptionGroup('A', [BaseException('B')])
233+
err = traceback.format_exception_only(eg, show_group=True)
234+
self.assertEqual(err, [
235+
'BaseExceptionGroup: A (1 sub-exception)\n',
236+
' BaseException: B\n',
237+
])
238+
239+
def test_format_exception_group_with_note(self):
240+
exc = ValueError('B')
241+
exc.add_note('Note')
242+
eg = ExceptionGroup('A', [exc])
243+
err = traceback.format_exception_only(eg, show_group=True)
244+
self.assertEqual(err, [
245+
'ExceptionGroup: A (1 sub-exception)\n',
246+
' ValueError: B\n',
247+
' Note\n',
248+
])
249+
250+
def test_format_exception_group_explicit_class(self):
251+
eg = ExceptionGroup('A', [ValueError('B')])
252+
err = traceback.format_exception_only(ExceptionGroup, eg, show_group=True)
253+
self.assertEqual(err, [
254+
'ExceptionGroup: A (1 sub-exception)\n',
255+
' ValueError: B\n',
256+
])
257+
258+
def test_format_exception_group_multiple_exceptions(self):
259+
eg = ExceptionGroup('A', [ValueError('B'), TypeError('C')])
260+
err = traceback.format_exception_only(eg, show_group=True)
261+
self.assertEqual(err, [
262+
'ExceptionGroup: A (2 sub-exceptions)\n',
263+
' ValueError: B\n',
264+
' TypeError: C\n',
265+
])
266+
267+
def test_format_exception_group_multiline_messages(self):
268+
eg = ExceptionGroup('A\n1', [ValueError('B\n2')])
269+
err = traceback.format_exception_only(eg, show_group=True)
270+
self.assertEqual(err, [
271+
'ExceptionGroup: A\n1 (1 sub-exception)\n',
272+
' ValueError: B\n',
273+
' 2\n',
274+
])
275+
276+
def test_format_exception_group_multiline2_messages(self):
277+
exc = ValueError('B\n\n2\n')
278+
exc.add_note('\nC\n\n3')
279+
eg = ExceptionGroup('A\n\n1\n', [exc, IndexError('D')])
280+
err = traceback.format_exception_only(eg, show_group=True)
281+
self.assertEqual(err, [
282+
'ExceptionGroup: A\n\n1\n (2 sub-exceptions)\n',
283+
' ValueError: B\n',
284+
' \n',
285+
' 2\n',
286+
' \n',
287+
' \n', # first char of `note`
288+
' C\n',
289+
' \n',
290+
' 3\n', # note ends
291+
' IndexError: D\n',
292+
])
293+
294+
def test_format_exception_group_syntax_error(self):
295+
exc = SyntaxError("error", ("x.py", 23, None, "bad syntax"))
296+
eg = ExceptionGroup('A\n1', [exc])
297+
err = traceback.format_exception_only(eg, show_group=True)
298+
self.assertEqual(err, [
299+
'ExceptionGroup: A\n1 (1 sub-exception)\n',
300+
' File "x.py", line 23\n',
301+
' bad syntax\n',
302+
' SyntaxError: error\n',
303+
])
304+
305+
def test_format_exception_group_nested_with_notes(self):
306+
exc = IndexError('D')
307+
exc.add_note('Note\nmultiline')
308+
eg = ExceptionGroup('A', [
309+
ValueError('B'),
310+
ExceptionGroup('C', [exc, LookupError('E')]),
311+
TypeError('F'),
312+
])
313+
err = traceback.format_exception_only(eg, show_group=True)
314+
self.assertEqual(err, [
315+
'ExceptionGroup: A (3 sub-exceptions)\n',
316+
' ValueError: B\n',
317+
' ExceptionGroup: C (2 sub-exceptions)\n',
318+
' IndexError: D\n',
319+
' Note\n',
320+
' multiline\n',
321+
' LookupError: E\n',
322+
' TypeError: F\n',
323+
])
324+
325+
def test_format_exception_group_with_tracebacks(self):
326+
def f():
327+
try:
328+
1 / 0
329+
except ZeroDivisionError as e:
330+
return e
331+
332+
def g():
333+
try:
334+
raise TypeError('g')
335+
except TypeError as e:
336+
return e
337+
338+
eg = ExceptionGroup('A', [
339+
f(),
340+
ExceptionGroup('B', [g()]),
341+
])
342+
err = traceback.format_exception_only(eg, show_group=True)
343+
self.assertEqual(err, [
344+
'ExceptionGroup: A (2 sub-exceptions)\n',
345+
' ZeroDivisionError: division by zero\n',
346+
' ExceptionGroup: B (1 sub-exception)\n',
347+
' TypeError: g\n',
348+
])
349+
350+
def test_format_exception_group_with_cause(self):
351+
def f():
352+
try:
353+
try:
354+
1 / 0
355+
except ZeroDivisionError:
356+
raise ValueError(0)
357+
except Exception as e:
358+
return e
359+
360+
eg = ExceptionGroup('A', [f()])
361+
err = traceback.format_exception_only(eg, show_group=True)
362+
self.assertEqual(err, [
363+
'ExceptionGroup: A (1 sub-exception)\n',
364+
' ValueError: 0\n',
365+
])
366+
218367
@requires_subprocess()
219368
def test_encoded_file(self):
220369
# Test that tracebacks are correctly printed for encoded source files:
@@ -381,7 +530,7 @@ def test_signatures(self):
381530

382531
self.assertEqual(
383532
str(inspect.signature(traceback.format_exception_only)),
384-
'(exc, /, value=<implicit>)')
533+
'(exc, /, value=<implicit>, *, show_group=False)')
385534

386535

387536
class PurePythonExceptionFormattingMixin:

Lib/traceback.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
148148
return list(te.format(chain=chain))
149149

150150

151-
def format_exception_only(exc, /, value=_sentinel):
151+
def format_exception_only(exc, /, value=_sentinel, *, show_group=False):
152152
"""Format the exception part of a traceback.
153153
154154
The return value is a list of strings, each ending in a newline.
@@ -158,21 +158,26 @@ def format_exception_only(exc, /, value=_sentinel):
158158
contains several lines that (when printed) display detailed information
159159
about where the syntax error occurred. Following the message, the list
160160
contains the exception's ``__notes__``.
161+
162+
When *show_group* is ``True``, and the exception is an instance of
163+
:exc:`BaseExceptionGroup`, the nested exceptions are included as
164+
well, recursively, with indentation relative to their nesting depth.
161165
"""
162166
if value is _sentinel:
163167
value = exc
164168
te = TracebackException(type(value), value, None, compact=True)
165-
return list(te.format_exception_only())
169+
return list(te.format_exception_only(show_group=show_group))
166170

167171

168172
# -- not official API but folk probably use these two functions.
169173

170-
def _format_final_exc_line(etype, value):
174+
def _format_final_exc_line(etype, value, *, insert_final_newline=True):
171175
valuestr = _safe_string(value, 'exception')
176+
end_char = "\n" if insert_final_newline else ""
172177
if value is None or not valuestr:
173-
line = "%s\n" % etype
178+
line = f"{etype}{end_char}"
174179
else:
175-
line = "%s: %s\n" % (etype, valuestr)
180+
line = f"{etype}: {valuestr}{end_char}"
176181
return line
177182

178183
def _safe_string(value, what, func=str):
@@ -889,6 +894,10 @@ def format_exception_only(self, *, show_group=False, _depth=0):
889894
display detailed information about where the syntax error occurred.
890895
Following the message, generator also yields
891896
all the exception's ``__notes__``.
897+
898+
When *show_group* is ``True``, and the exception is an instance of
899+
:exc:`BaseExceptionGroup`, the nested exceptions are included as
900+
well, recursively, with indentation relative to their nesting depth.
892901
"""
893902

894903
indent = 3 * _depth * ' '
@@ -904,7 +913,17 @@ def format_exception_only(self, *, show_group=False, _depth=0):
904913
stype = smod + '.' + stype
905914

906915
if not issubclass(self.exc_type, SyntaxError):
907-
yield indent + _format_final_exc_line(stype, self._str)
916+
if _depth > 0:
917+
# Nested exceptions needs correct handling of multiline messages.
918+
formatted = _format_final_exc_line(
919+
stype, self._str, insert_final_newline=False,
920+
).split('\n')
921+
yield from [
922+
indent + l + '\n'
923+
for l in formatted
924+
]
925+
else:
926+
yield _format_final_exc_line(stype, self._str)
908927
else:
909928
yield from [indent + l for l in self._format_syntax_error(stype)]
910929

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add ``show_group`` parameter to :func:`traceback.format_exception_only`,
2+
which allows to format :exc:`ExceptionGroup` instances.

0 commit comments

Comments
 (0)