]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-93883: elide traceback indicators when possible (#93994)
authorJohn Belmonte <john@neggie.net>
Mon, 11 Jul 2022 06:40:53 +0000 (15:40 +0900)
committerGitHub <noreply@github.com>
Mon, 11 Jul 2022 06:40:53 +0000 (07:40 +0100)
* 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 <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
```

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).

Doc/library/traceback.rst
Doc/whatsnew/3.11.rst
Lib/idlelib/idle_test/test_run.py
Lib/test/test_cmd_line_script.py
Lib/test/test_doctest.py
Lib/test/test_traceback.py
Lib/traceback.py
Misc/NEWS.d/next/Core and Builtins/2022-06-24-14-06-20.gh-issue-93883.8jVQQ4.rst [new file with mode: 0644]
Python/traceback.c

index 796309c6cf0bb9eb96e0154058598be1fe639710..a8412cc93d16f95a356e5a63cabeac1f085b479b 100644 (file)
@@ -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
 
index 95991528444c7cf4d8ebf4015581cadde74d1936..f7274a8cea1589a338aa086079bfef10f05a7524 100644 (file)
@@ -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
                ^^^^^^^^^^^^^
index d859ffc153fcddcc064ad3932c4b8d5d79f246ea..ec4637c5ca617a9eb380d46825ee32c429520edc 100644 (file)
@@ -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 @@ class ExceptionTest(unittest.TestCase):
                         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. "
index d783af65839ad9d2aa8ccb24b9d9487e0b41acab..9e98edf2146ca942dcb3e1f70da783e5bb03e632 100644 (file)
@@ -549,10 +549,10 @@ class CmdLineTest(unittest.TestCase):
             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.
index 7c799697d9c225fdc20e80886773a8a16ffa73d9..65e215f1cdda4a6f8e5f098fd7e3019a04d0230a 100644 (file)
@@ -2854,7 +2854,7 @@ except UnicodeEncodeError:
     # 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 @@ Check doctest with a non-ascii filename:
         Traceback (most recent call last):
           File ...
             exec(compile(example.source, filename, "single",
-            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
           File "<doctest foo-bär@baz[0]>", line 1, in <module>
             raise Exception('clé')
-            ^^^^^^^^^^^^^^^^^^^^^^
         Exception: clé
     TestResults(failed=1, attempted=1)
     """
index 72d67bf6ef14071824fc8f28877fa5f003937328..f4161fbf0178dedecd34d593ff971ebeea2b9494 100644 (file)
@@ -252,7 +252,7 @@ class TracebackCases(unittest.TestCase):
             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))
@@ -386,18 +386,19 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
     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())
@@ -406,17 +407,16 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
         # 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 +431,6 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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 +442,7 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
         # 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 +451,9 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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 +482,6 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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 +498,8 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
         # lines.
         def f_with_multiline():
             return (
-                1 /
-                0 +
-                2
+                2 + 1 /
+                0
             )
 
         lineno_f = f_with_multiline.__code__.co_firstlineno
@@ -511,10 +507,9 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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 +524,6 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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 +541,6 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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 +558,6 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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 +581,6 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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'
@@ -597,7 +588,7 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
         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 +603,54 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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]
@@ -672,13 +704,11 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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,13 +722,11 @@ class TracebackErrorLocationCaretTests(unittest.TestCase):
             '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())
 
@@ -816,12 +844,8 @@ class TracebackFormatTests(unittest.TestCase):
         # 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'))
@@ -885,16 +909,12 @@ class TracebackFormatTests(unittest.TestCase):
             '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)
@@ -945,14 +965,12 @@ class TracebackFormatTests(unittest.TestCase):
             '  [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()
@@ -977,7 +995,6 @@ class TracebackFormatTests(unittest.TestCase):
             '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'
@@ -990,7 +1007,6 @@ class TracebackFormatTests(unittest.TestCase):
             '  [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()
@@ -1016,14 +1032,12 @@ class TracebackFormatTests(unittest.TestCase):
             '           ^^^^^^^^^^\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()
@@ -1050,14 +1064,12 @@ class TracebackFormatTests(unittest.TestCase):
             '  [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()
@@ -1110,16 +1122,10 @@ class TracebackFormatTests(unittest.TestCase):
             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)
@@ -1255,12 +1261,8 @@ class BaseExceptionReportingTests:
         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])
@@ -1510,10 +1512,8 @@ class BaseExceptionReportingTests:
              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'
@@ -1535,7 +1535,6 @@ class BaseExceptionReportingTests:
         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'
@@ -1548,10 +1547,8 @@ class BaseExceptionReportingTests:
                     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'
@@ -1577,7 +1574,6 @@ class BaseExceptionReportingTests:
              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'
@@ -1590,7 +1586,6 @@ class BaseExceptionReportingTests:
              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'
@@ -1603,10 +1598,8 @@ class BaseExceptionReportingTests:
              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)
@@ -1629,7 +1622,6 @@ class BaseExceptionReportingTests:
         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'
@@ -1637,7 +1629,6 @@ class BaseExceptionReportingTests:
                     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'
@@ -1653,10 +1644,8 @@ class BaseExceptionReportingTests:
                     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'
@@ -1814,10 +1803,8 @@ class BaseExceptionReportingTests:
         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'
@@ -1829,14 +1816,12 @@ class BaseExceptionReportingTests:
                     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')
@@ -1869,10 +1854,8 @@ class BaseExceptionReportingTests:
         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'
@@ -1885,7 +1868,6 @@ class BaseExceptionReportingTests:
                     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'
@@ -1893,7 +1875,6 @@ class BaseExceptionReportingTests:
                     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'
@@ -2669,19 +2650,16 @@ class TestTracebackException_ExceptionGroups(unittest.TestCase):
                     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'      |     ~^~',
@@ -2690,20 +2668,16 @@ class TestTracebackException_ExceptionGroups(unittest.TestCase):
                     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'']
index 3afe49d1d8a0e655f156621d2b960f436a19f538..55f8080044053ea06260c6a54cf50c5de0324d88 100644 (file)
@@ -465,7 +465,8 @@ class StackSummary(list):
         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 @@ class StackSummary(list):
                             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 (file)
index 0000000..5334557
--- /dev/null
@@ -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.
index 439689b32aea8ab833f72e50eaba700482064ba1..de658b9103180ea0eed5044f8ccdcf6558994067 100644 (file)
@@ -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;