From a3155cf2e80e3396e3564212226eb39c7be1bd0d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 27 Oct 2023 09:56:43 +0300 Subject: [PATCH 1/3] gh-111388: Add `show_group` to `traceback.format_exception_only` --- Doc/library/traceback.rst | 9 +- Lib/test/test_traceback.py | 133 +++++++++++++++++- Lib/traceback.py | 22 ++- ...-10-27-09-56-20.gh-issue-111388.SlmDbC.rst | 2 + 4 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 67ee73d4b2e1e5..408da7fc5f0645 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -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 @@ -149,6 +149,10 @@ 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. @@ -156,6 +160,9 @@ The module defines the following functions: .. 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) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 0c5d7c9c8c50d3..855bc196dcd507 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -215,6 +215,137 @@ 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_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: @@ -381,7 +512,7 @@ def test_signatures(self): self.assertEqual( str(inspect.signature(traceback.format_exception_only)), - '(exc, /, value=)') + '(exc, /, value=, *, show_group=False)') class PurePythonExceptionFormattingMixin: diff --git a/Lib/traceback.py b/Lib/traceback.py index 0d41c3432eda2c..028422dbf7fcd3 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -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. @@ -158,11 +158,15 @@ 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. @@ -889,6 +893,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 * ' ' @@ -904,7 +912,15 @@ 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. + yield from [ + indent + l + '\n' + for l in _format_final_exc_line(stype, self._str).split('\n') + if l + ] + else: + yield _format_final_exc_line(stype, self._str) else: yield from [indent + l for l in self._format_syntax_error(stype)] diff --git a/Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst b/Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst new file mode 100644 index 00000000000000..318cf8735625e4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst @@ -0,0 +1,2 @@ +Add ``show_group`` parameter to :func:`traceback.format_exception_only`, +which allows to format :exception:`ExceptionGroup` instances. From a91e2589610d06c1a627cfddb3b5d01ec313fcfd Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Fri, 27 Oct 2023 10:03:11 +0300 Subject: [PATCH 2/3] It is `:exc:`, not `:exception:` --- .../next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst b/Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst index 318cf8735625e4..353196439a9cff 100644 --- a/Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst +++ b/Misc/NEWS.d/next/Library/2023-10-27-09-56-20.gh-issue-111388.SlmDbC.rst @@ -1,2 +1,2 @@ Add ``show_group`` parameter to :func:`traceback.format_exception_only`, -which allows to format :exception:`ExceptionGroup` instances. +which allows to format :exc:`ExceptionGroup` instances. From 6f971427ceeca2bbfbb2cead8a936ff6e3292561 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 27 Oct 2023 10:11:48 +0300 Subject: [PATCH 3/3] More tests --- Lib/test/test_traceback.py | 18 ++++++++++++++++++ Lib/traceback.py | 13 ++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 855bc196dcd507..b43dca6f640b9a 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -273,6 +273,24 @@ def test_format_exception_group_multiline_messages(self): ' 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]) diff --git a/Lib/traceback.py b/Lib/traceback.py index 028422dbf7fcd3..b25a7291f6be51 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -171,12 +171,13 @@ def format_exception_only(exc, /, value=_sentinel, *, show_group=False): # -- 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): @@ -914,10 +915,12 @@ def format_exception_only(self, *, show_group=False, _depth=0): if not issubclass(self.exc_type, SyntaxError): 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 _format_final_exc_line(stype, self._str).split('\n') - if l + for l in formatted ] else: yield _format_final_exc_line(stype, self._str)