Skip to content

Commit 16bbd9e

Browse files
committed
pythongh-111448: Add limit_group parameter to traceback.format_exception_only
1 parent baeb771 commit 16bbd9e

File tree

4 files changed

+371
-35
lines changed

4 files changed

+371
-35
lines changed

Doc/library/traceback.rst

Lines changed: 16 additions & 4 deletions
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], *, show_group=False)
138+
.. function:: format_exception_only(exc, /[, value], *, show_group=False, limit_group=True)
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
@@ -153,6 +153,12 @@ The module defines the following functions:
153153
:exc:`BaseExceptionGroup`, the nested exceptions are included as
154154
well, recursively, with indentation relative to their nesting depth.
155155

156+
When *limit_group* is ``True``, and the exception is an instance of
157+
:exc:`BaseExceptionGroup`, and *show_group* is ``True``,
158+
the number of nested exception groups
159+
and the number of exceptions in each exception group is limited
160+
the same way :meth:`TracebackException.format` does.
161+
156162
.. versionchanged:: 3.10
157163
The *etype* parameter has been renamed to *exc* and is now
158164
positional-only.
@@ -161,7 +167,7 @@ The module defines the following functions:
161167
The returned list now includes any notes attached to the exception.
162168

163169
.. versionchanged:: 3.13
164-
*show_group* parameter was added.
170+
*show_group* and *limit_group* parameters were added.
165171

166172

167173
.. function:: format_exception(exc, /[, value, tb], limit=None, chain=True)
@@ -346,7 +352,7 @@ capture data for later printing in a lightweight fashion.
346352
some containing internal newlines. :func:`~traceback.print_exception`
347353
is a wrapper around this method which just prints the lines to a file.
348354

349-
.. method:: format_exception_only(*, show_group=False)
355+
.. method:: format_exception_only(*, show_group=False, limit_group=True)
350356

351357
Format the exception part of the traceback.
352358

@@ -362,11 +368,17 @@ capture data for later printing in a lightweight fashion.
362368
:exc:`BaseExceptionGroup`, the nested exceptions are included as
363369
well, recursively, with indentation relative to their nesting depth.
364370

371+
When *limit_group* is ``True``, and the exception is an instance of
372+
:exc:`BaseExceptionGroup`, and *show_group* is ``True``,
373+
the number of nested exception groups
374+
and the number of exceptions in each exception group is limited
375+
the same way :meth:`TracebackException.format` does.
376+
365377
.. versionchanged:: 3.11
366378
The exception's notes are now included in the output.
367379

368380
.. versionchanged:: 3.13
369-
Added the *show_group* parameter.
381+
*show_group* and *limit_group* parameters were added.
370382

371383

372384
:class:`StackSummary` Objects

Lib/test/test_traceback.py

Lines changed: 244 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,11 @@ def test_caret(self):
111111
def test_nocaret(self):
112112
exc = SyntaxError("error", ("x.py", 23, None, "bad syntax"))
113113
err = traceback.format_exception_only(SyntaxError, exc)
114-
self.assertEqual(len(err), 3)
115-
self.assertEqual(err[1].strip(), "bad syntax")
114+
self.assertEqual(err, [
115+
' File "x.py", line 23\n',
116+
' bad syntax\n',
117+
'SyntaxError: error\n',
118+
])
116119

117120
def test_no_caret_with_no_debug_ranges_flag(self):
118121
# Make sure that if `-X no_debug_ranges` is used, there are no carets
@@ -364,6 +367,232 @@ def f():
364367
' ValueError: 0\n',
365368
])
366369

370+
def test_format_exception_group_width_limit(self):
371+
eg = ExceptionGroup('A', [ValueError(i) for i in range(100)])
372+
err = traceback.format_exception_only(eg, show_group=True)
373+
self.assertEqual(err, [
374+
'ExceptionGroup: A (100 sub-exceptions)\n',
375+
' ValueError: 0\n',
376+
' ValueError: 1\n',
377+
' ValueError: 2\n',
378+
' ValueError: 3\n',
379+
' ValueError: 4\n',
380+
' ValueError: 5\n',
381+
' ValueError: 6\n',
382+
' ValueError: 7\n',
383+
' ValueError: 8\n',
384+
' ValueError: 9\n',
385+
' ValueError: 10\n',
386+
' ValueError: 11\n',
387+
' ValueError: 12\n',
388+
' ValueError: 13\n',
389+
' ValueError: 14\n',
390+
' and 85 more exceptions\n',
391+
])
392+
393+
def test_format_exception_group_width_limit_n_plus_1(self):
394+
# bigger than `TracebackException.max_group_width`
395+
eg = ExceptionGroup('A', [ValueError(i) for i in range(16)])
396+
err = traceback.format_exception_only(eg, show_group=True)
397+
self.assertEqual(err, [
398+
'ExceptionGroup: A (16 sub-exceptions)\n',
399+
' ValueError: 0\n',
400+
' ValueError: 1\n',
401+
' ValueError: 2\n',
402+
' ValueError: 3\n',
403+
' ValueError: 4\n',
404+
' ValueError: 5\n',
405+
' ValueError: 6\n',
406+
' ValueError: 7\n',
407+
' ValueError: 8\n',
408+
' ValueError: 9\n',
409+
' ValueError: 10\n',
410+
' ValueError: 11\n',
411+
' ValueError: 12\n',
412+
' ValueError: 13\n',
413+
' ValueError: 14\n',
414+
' and 1 more exception\n',
415+
])
416+
417+
def test_format_exception_group_width_limit_of_syntax_errors(self):
418+
eg = ExceptionGroup('A', [
419+
SyntaxError(i, ("x.py", 23, None, "bad syntax"))
420+
for i in range(16)
421+
])
422+
err = traceback.format_exception_only(eg, show_group=True)
423+
self.assertEqual(err, [
424+
'ExceptionGroup: A (16 sub-exceptions)\n',
425+
' File "x.py", line 23\n',
426+
' bad syntax\n',
427+
' SyntaxError: <no detail available>\n',
428+
' File "x.py", line 23\n',
429+
' bad syntax\n',
430+
' SyntaxError: 1\n',
431+
' File "x.py", line 23\n',
432+
' bad syntax\n',
433+
' SyntaxError: 2\n',
434+
' File "x.py", line 23\n',
435+
' bad syntax\n',
436+
' SyntaxError: 3\n',
437+
' File "x.py", line 23\n',
438+
' bad syntax\n',
439+
' SyntaxError: 4\n',
440+
' File "x.py", line 23\n',
441+
' bad syntax\n',
442+
' SyntaxError: 5\n',
443+
' File "x.py", line 23\n',
444+
' bad syntax\n',
445+
' SyntaxError: 6\n',
446+
' File "x.py", line 23\n',
447+
' bad syntax\n',
448+
' SyntaxError: 7\n',
449+
' File "x.py", line 23\n',
450+
' bad syntax\n',
451+
' SyntaxError: 8\n',
452+
' File "x.py", line 23\n',
453+
' bad syntax\n',
454+
' SyntaxError: 9\n',
455+
' File "x.py", line 23\n',
456+
' bad syntax\n',
457+
' SyntaxError: 10\n',
458+
' File "x.py", line 23\n',
459+
' bad syntax\n',
460+
' SyntaxError: 11\n',
461+
' File "x.py", line 23\n',
462+
' bad syntax\n',
463+
' SyntaxError: 12\n',
464+
' File "x.py", line 23\n',
465+
' bad syntax\n',
466+
' SyntaxError: 13\n',
467+
' File "x.py", line 23\n',
468+
' bad syntax\n',
469+
' SyntaxError: 14\n',
470+
' and 1 more exception\n',
471+
])
472+
473+
def test_format_exception_group_no_width_limit(self):
474+
eg = ExceptionGroup('A', [ValueError(i) for i in range(17)])
475+
err = traceback.format_exception_only(eg, show_group=True, limit_group=False)
476+
self.assertEqual(err, [
477+
'ExceptionGroup: A (17 sub-exceptions)\n',
478+
' ValueError: 0\n',
479+
' ValueError: 1\n',
480+
' ValueError: 2\n',
481+
' ValueError: 3\n',
482+
' ValueError: 4\n',
483+
' ValueError: 5\n',
484+
' ValueError: 6\n',
485+
' ValueError: 7\n',
486+
' ValueError: 8\n',
487+
' ValueError: 9\n',
488+
' ValueError: 10\n',
489+
' ValueError: 11\n',
490+
' ValueError: 12\n',
491+
' ValueError: 13\n',
492+
' ValueError: 14\n',
493+
' ValueError: 15\n',
494+
' ValueError: 16\n',
495+
])
496+
497+
def test_format_nested_exception_group_nesting_limit(self):
498+
exc = TypeError('bad type')
499+
for i in range(11): # bigger than `TracebackException.max_group_depth`
500+
exc = ExceptionGroup(f'eg{i}', [ValueError(i), exc, ValueError(-i)])
501+
err = traceback.format_exception_only(exc, show_group=True)
502+
self.assertEqual(err, [
503+
'ExceptionGroup: eg10 (3 sub-exceptions)\n',
504+
' ValueError: 10\n',
505+
' ExceptionGroup: eg9 (3 sub-exceptions)\n',
506+
' ValueError: 9\n',
507+
' ExceptionGroup: eg8 (3 sub-exceptions)\n',
508+
' ValueError: 8\n',
509+
' ExceptionGroup: eg7 (3 sub-exceptions)\n',
510+
' ValueError: 7\n',
511+
' ExceptionGroup: eg6 (3 sub-exceptions)\n',
512+
' ValueError: 6\n',
513+
' ExceptionGroup: eg5 (3 sub-exceptions)\n',
514+
' ValueError: 5\n',
515+
' ExceptionGroup: eg4 (3 sub-exceptions)\n',
516+
' ValueError: 4\n',
517+
' ExceptionGroup: eg3 (3 sub-exceptions)\n',
518+
' ValueError: 3\n',
519+
' ExceptionGroup: eg2 (3 sub-exceptions)\n',
520+
' ValueError: 2\n',
521+
' ExceptionGroup: eg1 (3 sub-exceptions)\n',
522+
' ValueError: 1\n',
523+
' ... (max_group_depth is 10)\n',
524+
' ValueError: -1\n',
525+
' ValueError: -2\n',
526+
' ValueError: -3\n',
527+
' ValueError: -4\n',
528+
' ValueError: -5\n',
529+
' ValueError: -6\n',
530+
' ValueError: -7\n',
531+
' ValueError: -8\n',
532+
' ValueError: -9\n',
533+
' ValueError: -10\n',
534+
])
535+
536+
def test_format_nested_exception_group_nesting_no_limit(self):
537+
exc = TypeError('bad type')
538+
for i in range(11):
539+
exc = ExceptionGroup(f'eg{i}', [ValueError(i), exc, ValueError(-i)])
540+
err = traceback.format_exception_only(exc, show_group=True, limit_group=False)
541+
self.assertEqual(err, [
542+
'ExceptionGroup: eg10 (3 sub-exceptions)\n',
543+
' ValueError: 10\n',
544+
' ExceptionGroup: eg9 (3 sub-exceptions)\n',
545+
' ValueError: 9\n',
546+
' ExceptionGroup: eg8 (3 sub-exceptions)\n',
547+
' ValueError: 8\n',
548+
' ExceptionGroup: eg7 (3 sub-exceptions)\n',
549+
' ValueError: 7\n',
550+
' ExceptionGroup: eg6 (3 sub-exceptions)\n',
551+
' ValueError: 6\n',
552+
' ExceptionGroup: eg5 (3 sub-exceptions)\n',
553+
' ValueError: 5\n',
554+
' ExceptionGroup: eg4 (3 sub-exceptions)\n',
555+
' ValueError: 4\n',
556+
' ExceptionGroup: eg3 (3 sub-exceptions)\n',
557+
' ValueError: 3\n',
558+
' ExceptionGroup: eg2 (3 sub-exceptions)\n',
559+
' ValueError: 2\n',
560+
' ExceptionGroup: eg1 (3 sub-exceptions)\n',
561+
' ValueError: 1\n',
562+
' ExceptionGroup: eg0 (3 sub-exceptions)\n',
563+
' ValueError: 0\n',
564+
' TypeError: bad type\n',
565+
' ValueError: 0\n',
566+
' ValueError: -1\n',
567+
' ValueError: -2\n',
568+
' ValueError: -3\n',
569+
' ValueError: -4\n',
570+
' ValueError: -5\n',
571+
' ValueError: -6\n',
572+
' ValueError: -7\n',
573+
' ValueError: -8\n',
574+
' ValueError: -9\n',
575+
' ValueError: -10\n',
576+
])
577+
578+
def test_format_exception_group_no_width_limit_reursion_error(self):
579+
rec_limit = 100
580+
exc = TypeError('bad type')
581+
for i in range(rec_limit + 1):
582+
exc = ExceptionGroup(f'eg{i}', [ValueError(i), exc, ValueError(-i)])
583+
584+
with support.infinite_recursion(rec_limit):
585+
# Does not raise:
586+
traceback.format_exception_only(
587+
exc, show_group=True, limit_group=True,
588+
)
589+
590+
# Raises:
591+
with self.assertRaises(RecursionError):
592+
traceback.format_exception_only(
593+
exc, show_group=True, limit_group=False,
594+
)
595+
367596
@requires_subprocess()
368597
def test_encoded_file(self):
369598
# Test that tracebacks are correctly printed for encoded source files:
@@ -494,6 +723,18 @@ def test_format_exception_only_exc(self):
494723
output = traceback.format_exception_only(Exception("projector"))
495724
self.assertEqual(output, ["Exception: projector\n"])
496725

726+
def test_format_exception_only_exc_limit_group_false(self):
727+
# Extra kwargs do not affect regular exceptions:
728+
output = traceback.format_exception_only(
729+
Exception("projector"), limit_group=True,
730+
)
731+
self.assertEqual(output, ["Exception: projector\n"])
732+
733+
output = traceback.format_exception_only(
734+
Exception("projector"), show_group=True, limit_group=True,
735+
)
736+
self.assertEqual(output, ["Exception: projector\n"])
737+
497738
def test_exception_is_None(self):
498739
NONE_EXC_STRING = 'NoneType: None\n'
499740
excfile = StringIO()
@@ -530,7 +771,7 @@ def test_signatures(self):
530771

531772
self.assertEqual(
532773
str(inspect.signature(traceback.format_exception_only)),
533-
'(exc, /, value=<implicit>, *, show_group=False)')
774+
'(exc, /, value=<implicit>, *, show_group=False, limit_group=True)')
534775

535776

536777
class PurePythonExceptionFormattingMixin:

0 commit comments

Comments
 (0)