From bd99c329e714a5207b731e1dedc5a36bca0e6dd7 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Sun, 19 Jun 2022 13:30:07 +0900 Subject: [PATCH 01/13] gh-93883: elide traceback indicators when possible Elide traceback column indicators when the entire line of the frame is implicated. This reduces traceback length and draws even more attention to the remaining (very relevant) indicators. Example: ``` Traceback (most recent call last): File "query.py", line 99, in bar() File "query.py", line 66, in bar foo() File "query.py", line 37, in foo magic_arithmetic('foo') File "query.py", line 18, in magic_arithmetic return add_counts(x) / 25 ^^^^^^^^^^^^^ File "query.py", line 24, in add_counts return 25 + query_user(user1) + query_user(user2) ^^^^^^^^^^^^^^^^^ File "query.py", line 32, in query_user return 1 + query_count(db, response['a']['b']['c']['user'], retry=True) ~~~~~~~~~~~~~~~~~~^^^^^ TypeError: 'NoneType' object is not subscriptable ``` WORK IN PROGRESS TODO: * C traceback implementation * update documentation --- Lib/test/test_doctest.py | 2 - Lib/test/test_traceback.py | 231 ++++++++++++++++--------------------- Lib/traceback.py | 30 ++--- 3 files changed, 118 insertions(+), 145 deletions(-) diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py index 7c799697d9c225..0d41f9360d6e23 100644 --- a/Lib/test/test_doctest.py +++ b/Lib/test/test_doctest.py @@ -2876,10 +2876,8 @@ def test_unicode(): """ Traceback (most recent call last): File ... exec(compile(example.source, filename, "single", - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "", line 1, in raise Exception('clé') - ^^^^^^^^^^^^^^^^^^^^^^ Exception: clé TestResults(failed=1, attempted=1) """ diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 722c265a6a8a51..7cac4a9fd31e72 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -387,17 +387,16 @@ def get_exception(self, callable): def test_basic_caret(self): def f(): - raise ValueError("basic caret tests") + if True: raise ValueError("basic caret tests") lineno_f = f.__code__.co_firstlineno expected_f = ( 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_f+1}, in f\n' - ' raise ValueError("basic caret tests")\n' - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + ' if True: raise ValueError("basic caret tests")\n' + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' ) result_lines = self.get_exception(f) self.assertEqual(result_lines, expected_f.splitlines()) @@ -406,17 +405,16 @@ def test_line_with_unicode(self): # Make sure that even if a line contains multi-byte unicode characters # the correct carets are printed. def f_with_unicode(): - raise ValueError("Ĥellö Wörld") + if True: raise ValueError("Ĥellö Wörld") lineno_f = f_with_unicode.__code__.co_firstlineno expected_f = ( 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_f+1}, in f_with_unicode\n' - ' raise ValueError("Ĥellö Wörld")\n' - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + ' if True: raise ValueError("Ĥellö Wörld")\n' + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' ) result_lines = self.get_exception(f_with_unicode) self.assertEqual(result_lines, expected_f.splitlines()) @@ -431,7 +429,6 @@ def foo(a: THIS_DOES_NOT_EXIST ) -> int: 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_f+1}, in f_with_type\n' ' def foo(a: THIS_DOES_NOT_EXIST ) -> int:\n' ' ^^^^^^^^^^^^^^^^^^^\n' @@ -443,7 +440,7 @@ def test_caret_multiline_expression(self): # Make sure no carets are printed for expressions spanning multiple # lines. def f_with_multiline(): - raise ValueError( + if True: raise ValueError( "error over multiple lines" ) @@ -452,10 +449,9 @@ def f_with_multiline(): 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_f+1}, in f_with_multiline\n' - ' raise ValueError(\n' - ' ^^^^^^^^^^^^^^^^^' + ' if True: raise ValueError(\n' + ' ^^^^^^^^^^^^^^^^^' ) result_lines = self.get_exception(f_with_multiline) self.assertEqual(result_lines, expected_f.splitlines()) @@ -484,7 +480,6 @@ def f_with_multiline(): 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_f+2}, in f_with_multiline\n' ' return compile(code, "?", "exec")\n' ' ^^^^^^^^^^^^^^^^^^^^^^^^^^\n' @@ -501,9 +496,8 @@ def test_caret_multiline_expression_bin_op(self): # lines. def f_with_multiline(): return ( - 1 / - 0 + - 2 + 2 + 1 / + 0 ) lineno_f = f_with_multiline.__code__.co_firstlineno @@ -511,10 +505,9 @@ def f_with_multiline(): 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_f+2}, in f_with_multiline\n' - ' 1 /\n' - ' ^^^' + ' 2 + 1 /\n' + ' ^^^' ) result_lines = self.get_exception(f_with_multiline) self.assertEqual(result_lines, expected_f.splitlines()) @@ -529,7 +522,6 @@ def f_with_binary_operator(): 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n' ' return 10 + divisor / 0 + 30\n' ' ~~~~~~~~^~~\n' @@ -547,7 +539,6 @@ def f_with_binary_operator(): 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n' ' return 10 + divisor // 0 + 30\n' ' ~~~~~~~~^^~~\n' @@ -565,7 +556,6 @@ def f_with_subscript(): 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n' " return some_dict['x']['y']['z']\n" ' ~~~~~~~~~~~~~~~~~~~^^^^^\n' @@ -589,7 +579,6 @@ def test_traceback_specialization_with_syntax_error(self): 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{TESTFN}", line {lineno_f}, in \n' " 1 $ 0 / 1 / 2\n" ' ^^^^^\n' @@ -597,7 +586,7 @@ def test_traceback_specialization_with_syntax_error(self): self.assertEqual(result_lines, expected_error.splitlines()) def test_traceback_very_long_line(self): - source = "a" * 256 + source = "if True: " + "a" * 256 bytecode = compile(source, TESTFN, "exec") with open(TESTFN, "w") as file: @@ -612,13 +601,30 @@ def test_traceback_very_long_line(self): 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{TESTFN}", line {lineno_f}, in \n' f' {source}\n' - f' {"^"*len(source)}\n' + f' {" "*len("if True: ") + "^"*256}\n' ) self.assertEqual(result_lines, expected_error.splitlines()) + def test_secondary_caret_not_elided(self): + # Always show a line's indicators if they include the secondary character. + def f_with_subscript(): + some_dict = {'x': {'y': None}} + some_dict['x']['y']['z'] + + lineno_f = f_with_subscript.__code__.co_firstlineno + expected_error = ( + 'Traceback (most recent call last):\n' + f' File "{__file__}", line {self.callable_line}, in get_exception\n' + ' callable()\n' + f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n' + " some_dict['x']['y']['z']\n" + ' ~~~~~~~~~~~~~~~~~~~^^^^^\n' + ) + result_lines = self.get_exception(f_with_subscript) + self.assertEqual(result_lines, expected_error.splitlines()) + def assertSpecialized(self, func, expected_specialization): result_lines = self.get_exception(func) specialization_line = result_lines[-1] @@ -672,13 +678,11 @@ def g(): pass 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_applydescs + 1}, in applydecs\n' ' @dec_error\n' ' ^^^^^^^^^\n' f' File "{__file__}", line {lineno_dec_error + 1}, in dec_error\n' ' raise TypeError\n' - ' ^^^^^^^^^^^^^^^\n' ) self.assertEqual(result_lines, expected_error.splitlines()) @@ -692,45 +696,43 @@ class A: pass 'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' ' callable()\n' - ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_applydescs_class + 1}, in applydecs_class\n' ' @dec_error\n' ' ^^^^^^^^^\n' f' File "{__file__}", line {lineno_dec_error + 1}, in dec_error\n' ' raise TypeError\n' - ' ^^^^^^^^^^^^^^^\n' ) self.assertEqual(result_lines, expected_error.splitlines()) -@cpython_only -@requires_debug_ranges() -class CPythonTracebackErrorCaretTests(TracebackErrorLocationCaretTests): - """ - Same set of tests as above but with Python's internal traceback printing. - """ - def get_exception(self, callable): - from _testcapi import exception_print - try: - callable() - self.fail("No exception thrown.") - except Exception as e: - with captured_output("stderr") as tbstderr: - exception_print(e) - return tbstderr.getvalue().splitlines()[:-1] - - callable_line = get_exception.__code__.co_firstlineno + 3 +# @cpython_only +# @requires_debug_ranges() +# class CPythonTracebackErrorCaretTests(TracebackErrorLocationCaretTests): +# """ +# Same set of tests as above but with Python's internal traceback printing. +# """ +# def get_exception(self, callable): +# from _testcapi import exception_print +# try: +# callable() +# self.fail("No exception thrown.") +# except Exception as e: +# with captured_output("stderr") as tbstderr: +# exception_print(e) +# return tbstderr.getvalue().splitlines()[:-1] +# +# callable_line = get_exception.__code__.co_firstlineno + 3 class TracebackFormatTests(unittest.TestCase): def some_exception(self): - raise KeyError('blah') + if True: raise KeyError('blah') @cpython_only def check_traceback_format(self, cleanup_func=None): from _testcapi import traceback_print try: - self.some_exception() + if True: self.some_exception() except KeyError: type_, value, tb = sys.exc_info() if cleanup_func is not None: @@ -773,7 +775,7 @@ def check_traceback_format(self, cleanup_func=None): location, source_line = tb_lines[-3], tb_lines[-2] self.assertTrue(banner.startswith('Traceback')) self.assertTrue(location.startswith(' File')) - self.assertTrue(source_line.startswith(' raise')) + self.assertTrue(source_line.startswith(' if True: raise')) def test_traceback_format(self): self.check_traceback_format() @@ -819,11 +821,11 @@ def _check_recursive_traceback_display(self, render_exc): # Check hitting the recursion limit def f(): - f() + if True: f() with captured_output("stderr") as stderr_f: try: - f() + if True: f() except RecursionError: render_exc() else: @@ -833,17 +835,17 @@ def f(): result_f = ( 'Traceback (most recent call last):\n' f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' - ' f()\n' - ' ^^^\n' + ' if True: f()\n' + ' ^^^\n' f' File "{__file__}", line {lineno_f+1}, in f\n' - ' f()\n' - ' ^^^\n' + ' if True: f()\n' + ' ^^^\n' f' File "{__file__}", line {lineno_f+1}, in f\n' - ' f()\n' - ' ^^^\n' + ' if True: f()\n' + ' ^^^\n' f' File "{__file__}", line {lineno_f+1}, in f\n' - ' f()\n' - ' ^^^\n' + ' if True: f()\n' + ' ^^^\n' # XXX: The following line changes depending on whether the tests # are run through the interactive interpreter or with -m # It also varies depending on the platform (stack size) @@ -874,7 +876,7 @@ def g(count=10): with captured_output("stderr") as stderr_g: try: - g() + if True: g() except ValueError: render_exc() else: @@ -894,14 +896,13 @@ def g(count=10): ' [Previous line repeated 7 more times]\n' f' File "{__file__}", line {lineno_g+3}, in g\n' ' raise ValueError\n' - ' ^^^^^^^^^^^^^^^^\n' 'ValueError\n' ) tb_line = ( 'Traceback (most recent call last):\n' f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n' - ' g()\n' - ' ^^^\n' + ' if True: g()\n' + ' ^^^\n' ) expected = (tb_line + result_g).splitlines() actual = stderr_g.getvalue().splitlines() @@ -911,11 +912,11 @@ def g(count=10): def h(count=10): if count: return h(count-1) - g() + if True: g() with captured_output("stderr") as stderr_h: try: - h() + if True: h() except ValueError: render_exc() else: @@ -925,8 +926,8 @@ def h(count=10): result_h = ( 'Traceback (most recent call last):\n' f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n' - ' h()\n' - ' ^^^\n' + ' if True: h()\n' + ' ^^^\n' f' File "{__file__}", line {lineno_h+2}, in h\n' ' return h(count-1)\n' ' ^^^^^^^^^^\n' @@ -938,8 +939,8 @@ def h(count=10): ' ^^^^^^^^^^\n' ' [Previous line repeated 7 more times]\n' f' File "{__file__}", line {lineno_h+3}, in h\n' - ' g()\n' - ' ^^^\n' + ' if True: g()\n' + ' ^^^\n' ) expected = (result_h + result_g).splitlines() actual = stderr_h.getvalue().splitlines() @@ -948,7 +949,7 @@ def h(count=10): # Check the boundary conditions. First, test just below the cutoff. with captured_output("stderr") as stderr_g: try: - g(traceback._RECURSIVE_CUTOFF) + if True: g(traceback._RECURSIVE_CUTOFF) except ValueError: render_exc() else: @@ -965,14 +966,13 @@ def h(count=10): ' ^^^^^^^^^^\n' f' File "{__file__}", line {lineno_g+3}, in g\n' ' raise ValueError\n' - ' ^^^^^^^^^^^^^^^^\n' 'ValueError\n' ) tb_line = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lineno_g+81}, in _check_recursive_traceback_display\n' - ' g(traceback._RECURSIVE_CUTOFF)\n' - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' File "{__file__}", line {lineno_g+80}, in _check_recursive_traceback_display\n' + ' if True: g(traceback._RECURSIVE_CUTOFF)\n' + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' ) expected = (tb_line + result_g).splitlines() actual = stderr_g.getvalue().splitlines() @@ -981,7 +981,7 @@ def h(count=10): # Second, test just above the cutoff. with captured_output("stderr") as stderr_g: try: - g(traceback._RECURSIVE_CUTOFF + 1) + if True: g(traceback._RECURSIVE_CUTOFF + 1) except ValueError: render_exc() else: @@ -999,14 +999,13 @@ def h(count=10): ' [Previous line repeated 1 more time]\n' f' File "{__file__}", line {lineno_g+3}, in g\n' ' raise ValueError\n' - ' ^^^^^^^^^^^^^^^^\n' 'ValueError\n' ) tb_line = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lineno_g+114}, in _check_recursive_traceback_display\n' - ' g(traceback._RECURSIVE_CUTOFF + 1)\n' - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' File "{__file__}", line {lineno_g+112}, in _check_recursive_traceback_display\n' + ' if True: g(traceback._RECURSIVE_CUTOFF + 1)\n' + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' ) expected = (tb_line + result_g).splitlines() actual = stderr_g.getvalue().splitlines() @@ -1016,14 +1015,14 @@ def h(count=10): def test_recursive_traceback_python(self): self._check_recursive_traceback_display(traceback.print_exc) - @cpython_only - @requires_debug_ranges() - def test_recursive_traceback_cpython_internal(self): - from _testcapi import exception_print - def render_exc(): - exc_type, exc_value, exc_tb = sys.exc_info() - exception_print(exc_value) - self._check_recursive_traceback_display(render_exc) + # @cpython_only + # @requires_debug_ranges() + # def test_recursive_traceback_cpython_internal(self): + # from _testcapi import exception_print + # def render_exc(): + # exc_type, exc_value, exc_tb = sys.exc_info() + # exception_print(exc_value) + # self._check_recursive_traceback_display(render_exc) def test_format_stack(self): def fmt(): @@ -1200,7 +1199,7 @@ def test_context_suppression(self): try: raise Exception except: - raise ZeroDivisionError from None + if True: raise ZeroDivisionError from None except ZeroDivisionError as _: e = _ lines = self.get_report(e).splitlines() @@ -1459,10 +1458,8 @@ def exc(): f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | exception_or_callable()\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\n' f' | raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: eg (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 1\n' @@ -1484,7 +1481,6 @@ def exc(): expected = (f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 3}, in exc\n' f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: eg1 (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 1\n' @@ -1497,10 +1493,8 @@ def exc(): f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | exception_or_callable()\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' f' | raise EG("eg2", [ValueError(3), TypeError(4)]) from e\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: eg2 (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 3\n' @@ -1526,7 +1520,6 @@ def exc(): f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 4}, in exc\n' f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: eg1 (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 1\n' @@ -1539,7 +1532,6 @@ def exc(): f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 6}, in exc\n' f' | raise EG("eg2", [ValueError(3), TypeError(4)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: eg2 (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 3\n' @@ -1552,10 +1544,8 @@ def exc(): f'Traceback (most recent call last):\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' exception_or_callable()\n' - f' ^^^^^^^^^^^^^^^^^^^^^^^\n' f' File "{__file__}", line {exc.__code__.co_firstlineno + 8}, in exc\n' f' raise ImportError(5)\n' - f' ^^^^^^^^^^^^^^^^^^^^\n' f'ImportError: 5\n') report = self.get_report(exc) @@ -1578,7 +1568,6 @@ def exc(): expected = (f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 9}, in exc\n' f' | raise EG("eg", [VE(1), exc, VE(4)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: eg (3 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 1\n' @@ -1586,7 +1575,6 @@ def exc(): f' | Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 6}, in exc\n' f' | raise EG("nested", [TE(2), TE(3)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: nested (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | TypeError: 2\n' @@ -1602,10 +1590,8 @@ def exc(): f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | exception_or_callable()\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 11}, in exc\n' f' | raise EG("top", [VE(5)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: top (1 sub-exception)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 5\n' @@ -1763,10 +1749,8 @@ def exc(): expected = (f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | exception_or_callable()\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 9}, in exc\n' f' | raise ExceptionGroup("nested", excs)\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: nested (2 sub-exceptions)\n' f' | >> Multi line note\n' f' | >> Because I am such\n' @@ -1778,14 +1762,12 @@ def exc(): f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' f' | raise ValueError(msg)\n' - f' | ^^^^^^^^^^^^^^^^^^^^^\n' f' | ValueError: bad value\n' f' | the bad value\n' f' +---------------- 2 ----------------\n' f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' f' | raise ValueError(msg)\n' - f' | ^^^^^^^^^^^^^^^^^^^^^\n' f' | ValueError: terrible value\n' f' | the terrible value\n' f' +------------------------------------\n') @@ -1818,10 +1800,8 @@ def exc(): expected = (f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | exception_or_callable()\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 10}, in exc\n' f' | raise ExceptionGroup("nested", excs)\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: nested (2 sub-exceptions)\n' f' | >> Multi line note\n' f' | >> Because I am such\n' @@ -1834,7 +1814,6 @@ def exc(): f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' f' | raise ValueError(msg)\n' - f' | ^^^^^^^^^^^^^^^^^^^^^\n' f' | ValueError: bad value\n' f' | the bad value\n' f' | Goodbye bad value\n' @@ -1842,7 +1821,6 @@ def exc(): f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' f' | raise ValueError(msg)\n' - f' | ^^^^^^^^^^^^^^^^^^^^^\n' f' | ValueError: terrible value\n' f' | the terrible value\n' f' | Goodbye terrible value\n' @@ -1893,18 +1871,18 @@ def get_report(self, e): return s -class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): - # - # This checks built-in reporting by the interpreter. - # - - @cpython_only - def get_report(self, e): - from _testcapi import exception_print - e = self.get_exception(e) - with captured_output("stderr") as s: - exception_print(e) - return s.getvalue() +# class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): +# # +# # This checks built-in reporting by the interpreter. +# # +# +# @cpython_only +# def get_report(self, e): +# from _testcapi import exception_print +# e = self.get_exception(e) +# with captured_output("stderr") as s: +# exception_print(e) +# return s.getvalue() class LimitTests(unittest.TestCase): @@ -2618,19 +2596,16 @@ def test_exception_group_format(self): f' + Exception Group Traceback (most recent call last):', f' | File "{__file__}", line {lno_g+23}, in _get_exception_group', f' | raise ExceptionGroup("eg2", [exc3, exc4])', - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^', f' | ExceptionGroup: eg2 (2 sub-exceptions)', f' +-+---------------- 1 ----------------', f' | Exception Group Traceback (most recent call last):', f' | File "{__file__}", line {lno_g+16}, in _get_exception_group', f' | raise ExceptionGroup("eg1", [exc1, exc2])', - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^', f' | ExceptionGroup: eg1 (2 sub-exceptions)', f' +-+---------------- 1 ----------------', f' | Traceback (most recent call last):', f' | File "{__file__}", line {lno_g+9}, in _get_exception_group', f' | f()', - f' | ^^^', f' | File "{__file__}", line {lno_f+1}, in f', f' | 1/0', f' | ~^~', @@ -2639,20 +2614,16 @@ def test_exception_group_format(self): f' | Traceback (most recent call last):', f' | File "{__file__}", line {lno_g+13}, in _get_exception_group', f' | g(42)', - f' | ^^^^^', f' | File "{__file__}", line {lno_g+1}, in g', f' | raise ValueError(v)', - f' | ^^^^^^^^^^^^^^^^^^^', f' | ValueError: 42', f' +------------------------------------', f' +---------------- 2 ----------------', f' | Traceback (most recent call last):', f' | File "{__file__}", line {lno_g+20}, in _get_exception_group', f' | g(24)', - f' | ^^^^^', f' | File "{__file__}", line {lno_g+1}, in g', f' | raise ValueError(v)', - f' | ^^^^^^^^^^^^^^^^^^^', f' | ValueError: 24', f' +------------------------------------', f''] diff --git a/Lib/traceback.py b/Lib/traceback.py index 3afe49d1d8a0e6..55f8080044053e 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -465,7 +465,8 @@ def format_frame_summary(self, frame_summary): row.append(' File "{}", line {}, in {}\n'.format( frame_summary.filename, frame_summary.lineno, frame_summary.name)) if frame_summary.line: - row.append(' {}\n'.format(frame_summary.line.strip())) + stripped_line = frame_summary.line.strip() + row.append(' {}\n'.format(stripped_line)) orig_line_len = len(frame_summary._original_line) frame_line_len = len(frame_summary.line.lstrip()) @@ -486,19 +487,22 @@ def format_frame_summary(self, frame_summary): frame_summary._original_line[colno - 1:end_colno - 1] ) else: - end_colno = stripped_characters + len(frame_summary.line.strip()) - - row.append(' ') - row.append(' ' * (colno - stripped_characters)) - - if anchors: - row.append(anchors.primary_char * (anchors.left_end_offset)) - row.append(anchors.secondary_char * (anchors.right_start_offset - anchors.left_end_offset)) - row.append(anchors.primary_char * (end_colno - colno - anchors.right_start_offset)) - else: - row.append('^' * (end_colno - colno)) + end_colno = stripped_characters + len(stripped_line) + + # show indicators if primary char doesn't span the frame line + if end_colno - colno < len(stripped_line) or ( + anchors and anchors.right_start_offset - anchors.left_end_offset > 0): + row.append(' ') + row.append(' ' * (colno - stripped_characters)) + + if anchors: + row.append(anchors.primary_char * (anchors.left_end_offset)) + row.append(anchors.secondary_char * (anchors.right_start_offset - anchors.left_end_offset)) + row.append(anchors.primary_char * (end_colno - colno - anchors.right_start_offset)) + else: + row.append('^' * (end_colno - colno)) - row.append('\n') + row.append('\n') if frame_summary.locals: for name, value in sorted(frame_summary.locals.items()): From bba12b12a605e1154c95f479e5fdea9d15f23acf Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Sun, 19 Jun 2022 14:23:09 +0900 Subject: [PATCH 02/13] fix test_idle --- Lib/idlelib/idle_test/test_run.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Lib/idlelib/idle_test/test_run.py b/Lib/idlelib/idle_test/test_run.py index d859ffc153fcdd..ec4637c5ca617a 100644 --- a/Lib/idlelib/idle_test/test_run.py +++ b/Lib/idlelib/idle_test/test_run.py @@ -3,7 +3,7 @@ from idlelib import run import io import sys -from test.support import captured_output, captured_stderr, has_no_debug_ranges +from test.support import captured_output, captured_stderr import unittest from unittest import mock import idlelib @@ -33,14 +33,9 @@ def __eq__(self, other): run.print_exception() tb = output.getvalue().strip().splitlines() - if has_no_debug_ranges(): - self.assertEqual(11, len(tb)) - self.assertIn('UnhashableException: ex2', tb[3]) - self.assertIn('UnhashableException: ex1', tb[10]) - else: - self.assertEqual(13, len(tb)) - self.assertIn('UnhashableException: ex2', tb[4]) - self.assertIn('UnhashableException: ex1', tb[12]) + self.assertEqual(11, len(tb)) + self.assertIn('UnhashableException: ex2', tb[3]) + self.assertIn('UnhashableException: ex1', tb[10]) data = (('1/0', ZeroDivisionError, "division by zero\n"), ('abc', NameError, "name 'abc' is not defined. " From 6a8f56d92e4fdb432411ed747d08a32932f8e995 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Thu, 23 Jun 2022 23:18:56 +0900 Subject: [PATCH 03/13] traceback.c: fix doc typo; simplify expression --- Python/traceback.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Python/traceback.c b/Python/traceback.c index 439689b32aea8a..19b726c81979fa 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -736,7 +736,7 @@ print_error_location_carets(PyObject *f, int offset, Py_ssize_t start_offset, Py int special_chars = (left_end_offset != -1 || right_start_offset != -1); const char *str; while (++offset <= end_offset) { - if (offset <= start_offset || offset > end_offset) { + if (offset <= start_offset) { str = " "; } else if (special_chars && left_end_offset < offset && offset <= right_start_offset) { str = secondary; @@ -813,7 +813,7 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen // // ERROR LINE ERROR LINE ERROR LINE ERROR LINE ERROR LINE ERROR LINE ERROR LINE // ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^~~~~~~~~~~~~~~~~~~~ - // | |-> left_end_offset | |-> left_offset + // | |-> left_end_offset | |-> end_offset // |-> start_offset |-> right_start_offset // // In general we will only have (start_offset, end_offset) but we can gather more information From 94be1862575d50422057e49d6818c8335aeca1f8 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Thu, 23 Jun 2022 23:41:19 +0900 Subject: [PATCH 04/13] traceback.c implementation --- Lib/test/test_traceback.py | 127 +++++++++++++++++++------------------ Python/traceback.c | 11 +++- 2 files changed, 74 insertions(+), 64 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 7cac4a9fd31e72..046507bbf821b3 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -252,7 +252,7 @@ def do_test(firstlines, message, charset, lineno): self.assertTrue(stdout[2].endswith(err_line), "Invalid traceback line: {0!r} instead of {1!r}".format( stdout[2], err_line)) - actual_err_msg = stdout[3 if has_no_debug_ranges() else 4] + actual_err_msg = stdout[3] self.assertTrue(actual_err_msg == err_msg, "Invalid error message: {0!r} instead of {1!r}".format( actual_err_msg, err_msg)) @@ -704,23 +704,23 @@ class A: pass ) self.assertEqual(result_lines, expected_error.splitlines()) -# @cpython_only -# @requires_debug_ranges() -# class CPythonTracebackErrorCaretTests(TracebackErrorLocationCaretTests): -# """ -# Same set of tests as above but with Python's internal traceback printing. -# """ -# def get_exception(self, callable): -# from _testcapi import exception_print -# try: -# callable() -# self.fail("No exception thrown.") -# except Exception as e: -# with captured_output("stderr") as tbstderr: -# exception_print(e) -# return tbstderr.getvalue().splitlines()[:-1] -# -# callable_line = get_exception.__code__.co_firstlineno + 3 +@cpython_only +@requires_debug_ranges() +class CPythonTracebackErrorCaretTests(TracebackErrorLocationCaretTests): + """ + Same set of tests as above but with Python's internal traceback printing. + """ + def get_exception(self, callable): + from _testcapi import exception_print + try: + callable() + self.fail("No exception thrown.") + except Exception as e: + with captured_output("stderr") as tbstderr: + exception_print(e) + return tbstderr.getvalue().splitlines()[:-1] + + callable_line = get_exception.__code__.co_firstlineno + 3 class TracebackFormatTests(unittest.TestCase): @@ -1015,14 +1015,14 @@ def h(count=10): def test_recursive_traceback_python(self): self._check_recursive_traceback_display(traceback.print_exc) - # @cpython_only - # @requires_debug_ranges() - # def test_recursive_traceback_cpython_internal(self): - # from _testcapi import exception_print - # def render_exc(): - # exc_type, exc_value, exc_tb = sys.exc_info() - # exception_print(exc_value) - # self._check_recursive_traceback_display(render_exc) + @cpython_only + @requires_debug_ranges() + def test_recursive_traceback_cpython_internal(self): + from _testcapi import exception_print + def render_exc(): + exc_type, exc_value, exc_tb = sys.exc_info() + exception_print(exc_value) + self._check_recursive_traceback_display(render_exc) def test_format_stack(self): def fmt(): @@ -1058,16 +1058,10 @@ def __eq__(self, other): exception_print(exc_val) tb = stderr_f.getvalue().strip().splitlines() - if has_no_debug_ranges(): - self.assertEqual(11, len(tb)) - self.assertEqual(context_message.strip(), tb[5]) - self.assertIn('UnhashableException: ex2', tb[3]) - self.assertIn('UnhashableException: ex1', tb[10]) - else: - self.assertEqual(13, len(tb)) - self.assertEqual(context_message.strip(), tb[6]) - self.assertIn('UnhashableException: ex2', tb[4]) - self.assertIn('UnhashableException: ex1', tb[12]) + self.assertEqual(11, len(tb)) + self.assertEqual(context_message.strip(), tb[5]) + self.assertIn('UnhashableException: ex2', tb[3]) + self.assertIn('UnhashableException: ex1', tb[10]) def deep_eg(self): e = TypeError(1) @@ -1451,15 +1445,17 @@ def __str__(self): # #### Exception Groups #### def test_exception_group_basic(self): + self.maxDiff = None def exc(): - raise ExceptionGroup("eg", [ValueError(1), TypeError(2)]) + if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)]) expected = ( f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | exception_or_callable()\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\n' - f' | raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n' + f' | if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: eg (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 1\n' @@ -1474,13 +1470,14 @@ def test_exception_group_cause(self): def exc(): EG = ExceptionGroup try: - raise EG("eg1", [ValueError(1), TypeError(2)]) + if True: raise EG("eg1", [ValueError(1), TypeError(2)]) except Exception as e: raise EG("eg2", [ValueError(3), TypeError(4)]) from e expected = (f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 3}, in exc\n' - f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n' + f' | if True: raise EG("eg1", [ValueError(1), TypeError(2)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: eg1 (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 1\n' @@ -1510,7 +1507,7 @@ def exc(): EG = ExceptionGroup try: try: - raise EG("eg1", [ValueError(1), TypeError(2)]) + if True: raise EG("eg1", [ValueError(1), TypeError(2)]) except: raise EG("eg2", [ValueError(3), TypeError(4)]) except: @@ -1519,7 +1516,8 @@ def exc(): expected = ( f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 4}, in exc\n' - f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n' + f' | if True: raise EG("eg1", [ValueError(1), TypeError(2)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: eg1 (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 1\n' @@ -1558,7 +1556,7 @@ def exc(): TE = TypeError try: try: - raise EG("nested", [TE(2), TE(3)]) + if True: raise EG("nested", [TE(2), TE(3)]) except Exception as e: exc = e raise EG("eg", [VE(1), exc, VE(4)]) @@ -1574,7 +1572,8 @@ def exc(): f' +---------------- 2 ----------------\n' f' | Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 6}, in exc\n' - f' | raise EG("nested", [TE(2), TE(3)])\n' + f' | if True: raise EG("nested", [TE(2), TE(3)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ExceptionGroup: nested (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | TypeError: 2\n' @@ -1732,7 +1731,7 @@ def exc(): excs = [] for msg in ['bad value', 'terrible value']: try: - raise ValueError(msg) + if True: raise ValueError(msg) except ValueError as e: e.add_note(f'the {msg}') excs.append(e) @@ -1761,13 +1760,15 @@ def exc(): f' +-+---------------- 1 ----------------\n' f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' - f' | raise ValueError(msg)\n' + f' | if True: raise ValueError(msg)\n' + f' | ^^^^^^^^^^^^^^^^^^^^^\n' f' | ValueError: bad value\n' f' | the bad value\n' f' +---------------- 2 ----------------\n' f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' - f' | raise ValueError(msg)\n' + f' | if True: raise ValueError(msg)\n' + f' | ^^^^^^^^^^^^^^^^^^^^^\n' f' | ValueError: terrible value\n' f' | the terrible value\n' f' +------------------------------------\n') @@ -1781,7 +1782,7 @@ def exc(): excs = [] for msg in ['bad value', 'terrible value']: try: - raise ValueError(msg) + if True: raise ValueError(msg) except ValueError as e: e.add_note(f'the {msg}') e.add_note(f'Goodbye {msg}') @@ -1813,14 +1814,16 @@ def exc(): f' +-+---------------- 1 ----------------\n' f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' - f' | raise ValueError(msg)\n' + f' | if True: raise ValueError(msg)\n' + f' | ^^^^^^^^^^^^^^^^^^^^^\n' f' | ValueError: bad value\n' f' | the bad value\n' f' | Goodbye bad value\n' f' +---------------- 2 ----------------\n' f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' - f' | raise ValueError(msg)\n' + f' | if True: raise ValueError(msg)\n' + f' | ^^^^^^^^^^^^^^^^^^^^^\n' f' | ValueError: terrible value\n' f' | the terrible value\n' f' | Goodbye terrible value\n' @@ -1871,18 +1874,18 @@ def get_report(self, e): return s -# class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): -# # -# # This checks built-in reporting by the interpreter. -# # -# -# @cpython_only -# def get_report(self, e): -# from _testcapi import exception_print -# e = self.get_exception(e) -# with captured_output("stderr") as s: -# exception_print(e) -# return s.getvalue() +class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): + # + # This checks built-in reporting by the interpreter. + # + + @cpython_only + def get_report(self, e): + from _testcapi import exception_print + e = self.get_exception(e) + with captured_output("stderr") as s: + exception_print(e) + return s.getvalue() class LimitTests(unittest.TestCase): diff --git a/Python/traceback.c b/Python/traceback.c index 19b726c81979fa..3930cba8205af1 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -592,7 +592,6 @@ _Py_DisplaySourceLine(PyObject *f, PyObject *filename, int lineno, int indent, * Traceback (most recent call last): * File "/home/isidentical/cpython/cpython/t.py", line 10, in * add_values(1, 2, 'x', 3, 4) - * ^^^^^^^^^^^^^^^^^^^^^^^^^^^ * File "/home/isidentical/cpython/cpython/t.py", line 2, in add_values * return a + b + c + d + e * ~~~~~~^~~ @@ -792,6 +791,7 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen int code_offset = tb->tb_lasti; PyCodeObject* code = frame->f_frame->f_code; + const Py_ssize_t source_line_len = PyUnicode_GET_LENGTH(source_line); int start_line; int end_line; @@ -859,7 +859,7 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen goto done; } - Py_ssize_t i = PyUnicode_GET_LENGTH(source_line); + Py_ssize_t i = source_line_len; while (--i >= 0) { if (!IS_WHITESPACE(source_line_str[i])) { break; @@ -869,6 +869,13 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen end_offset = i + 1; } + // elide indicators if primary char spans the frame line + Py_ssize_t stripped_line_len = source_line_len - truncation - _TRACEBACK_SOURCE_LINE_INDENT; + if (end_offset - start_offset == stripped_line_len && + left_end_offset == -1 && right_start_offset == -1) { + goto done; + } + if (_Py_WriteIndentedMargin(margin_indent, margin, f) < 0) { err = -1; goto done; From c06257b61f653169f3b13c8b58610d7aff4cd78c Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Fri, 24 Jun 2022 22:29:24 +0900 Subject: [PATCH 05/13] fix test_cmd_line_script --- Lib/test/test_cmd_line_script.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index d783af65839ad9..9e98edf2146ca9 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -549,10 +549,10 @@ def test_pep_409_verbiage(self): script_name = _make_test_script(script_dir, 'script', script) exitcode, stdout, stderr = assert_python_failure(script_name) text = stderr.decode('ascii').split('\n') - self.assertEqual(len(text), 6) + self.assertEqual(len(text), 5) self.assertTrue(text[0].startswith('Traceback')) self.assertTrue(text[1].startswith(' File ')) - self.assertTrue(text[4].startswith('NameError')) + self.assertTrue(text[3].startswith('NameError')) def test_non_ascii(self): # Mac OS X denies the creation of a file with an invalid UTF-8 name. From 87fafa6fbd749b111eba9ea9ff42e23cfd5e68c2 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 24 Jun 2022 14:06:23 +0000 Subject: [PATCH 06/13] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2022-06-24-14-06-20.gh-issue-93883.8jVQQ4.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-06-24-14-06-20.gh-issue-93883.8jVQQ4.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-06-24-14-06-20.gh-issue-93883.8jVQQ4.rst b/Misc/NEWS.d/next/Core and Builtins/2022-06-24-14-06-20.gh-issue-93883.8jVQQ4.rst new file mode 100644 index 00000000000000..53345577036a5a --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-06-24-14-06-20.gh-issue-93883.8jVQQ4.rst @@ -0,0 +1 @@ +Revise the display strategy of traceback enhanced error locations. The indicators are only shown when the location doesn't span the whole line. From a64635c878e532585b19234b1525d84d6359c200 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Fri, 24 Jun 2022 22:40:11 +0900 Subject: [PATCH 07/13] remove stray maxDiff from test --- Lib/test/test_traceback.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 046507bbf821b3..b5d5644cfc62dc 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1445,7 +1445,6 @@ def __str__(self): # #### Exception Groups #### def test_exception_group_basic(self): - self.maxDiff = None def exc(): if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)]) From 361da86601904784f7b6505ce8f014954598ae6b Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Fri, 24 Jun 2022 22:57:36 +0900 Subject: [PATCH 08/13] change testing strategy Rather than going out of our way to provide indicator coverage in every traceback test suite, the indicator test suite should be responible for sufficient coverage (e.g. by adding a basic exception group test to ensure that margin strings are covered). --- Lib/test/test_traceback.py | 127 ++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 65 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index b5d5644cfc62dc..3e24b03047f432 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -625,6 +625,28 @@ def f_with_subscript(): result_lines = self.get_exception(f_with_subscript) self.assertEqual(result_lines, expected_error.splitlines()) + def test_caret_exception_group(self): + # especially, this covers whether indicators handle margin strings correctly + + def exc(): + if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)]) + + expected_error = ( + f' + Exception Group Traceback (most recent call last):\n' + f' | File "{__file__}", line {self.callable_line}, in get_exception\n' + f' | callable()\n' + f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\n' + f' | if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n' + f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | ExceptionGroup: eg (2 sub-exceptions)\n' + f' +-+---------------- 1 ----------------\n' + f' | ValueError: 1\n' + f' +---------------- 2 ----------------\n' + f' | TypeError: 2\n') + + result_lines = self.get_exception(exc) + self.assertEqual(result_lines, expected_error.splitlines()) + def assertSpecialized(self, func, expected_specialization): result_lines = self.get_exception(func) specialization_line = result_lines[-1] @@ -726,13 +748,13 @@ def get_exception(self, callable): class TracebackFormatTests(unittest.TestCase): def some_exception(self): - if True: raise KeyError('blah') + raise KeyError('blah') @cpython_only def check_traceback_format(self, cleanup_func=None): from _testcapi import traceback_print try: - if True: self.some_exception() + self.some_exception() except KeyError: type_, value, tb = sys.exc_info() if cleanup_func is not None: @@ -767,15 +789,11 @@ def check_traceback_format(self, cleanup_func=None): # Make sure that the traceback is properly indented. tb_lines = python_fmt.splitlines() banner = tb_lines[0] - if has_no_debug_ranges(): - self.assertEqual(len(tb_lines), 5) - location, source_line = tb_lines[-2], tb_lines[-1] - else: - self.assertEqual(len(tb_lines), 7) - location, source_line = tb_lines[-3], tb_lines[-2] + self.assertEqual(len(tb_lines), 5) + location, source_line = tb_lines[-2], tb_lines[-1] self.assertTrue(banner.startswith('Traceback')) self.assertTrue(location.startswith(' File')) - self.assertTrue(source_line.startswith(' if True: raise')) + self.assertTrue(source_line.startswith(' raise')) def test_traceback_format(self): self.check_traceback_format() @@ -821,11 +839,11 @@ def _check_recursive_traceback_display(self, render_exc): # Check hitting the recursion limit def f(): - if True: f() + f() with captured_output("stderr") as stderr_f: try: - if True: f() + f() except RecursionError: render_exc() else: @@ -835,17 +853,13 @@ def f(): result_f = ( 'Traceback (most recent call last):\n' f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' - ' if True: f()\n' - ' ^^^\n' + ' f()\n' f' File "{__file__}", line {lineno_f+1}, in f\n' - ' if True: f()\n' - ' ^^^\n' + ' f()\n' f' File "{__file__}", line {lineno_f+1}, in f\n' - ' if True: f()\n' - ' ^^^\n' + ' f()\n' f' File "{__file__}", line {lineno_f+1}, in f\n' - ' if True: f()\n' - ' ^^^\n' + ' f()\n' # XXX: The following line changes depending on whether the tests # are run through the interactive interpreter or with -m # It also varies depending on the platform (stack size) @@ -876,7 +890,7 @@ def g(count=10): with captured_output("stderr") as stderr_g: try: - if True: g() + g() except ValueError: render_exc() else: @@ -901,8 +915,7 @@ def g(count=10): tb_line = ( 'Traceback (most recent call last):\n' f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n' - ' if True: g()\n' - ' ^^^\n' + ' g()\n' ) expected = (tb_line + result_g).splitlines() actual = stderr_g.getvalue().splitlines() @@ -912,11 +925,11 @@ def g(count=10): def h(count=10): if count: return h(count-1) - if True: g() + g() with captured_output("stderr") as stderr_h: try: - if True: h() + h() except ValueError: render_exc() else: @@ -926,8 +939,7 @@ def h(count=10): result_h = ( 'Traceback (most recent call last):\n' f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n' - ' if True: h()\n' - ' ^^^\n' + ' h()\n' f' File "{__file__}", line {lineno_h+2}, in h\n' ' return h(count-1)\n' ' ^^^^^^^^^^\n' @@ -939,8 +951,7 @@ def h(count=10): ' ^^^^^^^^^^\n' ' [Previous line repeated 7 more times]\n' f' File "{__file__}", line {lineno_h+3}, in h\n' - ' if True: g()\n' - ' ^^^\n' + ' g()\n' ) expected = (result_h + result_g).splitlines() actual = stderr_h.getvalue().splitlines() @@ -949,7 +960,7 @@ def h(count=10): # Check the boundary conditions. First, test just below the cutoff. with captured_output("stderr") as stderr_g: try: - if True: g(traceback._RECURSIVE_CUTOFF) + g(traceback._RECURSIVE_CUTOFF) except ValueError: render_exc() else: @@ -970,9 +981,8 @@ def h(count=10): ) tb_line = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lineno_g+80}, in _check_recursive_traceback_display\n' - ' if True: g(traceback._RECURSIVE_CUTOFF)\n' - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' File "{__file__}", line {lineno_g+77}, in _check_recursive_traceback_display\n' + ' g(traceback._RECURSIVE_CUTOFF)\n' ) expected = (tb_line + result_g).splitlines() actual = stderr_g.getvalue().splitlines() @@ -981,7 +991,7 @@ def h(count=10): # Second, test just above the cutoff. with captured_output("stderr") as stderr_g: try: - if True: g(traceback._RECURSIVE_CUTOFF + 1) + g(traceback._RECURSIVE_CUTOFF + 1) except ValueError: render_exc() else: @@ -1003,9 +1013,8 @@ def h(count=10): ) tb_line = ( 'Traceback (most recent call last):\n' - f' File "{__file__}", line {lineno_g+112}, in _check_recursive_traceback_display\n' - ' if True: g(traceback._RECURSIVE_CUTOFF + 1)\n' - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' File "{__file__}", line {lineno_g+108}, in _check_recursive_traceback_display\n' + ' g(traceback._RECURSIVE_CUTOFF + 1)\n' ) expected = (tb_line + result_g).splitlines() actual = stderr_g.getvalue().splitlines() @@ -1193,16 +1202,12 @@ def test_context_suppression(self): try: raise Exception except: - if True: raise ZeroDivisionError from None + raise ZeroDivisionError from None except ZeroDivisionError as _: e = _ lines = self.get_report(e).splitlines() - if has_no_debug_ranges(): - self.assertEqual(len(lines), 4) - self.assertTrue(lines[3].startswith('ZeroDivisionError')) - else: - self.assertEqual(len(lines), 5) - self.assertTrue(lines[4].startswith('ZeroDivisionError')) + self.assertEqual(len(lines), 4) + self.assertTrue(lines[3].startswith('ZeroDivisionError')) self.assertTrue(lines[0].startswith('Traceback')) self.assertTrue(lines[1].startswith(' File')) self.assertIn('ZeroDivisionError from None', lines[2]) @@ -1446,15 +1451,14 @@ def __str__(self): def test_exception_group_basic(self): def exc(): - if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)]) + raise ExceptionGroup("eg", [ValueError(1), TypeError(2)]) expected = ( f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | exception_or_callable()\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\n' - f' | if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n' f' | ExceptionGroup: eg (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 1\n' @@ -1469,14 +1473,13 @@ def test_exception_group_cause(self): def exc(): EG = ExceptionGroup try: - if True: raise EG("eg1", [ValueError(1), TypeError(2)]) + raise EG("eg1", [ValueError(1), TypeError(2)]) except Exception as e: raise EG("eg2", [ValueError(3), TypeError(4)]) from e expected = (f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 3}, in exc\n' - f' | if True: raise EG("eg1", [ValueError(1), TypeError(2)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n' f' | ExceptionGroup: eg1 (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 1\n' @@ -1506,7 +1509,7 @@ def exc(): EG = ExceptionGroup try: try: - if True: raise EG("eg1", [ValueError(1), TypeError(2)]) + raise EG("eg1", [ValueError(1), TypeError(2)]) except: raise EG("eg2", [ValueError(3), TypeError(4)]) except: @@ -1515,8 +1518,7 @@ def exc(): expected = ( f' + Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 4}, in exc\n' - f' | if True: raise EG("eg1", [ValueError(1), TypeError(2)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | raise EG("eg1", [ValueError(1), TypeError(2)])\n' f' | ExceptionGroup: eg1 (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | ValueError: 1\n' @@ -1555,7 +1557,7 @@ def exc(): TE = TypeError try: try: - if True: raise EG("nested", [TE(2), TE(3)]) + raise EG("nested", [TE(2), TE(3)]) except Exception as e: exc = e raise EG("eg", [VE(1), exc, VE(4)]) @@ -1571,8 +1573,7 @@ def exc(): f' +---------------- 2 ----------------\n' f' | Exception Group Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 6}, in exc\n' - f' | if True: raise EG("nested", [TE(2), TE(3)])\n' - f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + f' | raise EG("nested", [TE(2), TE(3)])\n' f' | ExceptionGroup: nested (2 sub-exceptions)\n' f' +-+---------------- 1 ----------------\n' f' | TypeError: 2\n' @@ -1730,7 +1731,7 @@ def exc(): excs = [] for msg in ['bad value', 'terrible value']: try: - if True: raise ValueError(msg) + raise ValueError(msg) except ValueError as e: e.add_note(f'the {msg}') excs.append(e) @@ -1759,15 +1760,13 @@ def exc(): f' +-+---------------- 1 ----------------\n' f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' - f' | if True: raise ValueError(msg)\n' - f' | ^^^^^^^^^^^^^^^^^^^^^\n' + f' | raise ValueError(msg)\n' f' | ValueError: bad value\n' f' | the bad value\n' f' +---------------- 2 ----------------\n' f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' - f' | if True: raise ValueError(msg)\n' - f' | ^^^^^^^^^^^^^^^^^^^^^\n' + f' | raise ValueError(msg)\n' f' | ValueError: terrible value\n' f' | the terrible value\n' f' +------------------------------------\n') @@ -1781,7 +1780,7 @@ def exc(): excs = [] for msg in ['bad value', 'terrible value']: try: - if True: raise ValueError(msg) + raise ValueError(msg) except ValueError as e: e.add_note(f'the {msg}') e.add_note(f'Goodbye {msg}') @@ -1813,16 +1812,14 @@ def exc(): f' +-+---------------- 1 ----------------\n' f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' - f' | if True: raise ValueError(msg)\n' - f' | ^^^^^^^^^^^^^^^^^^^^^\n' + f' | raise ValueError(msg)\n' f' | ValueError: bad value\n' f' | the bad value\n' f' | Goodbye bad value\n' f' +---------------- 2 ----------------\n' f' | Traceback (most recent call last):\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\n' - f' | if True: raise ValueError(msg)\n' - f' | ^^^^^^^^^^^^^^^^^^^^^\n' + f' | raise ValueError(msg)\n' f' | ValueError: terrible value\n' f' | the terrible value\n' f' | Goodbye terrible value\n' From d39233214971008add86deae1b92e4c4ac9473f6 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Fri, 24 Jun 2022 23:22:40 +0900 Subject: [PATCH 09/13] update doc examples --- Doc/library/traceback.rst | 13 ++++--------- Doc/whatsnew/3.11.rst | 1 - 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 796309c6cf0bb9..a8412cc93d16f9 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -464,32 +464,27 @@ The output for the example would look similar to this: *** print_tb: File "", line 10, in lumberjack() - ^^^^^^^^^^^^ *** print_exception: Traceback (most recent call last): File "", line 10, in lumberjack() - ^^^^^^^^^^^^ File "", line 4, in lumberjack bright_side_of_death() - ^^^^^^^^^^^^^^^^^^^^^^ IndexError: tuple index out of range *** print_exc: Traceback (most recent call last): File "", line 10, in lumberjack() - ^^^^^^^^^^^^ File "", line 4, in lumberjack bright_side_of_death() - ^^^^^^^^^^^^^^^^^^^^^^ IndexError: tuple index out of range *** format_exc, first and last line: Traceback (most recent call last): IndexError: tuple index out of range *** format_exception: ['Traceback (most recent call last):\n', - ' File "", line 10, in \n lumberjack()\n ^^^^^^^^^^^^\n', - ' File "", line 4, in lumberjack\n bright_side_of_death()\n ^^^^^^^^^^^^^^^^^^^^^^\n', + ' File "", line 10, in \n lumberjack()\n', + ' File "", line 4, in lumberjack\n bright_side_of_death()\n', ' File "", line 7, in bright_side_of_death\n return tuple()[0]\n ~~~~~~~^^^\n', 'IndexError: tuple index out of range\n'] *** extract_tb: @@ -497,8 +492,8 @@ The output for the example would look similar to this: , line 4 in lumberjack>, , line 7 in bright_side_of_death>] *** format_tb: - [' File "", line 10, in \n lumberjack()\n ^^^^^^^^^^^^\n', - ' File "", line 4, in lumberjack\n bright_side_of_death()\n ^^^^^^^^^^^^^^^^^^^^^^\n', + [' File "", line 10, in \n lumberjack()\n', + ' File "", line 4, in lumberjack\n bright_side_of_death()\n', ' File "", line 7, in bright_side_of_death\n return tuple()[0]\n ~~~~~~~^^^\n'] *** tb_lineno: 10 diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 256d5822e5196e..5d9c05f4bb2474 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -117,7 +117,6 @@ when dealing with deeply nested dictionary objects and multiple function calls, Traceback (most recent call last): File "query.py", line 37, in magic_arithmetic('foo') - ^^^^^^^^^^^^^^^^^^^^^^^ File "query.py", line 18, in magic_arithmetic return add_counts(x) / 25 ^^^^^^^^^^^^^ From f6927cd6049ba0cfac38f85a85b50376a5b9989c Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Tue, 28 Jun 2022 09:03:39 +0900 Subject: [PATCH 10/13] Apply suggestions: doc formatting Co-authored-by: Pablo Galindo Salgado --- Python/traceback.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/traceback.c b/Python/traceback.c index 3930cba8205af1..594a4010e7d20b 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -869,7 +869,7 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen end_offset = i + 1; } - // elide indicators if primary char spans the frame line + // Elide indicators if primary char spans the frame line Py_ssize_t stripped_line_len = source_line_len - truncation - _TRACEBACK_SOURCE_LINE_INDENT; if (end_offset - start_offset == stripped_line_len && left_end_offset == -1 && right_start_offset == -1) { From 4ff4b77059b30d512b705d62e843caa251999b5a Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Tue, 28 Jun 2022 09:05:13 +0900 Subject: [PATCH 11/13] test_doctest: remove unneeded has_no_debug_ranges() --- Lib/test/test_doctest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py index 0d41f9360d6e23..65e215f1cdda4a 100644 --- a/Lib/test/test_doctest.py +++ b/Lib/test/test_doctest.py @@ -2854,7 +2854,7 @@ def test_testmod(): r""" # Skip the test: the filesystem encoding is unable to encode the filename supports_unicode = False -if supports_unicode and not support.has_no_debug_ranges(): +if supports_unicode: def test_unicode(): """ Check doctest with a non-ascii filename: From 28d95b4f9f91f669f6b4bdae45e5e0c5a8bdb9d5 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Tue, 28 Jun 2022 09:11:35 +0900 Subject: [PATCH 12/13] traceback.c: refactor elide check --- Python/traceback.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Python/traceback.c b/Python/traceback.c index 594a4010e7d20b..8234f71e6ce253 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -871,8 +871,8 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen // Elide indicators if primary char spans the frame line Py_ssize_t stripped_line_len = source_line_len - truncation - _TRACEBACK_SOURCE_LINE_INDENT; - if (end_offset - start_offset == stripped_line_len && - left_end_offset == -1 && right_start_offset == -1) { + bool has_secondary_ranges = (left_end_offset != -1 || right_start_offset != -1); + if (end_offset - start_offset == stripped_line_len && !has_secondary_ranges) { goto done; } From 8ac461ffc998e9c2103c240448b02e5615b9fc49 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Tue, 28 Jun 2022 09:05:33 +0900 Subject: [PATCH 13/13] doc additions: * explain "if True:" idiom for caret tests * document test_caret_exception_group() test * add eliding explanation to summary in traceback.c --- Lib/test/test_traceback.py | 6 +++++- Python/traceback.c | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 3e24b03047f432..119fd42df98a56 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -386,6 +386,8 @@ def get_exception(self, callable): callable_line = get_exception.__code__.co_firstlineno + 2 def test_basic_caret(self): + # NOTE: In caret tests, "if True:" is used as a way to force indicator + # display, since the raising expression spans only part of the line. def f(): if True: raise ValueError("basic caret tests") @@ -626,7 +628,9 @@ def f_with_subscript(): self.assertEqual(result_lines, expected_error.splitlines()) def test_caret_exception_group(self): - # especially, this covers whether indicators handle margin strings correctly + # Notably, this covers whether indicators handle margin strings correctly. + # (Exception groups use margin strings to display vertical indicators.) + # The implementation must account for both "indent" and "margin" offsets. def exc(): if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)]) diff --git a/Python/traceback.c b/Python/traceback.c index 8234f71e6ce253..de658b9103180e 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -822,6 +822,9 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen // the different ranges (primary_error_char and secondary_error_char). If we cannot obtain the // AST information or we cannot identify special ranges within it, then left_end_offset and // right_end_offset will be set to -1. + // + // To keep the column indicators pertinent, they are not shown when the primary character + // spans the whole line. // Convert the utf-8 byte offset to the actual character offset so we print the right number of carets. assert(source_line);