[Python-checkins] [3.11] gh-93883: elide traceback indicators when possible (GH-93994) (GH-94740)

miss-islington webhook-mailer at python.org
Mon Jul 11 07:27:43 EDT 2022


https://github.com/python/cpython/commit/45896f2a02f02f296d14cccd6959179ccd47e410
commit: 45896f2a02f02f296d14cccd6959179ccd47e410
branch: 3.11
author: John Belmonte <john at neggie.net>
committer: miss-islington <31488909+miss-islington at users.noreply.github.com>
date: 2022-07-11T04:27:29-07:00
summary:

[3.11] gh-93883: elide traceback indicators when possible (GH-93994) (GH-94740)



Elide traceback column indicators when the entire line of the
frame is implicated.  This reduces traceback length and draws
more attention to the remaining (very relevant) indicators.

Example:
```
Traceback (most recent call last):
  File "query.py", line 99, in <module>
    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
```

Automerge-Triggered-By: GH:pablogsal

files:
A Misc/NEWS.d/next/Core and Builtins/2022-06-24-14-06-20.gh-issue-93883.8jVQQ4.rst
M Doc/library/traceback.rst
M Doc/whatsnew/3.11.rst
M Lib/idlelib/idle_test/test_run.py
M Lib/test/test_cmd_line_script.py
M Lib/test/test_doctest.py
M Lib/test/test_traceback.py
M Lib/traceback.py
M Python/traceback.c

diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst
index 796309c6cf0bb..a8412cc93d16f 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 "<doctest...>", line 10, in <module>
        lumberjack()
-       ^^^^^^^^^^^^
    *** print_exception:
    Traceback (most recent call last):
      File "<doctest...>", line 10, in <module>
        lumberjack()
-       ^^^^^^^^^^^^
      File "<doctest...>", line 4, in lumberjack
        bright_side_of_death()
-       ^^^^^^^^^^^^^^^^^^^^^^
    IndexError: tuple index out of range
    *** print_exc:
    Traceback (most recent call last):
      File "<doctest...>", line 10, in <module>
        lumberjack()
-       ^^^^^^^^^^^^
      File "<doctest...>", 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 "<doctest default[0]>", line 10, in <module>\n    lumberjack()\n    ^^^^^^^^^^^^\n',
-    '  File "<doctest default[0]>", line 4, in lumberjack\n    bright_side_of_death()\n    ^^^^^^^^^^^^^^^^^^^^^^\n',
+    '  File "<doctest default[0]>", line 10, in <module>\n    lumberjack()\n',
+    '  File "<doctest default[0]>", line 4, in lumberjack\n    bright_side_of_death()\n',
     '  File "<doctest default[0]>", 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:
     <FrameSummary file <doctest...>, line 4 in lumberjack>,
     <FrameSummary file <doctest...>, line 7 in bright_side_of_death>]
    *** format_tb:
-   ['  File "<doctest default[0]>", line 10, in <module>\n    lumberjack()\n    ^^^^^^^^^^^^\n',
-    '  File "<doctest default[0]>", line 4, in lumberjack\n    bright_side_of_death()\n    ^^^^^^^^^^^^^^^^^^^^^^\n',
+   ['  File "<doctest default[0]>", line 10, in <module>\n    lumberjack()\n',
+    '  File "<doctest default[0]>", line 4, in lumberjack\n    bright_side_of_death()\n',
     '  File "<doctest default[0]>", 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 97ad5b3fd5ca6..a5b9c31afac25 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 <module>
         magic_arithmetic('foo')
-        ^^^^^^^^^^^^^^^^^^^^^^^
       File "query.py", line 18, in magic_arithmetic
         return add_counts(x) / 25
                ^^^^^^^^^^^^^
diff --git a/Lib/idlelib/idle_test/test_run.py b/Lib/idlelib/idle_test/test_run.py
index d859ffc153fcd..ec4637c5ca617 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. "
diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py
index d783af65839ad..9e98edf2146ca 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.
diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py
index a4aab6cf4db3b..00aeacddc12ca 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:
 
@@ -2876,10 +2876,8 @@ def test_unicode(): """
         Traceback (most recent call last):
           File ...
             exec(compile(example.source, filename, "single",
-            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
           File "<doctest foo-bär at baz[0]>", line 1, in <module>
             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 40d5a84f92d99..94ccc3f48d3ea 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -253,7 +253,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))
@@ -387,18 +387,19 @@ 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():
-            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())
@@ -407,17 +408,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())
@@ -432,7 +432,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'
@@ -444,7 +443,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"
             )
 
@@ -453,10 +452,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())
@@ -485,7 +483,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'
@@ -502,9 +499,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
@@ -512,10 +508,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())
@@ -530,7 +525,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'
@@ -548,7 +542,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'
@@ -566,7 +559,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'
@@ -590,7 +582,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 <module>\n'
             "    1 $ 0 / 1 / 2\n"
             '    ^^^^^\n'
@@ -598,7 +589,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:
@@ -613,13 +604,54 @@ 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 <module>\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 test_caret_exception_group(self):
+        # 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)])
+
+        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]
@@ -673,13 +705,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())
 
@@ -693,13 +723,11 @@ 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())
 
@@ -713,7 +741,6 @@ def f():
             f"Traceback (most recent call last):",
             f"  File \"{__file__}\", line {self.callable_line}, in get_exception",
             f"    callable()",
-            f"    ^^^^^^^^^^",
             f"  File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f",
             f"    .method",
             f"     ^^^^^^",
@@ -730,10 +757,8 @@ def f():
             f"Traceback (most recent call last):",
             f"  File \"{__file__}\", line {self.callable_line}, in get_exception",
             f"    callable()",
-            f"    ^^^^^^^^^^",
             f"  File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f",
             f"    method",
-            f"    ^^^^^^",
         ]
         self.assertEqual(actual, expected)
 
@@ -747,7 +772,6 @@ def f():
             f"Traceback (most recent call last):",
             f"  File \"{__file__}\", line {self.callable_line}, in get_exception",
             f"    callable()",
-            f"    ^^^^^^^^^^",
             f"  File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f",
             f"    . method",
             f"      ^^^^^^",
@@ -817,12 +841,8 @@ 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('    raise'))
@@ -886,16 +906,12 @@ def f():
             'Traceback (most recent call last):\n'
             f'  File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
             '    f()\n'
-            '    ^^^\n'
             f'  File "{__file__}", line {lineno_f+1}, in f\n'
             '    f()\n'
-            '    ^^^\n'
             f'  File "{__file__}", line {lineno_f+1}, in f\n'
             '    f()\n'
-            '    ^^^\n'
             f'  File "{__file__}", line {lineno_f+1}, in f\n'
             '    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)
@@ -946,14 +962,12 @@ 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'
         )
         expected = (tb_line + result_g).splitlines()
         actual = stderr_g.getvalue().splitlines()
@@ -978,7 +992,6 @@ def h(count=10):
             'Traceback (most recent call last):\n'
             f'  File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n'
             '    h()\n'
-            '    ^^^\n'
             f'  File "{__file__}", line {lineno_h+2}, in h\n'
             '    return h(count-1)\n'
             '           ^^^^^^^^^^\n'
@@ -991,7 +1004,6 @@ def h(count=10):
             '  [Previous line repeated 7 more times]\n'
             f'  File "{__file__}", line {lineno_h+3}, in h\n'
             '    g()\n'
-            '    ^^^\n'
         )
         expected = (result_h + result_g).splitlines()
         actual = stderr_h.getvalue().splitlines()
@@ -1017,14 +1029,12 @@ 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'
+            f'  File "{__file__}", line {lineno_g+77}, in _check_recursive_traceback_display\n'
             '    g(traceback._RECURSIVE_CUTOFF)\n'
-            '    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
         )
         expected = (tb_line + result_g).splitlines()
         actual = stderr_g.getvalue().splitlines()
@@ -1051,14 +1061,12 @@ 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'
+            f'  File "{__file__}", line {lineno_g+108}, in _check_recursive_traceback_display\n'
             '    g(traceback._RECURSIVE_CUTOFF + 1)\n'
-            '    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
         )
         expected = (tb_line + result_g).splitlines()
         actual = stderr_g.getvalue().splitlines()
@@ -1111,16 +1119,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)
@@ -1256,12 +1258,8 @@ def test_context_suppression(self):
         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])
@@ -1511,10 +1509,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'
@@ -1536,7 +1532,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'
@@ -1549,10 +1544,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'
@@ -1578,7 +1571,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'
@@ -1591,7 +1583,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'
@@ -1604,10 +1595,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)
@@ -1630,7 +1619,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'
@@ -1638,7 +1626,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'
@@ -1654,10 +1641,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'
@@ -1815,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 + 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'
@@ -1830,14 +1813,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')
@@ -1870,10 +1851,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'
@@ -1886,7 +1865,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'
@@ -1894,7 +1872,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'
@@ -2670,19 +2647,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'      |     ~^~',
@@ -2691,20 +2665,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 3afe49d1d8a0e..55f8080044053 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()):
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 0000000000000..53345577036a5
--- /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.
diff --git a/Python/traceback.c b/Python/traceback.c
index 0c49a8c20a7f5..20348c06fd802 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 <module>
  *      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
  *             ~~~~~~^~~
@@ -736,7 +735,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;
@@ -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;
@@ -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
@@ -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);
@@ -859,7 +862,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 +872,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;
+    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;
+    }
+
     if (_Py_WriteIndentedMargin(margin_indent, margin, f) < 0) {
         err = -1;
         goto done;



More information about the Python-checkins mailing list