gh-106922: Support multi-line error locations in traceback (attempt 2) (#112097)

This commit is contained in:
William Wen 2023-12-01 14:18:16 -08:00 committed by GitHub
parent 5c5022b862
commit 939fc6d6ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 706 additions and 124 deletions

View file

@ -523,27 +523,32 @@ The output for the example would look similar to this:
*** print_tb: *** print_tb:
File "<doctest...>", line 10, in <module> File "<doctest...>", line 10, in <module>
lumberjack() lumberjack()
~~~~~~~~~~^^
*** print_exception: *** print_exception:
Traceback (most recent call last): Traceback (most recent call last):
File "<doctest...>", line 10, in <module> File "<doctest...>", line 10, in <module>
lumberjack() lumberjack()
~~~~~~~~~~^^
File "<doctest...>", line 4, in lumberjack File "<doctest...>", line 4, in lumberjack
bright_side_of_life() bright_side_of_life()
~~~~~~~~~~~~~~~~~~~^^
IndexError: tuple index out of range IndexError: tuple index out of range
*** print_exc: *** print_exc:
Traceback (most recent call last): Traceback (most recent call last):
File "<doctest...>", line 10, in <module> File "<doctest...>", line 10, in <module>
lumberjack() lumberjack()
~~~~~~~~~~^^
File "<doctest...>", line 4, in lumberjack File "<doctest...>", line 4, in lumberjack
bright_side_of_life() bright_side_of_life()
~~~~~~~~~~~~~~~~~~~^^
IndexError: tuple index out of range IndexError: tuple index out of range
*** format_exc, first and last line: *** format_exc, first and last line:
Traceback (most recent call last): Traceback (most recent call last):
IndexError: tuple index out of range IndexError: tuple index out of range
*** format_exception: *** format_exception:
['Traceback (most recent call last):\n', ['Traceback (most recent call last):\n',
' File "<doctest default[0]>", line 10, in <module>\n lumberjack()\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_life()\n', ' File "<doctest default[0]>", line 4, in lumberjack\n bright_side_of_life()\n ~~~~~~~~~~~~~~~~~~~^^\n',
' File "<doctest default[0]>", line 7, in bright_side_of_life\n return tuple()[0]\n ~~~~~~~^^^\n', ' File "<doctest default[0]>", line 7, in bright_side_of_life\n return tuple()[0]\n ~~~~~~~^^^\n',
'IndexError: tuple index out of range\n'] 'IndexError: tuple index out of range\n']
*** extract_tb: *** extract_tb:
@ -551,8 +556,8 @@ The output for the example would look similar to this:
<FrameSummary file <doctest...>, line 4 in lumberjack>, <FrameSummary file <doctest...>, line 4 in lumberjack>,
<FrameSummary file <doctest...>, line 7 in bright_side_of_life>] <FrameSummary file <doctest...>, line 7 in bright_side_of_life>]
*** format_tb: *** format_tb:
[' File "<doctest default[0]>", line 10, in <module>\n lumberjack()\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_life()\n', ' File "<doctest default[0]>", line 4, in lumberjack\n bright_side_of_life()\n ~~~~~~~~~~~~~~~~~~~^^\n',
' File "<doctest default[0]>", line 7, in bright_side_of_life\n return tuple()[0]\n ~~~~~~~^^^\n'] ' File "<doctest default[0]>", line 7, in bright_side_of_life\n return tuple()[0]\n ~~~~~~~^^^\n']
*** tb_lineno: 10 *** tb_lineno: 10

View file

@ -2922,6 +2922,9 @@ Check doctest with a non-ascii filename:
Traceback (most recent call last): Traceback (most recent call last):
File ... File ...
exec(compile(example.source, filename, "single", exec(compile(example.source, filename, "single",
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
compileflags, True), test.globs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<doctest foo-bär@baz[0]>", line 1, in <module> File "<doctest foo-bär@baz[0]>", line 1, in <module>
raise Exception('clé') raise Exception('clé')
Exception: clé Exception: clé

View file

@ -2080,6 +2080,7 @@ class AssertionErrorTests(unittest.TestCase):
""", """,
[ [
' 1 < 2 and', ' 1 < 2 and',
' 3 > 4',
'AssertionError', 'AssertionError',
], ],
), ),
@ -2087,7 +2088,7 @@ class AssertionErrorTests(unittest.TestCase):
for source, expected in cases: for source, expected in cases:
with self.subTest(source): with self.subTest(source):
result = self.write_source(source) result = self.write_source(source)
self.assertEqual(result[-2:], expected) self.assertEqual(result[-len(expected):], expected)
class SyntaxErrorTests(unittest.TestCase): class SyntaxErrorTests(unittest.TestCase):

View file

@ -161,10 +161,11 @@ class TestInteractiveInterpreter(unittest.TestCase):
output = kill_python(p) output = kill_python(p)
self.assertEqual(p.returncode, 0) self.assertEqual(p.returncode, 0)
traceback_lines = output.splitlines()[-7:-1] traceback_lines = output.splitlines()[-8:-1]
expected_lines = [ expected_lines = [
' File "<stdin>", line 1, in <module>', ' File "<stdin>", line 1, in <module>',
' foo(0)', ' foo(0)',
' ~~~^^^',
' File "<stdin>", line 2, in foo', ' File "<stdin>", line 2, in foo',
' 1 / x', ' 1 / x',
' ~~^~~', ' ~~^~~',

View file

@ -1115,8 +1115,10 @@ class SysModuleTest(unittest.TestCase):
b'Traceback (most recent call last):', b'Traceback (most recent call last):',
b' File "<string>", line 8, in <module>', b' File "<string>", line 8, in <module>',
b' f2()', b' f2()',
b' ~~^^',
b' File "<string>", line 6, in f2', b' File "<string>", line 6, in f2',
b' f1()', b' f1()',
b' ~~^^',
b' File "<string>", line 4, in f1', b' File "<string>", line 4, in f1',
b' 1 / 0', b' 1 / 0',
b' ~~^~~', b' ~~^~~',
@ -1124,8 +1126,8 @@ class SysModuleTest(unittest.TestCase):
] ]
check(10, traceback) check(10, traceback)
check(3, traceback) check(3, traceback)
check(2, traceback[:1] + traceback[3:]) check(2, traceback[:1] + traceback[4:])
check(1, traceback[:1] + traceback[5:]) check(1, traceback[:1] + traceback[7:])
check(0, [traceback[-1]]) check(0, [traceback[-1]])
check(-1, [traceback[-1]]) check(-1, [traceback[-1]])
check(1<<1000, traceback) check(1<<1000, traceback)

View file

@ -578,6 +578,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+1}, in f\n' f' File "{__file__}", line {lineno_f+1}, in f\n'
' if True: raise ValueError("basic caret tests")\n' ' if True: raise ValueError("basic caret tests")\n'
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
@ -596,6 +597,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+1}, in f_with_unicode\n' f' File "{__file__}", line {lineno_f+1}, in f_with_unicode\n'
' if True: raise ValueError("Ĥellö Wörld")\n' ' if True: raise ValueError("Ĥellö Wörld")\n'
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
@ -613,6 +615,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+1}, in f_with_type\n' f' File "{__file__}", line {lineno_f+1}, in f_with_type\n'
' def foo(a: THIS_DOES_NOT_EXIST ) -> int:\n' ' def foo(a: THIS_DOES_NOT_EXIST ) -> int:\n'
' ^^^^^^^^^^^^^^^^^^^\n' ' ^^^^^^^^^^^^^^^^^^^\n'
@ -633,9 +636,14 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+1}, in f_with_multiline\n' f' File "{__file__}", line {lineno_f+1}, in f_with_multiline\n'
' if True: raise ValueError(\n' ' if True: raise ValueError(\n'
' ^^^^^^^^^^^^^^^^^' ' ^^^^^^^^^^^^^^^^^\n'
' "error over multiple lines"\n'
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
' )\n'
' ^'
) )
result_lines = self.get_exception(f_with_multiline) result_lines = self.get_exception(f_with_multiline)
self.assertEqual(result_lines, expected_f.splitlines()) self.assertEqual(result_lines, expected_f.splitlines())
@ -664,9 +672,10 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_multiline\n' f' File "{__file__}", line {lineno_f+2}, in f_with_multiline\n'
' return compile(code, "?", "exec")\n' ' return compile(code, "?", "exec")\n'
' ^^^^^^^^^^^^^^^^^^^^^^^^^^\n' ' ~~~~~~~^^^^^^^^^^^^^^^^^^^\n'
' File "?", line 7\n' ' File "?", line 7\n'
' foo(a, z\n' ' foo(a, z\n'
' ^' ' ^'
@ -689,9 +698,12 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_multiline\n' f' File "{__file__}", line {lineno_f+2}, in f_with_multiline\n'
' 2 + 1 /\n' ' 2 + 1 /\n'
' ^^^' ' ~~^\n'
' 0\n'
' ~'
) )
result_lines = self.get_exception(f_with_multiline) result_lines = self.get_exception(f_with_multiline)
self.assertEqual(result_lines, expected_f.splitlines()) self.assertEqual(result_lines, expected_f.splitlines())
@ -706,6 +718,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n' f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n'
' return 10 + divisor / 0 + 30\n' ' return 10 + divisor / 0 + 30\n'
' ~~~~~~~~^~~\n' ' ~~~~~~~~^~~\n'
@ -723,6 +736,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n' f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n'
' return 10 + áóí / 0 + 30\n' ' return 10 + áóí / 0 + 30\n'
' ~~~~^~~\n' ' ~~~~^~~\n'
@ -740,6 +754,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n' f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n'
' return 10 + divisor // 0 + 30\n' ' return 10 + divisor // 0 + 30\n'
' ~~~~~~~~^^~~\n' ' ~~~~~~~~^^~~\n'
@ -751,16 +766,102 @@ class TracebackErrorLocationCaretTestBase:
def f_with_binary_operator(): def f_with_binary_operator():
a = 1 a = 1
b = "" b = ""
return ( a ) + b return ( a ) +b
lineno_f = f_with_binary_operator.__code__.co_firstlineno lineno_f = f_with_binary_operator.__code__.co_firstlineno
expected_error = ( expected_error = (
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+3}, in f_with_binary_operator\n' f' File "{__file__}", line {lineno_f+3}, in f_with_binary_operator\n'
' return ( a ) + b\n' ' return ( a ) +b\n'
' ~~~~~~~~~~^~~\n' ' ~~~~~~~~~~^~\n'
)
result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines())
def test_caret_for_binary_operators_multiline(self):
def f_with_binary_operator():
b = 1
c = ""
a = b \
+\
c # test
return a
lineno_f = f_with_binary_operator.__code__.co_firstlineno
expected_error = (
'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+3}, in f_with_binary_operator\n'
' a = b \\\n'
' ~~~~~~\n'
' +\\\n'
' ^~\n'
' c # test\n'
' ~\n'
)
result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines())
def test_caret_for_binary_operators_multiline_two_char(self):
def f_with_binary_operator():
b = 1
c = ""
a = (
(b # test +
) \
# +
<< (c # test
\
) # test
)
return a
lineno_f = f_with_binary_operator.__code__.co_firstlineno
expected_error = (
'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+4}, in f_with_binary_operator\n'
' (b # test +\n'
' ~~~~~~~~~~~~\n'
' ) \\\n'
' ~~~~\n'
' # +\n'
' ~~~\n'
' << (c # test\n'
' ^^~~~~~~~~~~~\n'
' \\\n'
' ~\n'
' ) # test\n'
' ~\n'
)
result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines())
def test_caret_for_binary_operators_multiline_with_unicode(self):
def f_with_binary_operator():
b = 1
a = ("ááá" +
"áá") + b
return a
lineno_f = f_with_binary_operator.__code__.co_firstlineno
expected_error = (
'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'
' a = ("ááá" +\n'
' ~~~~~~~~\n'
' "áá") + b\n'
' ~~~~~~^~~\n'
) )
result_lines = self.get_exception(f_with_binary_operator) result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines()) self.assertEqual(result_lines, expected_error.splitlines())
@ -775,6 +876,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n' f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n'
" return some_dict['x']['y']['z']\n" " return some_dict['x']['y']['z']\n"
' ~~~~~~~~~~~~~~~~~~~^^^^^\n' ' ~~~~~~~~~~~~~~~~~~~^^^^^\n'
@ -792,6 +894,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n' f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n'
" return some_dict['ó']['á']['í']['beta']\n" " return some_dict['ó']['á']['í']['beta']\n"
' ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^\n' ' ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^\n'
@ -810,6 +913,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+3}, in f_with_binary_operator\n' f' File "{__file__}", line {lineno_f+3}, in f_with_binary_operator\n'
' return b [ a ] + c\n' ' return b [ a ] + c\n'
' ~~~~~~^^^^^^^^^\n' ' ~~~~~~^^^^^^^^^\n'
@ -817,6 +921,226 @@ class TracebackErrorLocationCaretTestBase:
result_lines = self.get_exception(f_with_binary_operator) result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines()) self.assertEqual(result_lines, expected_error.splitlines())
def test_caret_for_subscript_multiline(self):
def f_with_subscript():
bbbbb = {}
ccc = 1
ddd = 2
b = bbbbb \
[ ccc # test
+ ddd \
] # test
return b
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'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+4}, in f_with_subscript\n'
' b = bbbbb \\\n'
' ~~~~~~~\n'
' [ ccc # test\n'
' ^^^^^^^^^^^^^\n'
' \n'
' \n'
' + ddd \\\n'
' ^^^^^^^^\n'
' \n'
' \n'
' ] # test\n'
' ^\n'
)
result_lines = self.get_exception(f_with_subscript)
self.assertEqual(result_lines, expected_error.splitlines())
def test_caret_for_call(self):
def f_with_call():
def f1(a):
def f2(b):
raise RuntimeError("fail")
return f2
return f1("x")("y")
lineno_f = f_with_call.__code__.co_firstlineno
expected_error = (
'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+5}, in f_with_call\n'
' return f1("x")("y")\n'
' ~~~~~~~^^^^^\n'
f' File "{__file__}", line {lineno_f+3}, in f2\n'
' raise RuntimeError("fail")\n'
)
result_lines = self.get_exception(f_with_call)
self.assertEqual(result_lines, expected_error.splitlines())
def test_caret_for_call_unicode(self):
def f_with_call():
def f1(a):
def f2(b):
raise RuntimeError("fail")
return f2
return f1("ó")("á")
lineno_f = f_with_call.__code__.co_firstlineno
expected_error = (
'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+5}, in f_with_call\n'
' return f1("ó")("á")\n'
' ~~~~~~~^^^^^\n'
f' File "{__file__}", line {lineno_f+3}, in f2\n'
' raise RuntimeError("fail")\n'
)
result_lines = self.get_exception(f_with_call)
self.assertEqual(result_lines, expected_error.splitlines())
def test_caret_for_call_with_spaces_and_parenthesis(self):
def f_with_binary_operator():
def f(a):
raise RuntimeError("fail")
return f ( "x" ) + 2
lineno_f = f_with_binary_operator.__code__.co_firstlineno
expected_error = (
'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+3}, in f_with_binary_operator\n'
' return f ( "x" ) + 2\n'
' ~~~~~~^^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_f+2}, in f\n'
' raise RuntimeError("fail")\n'
)
result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines())
def test_caret_for_call_multiline(self):
def f_with_call():
class C:
def y(self, a):
def f(b):
raise RuntimeError("fail")
return f
def g(x):
return C()
a = (g(1).y)(
2
)(3)(4)
return a
lineno_f = f_with_call.__code__.co_firstlineno
expected_error = (
'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+8}, in f_with_call\n'
' a = (g(1).y)(\n'
' ~~~~~~~~~\n'
' 2\n'
' ~\n'
' )(3)(4)\n'
' ~^^^\n'
f' File "{__file__}", line {lineno_f+4}, in f\n'
' raise RuntimeError("fail")\n'
)
result_lines = self.get_exception(f_with_call)
self.assertEqual(result_lines, expected_error.splitlines())
def test_many_lines(self):
def f():
x = 1
if True: x += (
"a" +
"a"
) # test
lineno_f = f.__code__.co_firstlineno
expected_error = (
'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\n'
' if True: x += (\n'
' ^^^^^^\n'
' ...<2 lines>...\n'
' ) # test\n'
' ^\n'
)
result_lines = self.get_exception(f)
self.assertEqual(result_lines, expected_error.splitlines())
def test_many_lines_no_caret(self):
def f():
x = 1
x += (
"a" +
"a"
)
lineno_f = f.__code__.co_firstlineno
expected_error = (
'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\n'
' x += (\n'
' ...<2 lines>...\n'
' )\n'
)
result_lines = self.get_exception(f)
self.assertEqual(result_lines, expected_error.splitlines())
def test_many_lines_binary_op(self):
def f_with_binary_operator():
b = 1
c = "a"
a = (
b +
b
) + (
c +
c +
c
)
return a
lineno_f = f_with_binary_operator.__code__.co_firstlineno
expected_error = (
'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+3}, in f_with_binary_operator\n'
' a = (\n'
' ~\n'
' b +\n'
' ~~~\n'
' b\n'
' ~\n'
' ) + (\n'
' ~~^~~\n'
' c +\n'
' ~~~\n'
' ...<2 lines>...\n'
' )\n'
' ~\n'
)
result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines())
def test_traceback_specialization_with_syntax_error(self): def test_traceback_specialization_with_syntax_error(self):
bytecode = compile("1 / 0 / 1 / 2\n", TESTFN, "exec") bytecode = compile("1 / 0 / 1 / 2\n", TESTFN, "exec")
@ -833,6 +1157,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{TESTFN}", line {lineno_f}, in <module>\n' f' File "{TESTFN}", line {lineno_f}, in <module>\n'
" 1 $ 0 / 1 / 2\n" " 1 $ 0 / 1 / 2\n"
' ^^^^^\n' ' ^^^^^\n'
@ -855,6 +1180,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{TESTFN}", line {lineno_f}, in <module>\n' f' File "{TESTFN}", line {lineno_f}, in <module>\n'
f' {source}\n' f' {source}\n'
f' {" "*len("if True: ") + "^"*256}\n' f' {" "*len("if True: ") + "^"*256}\n'
@ -872,6 +1198,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n' f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n'
" some_dict['x']['y']['z']\n" " some_dict['x']['y']['z']\n"
' ~~~~~~~~~~~~~~~~~~~^^^^^\n' ' ~~~~~~~~~~~~~~~~~~~^^^^^\n'
@ -891,6 +1218,7 @@ class TracebackErrorLocationCaretTestBase:
f' + Exception Group Traceback (most recent call last):\n' f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
f' | callable()\n' f' | callable()\n'
f' | ~~~~~~~~^^\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\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' | if True: raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n'
f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' f' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
@ -956,6 +1284,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_applydescs + 1}, in applydecs\n' f' File "{__file__}", line {lineno_applydescs + 1}, in applydecs\n'
' @dec_error\n' ' @dec_error\n'
' ^^^^^^^^^\n' ' ^^^^^^^^^\n'
@ -974,6 +1303,7 @@ class TracebackErrorLocationCaretTestBase:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
' callable()\n' ' callable()\n'
' ~~~~~~~~^^\n'
f' File "{__file__}", line {lineno_applydescs_class + 1}, in applydecs_class\n' f' File "{__file__}", line {lineno_applydescs_class + 1}, in applydecs_class\n'
' @dec_error\n' ' @dec_error\n'
' ^^^^^^^^^\n' ' ^^^^^^^^^\n'
@ -992,6 +1322,7 @@ class TracebackErrorLocationCaretTestBase:
"Traceback (most recent call last):", "Traceback (most recent call last):",
f" File \"{__file__}\", line {self.callable_line}, in get_exception", f" File \"{__file__}\", line {self.callable_line}, in get_exception",
" callable()", " callable()",
" ~~~~~~~~^^",
f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f", f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f",
" .method", " .method",
" ^^^^^^", " ^^^^^^",
@ -1008,6 +1339,7 @@ class TracebackErrorLocationCaretTestBase:
"Traceback (most recent call last):", "Traceback (most recent call last):",
f" File \"{__file__}\", line {self.callable_line}, in get_exception", f" File \"{__file__}\", line {self.callable_line}, in get_exception",
" callable()", " callable()",
" ~~~~~~~~^^",
f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f", f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f",
" method", " method",
] ]
@ -1023,6 +1355,7 @@ class TracebackErrorLocationCaretTestBase:
"Traceback (most recent call last):", "Traceback (most recent call last):",
f" File \"{__file__}\", line {self.callable_line}, in get_exception", f" File \"{__file__}\", line {self.callable_line}, in get_exception",
" callable()", " callable()",
" ~~~~~~~~^^",
f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f", f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f",
" . method", " . method",
" ^^^^^^", " ^^^^^^",
@ -1038,6 +1371,7 @@ class TracebackErrorLocationCaretTestBase:
"Traceback (most recent call last):", "Traceback (most recent call last):",
f" File \"{__file__}\", line {self.callable_line}, in get_exception", f" File \"{__file__}\", line {self.callable_line}, in get_exception",
" callable()", " callable()",
" ~~~~~~~~^^",
f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f",
" ", " ",
] ]
@ -1054,6 +1388,7 @@ class TracebackErrorLocationCaretTestBase:
"Traceback (most recent call last):", "Traceback (most recent call last):",
f" File \"{__file__}\", line {self.callable_line}, in get_exception", f" File \"{__file__}\", line {self.callable_line}, in get_exception",
" callable()", " callable()",
" ~~~~~~~~^^",
f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f", f" File \"{__file__}\", line {f.__code__.co_firstlineno + 2}, in f",
" raise ValueError()", " raise ValueError()",
] ]
@ -1072,9 +1407,12 @@ class TracebackErrorLocationCaretTestBase:
"Traceback (most recent call last):", "Traceback (most recent call last):",
f" File \"{__file__}\", line {self.callable_line}, in get_exception", f" File \"{__file__}\", line {self.callable_line}, in get_exception",
" callable()", " callable()",
" ~~~~~~~~^^",
f" File \"{__file__}\", line {f.__code__.co_firstlineno + 4}, in f", f" File \"{__file__}\", line {f.__code__.co_firstlineno + 4}, in f",
f" print(1, (", f" print(1, (",
f" ^^^^^^^", f" ~~~~~~^",
f" ))",
f" ^^^^^",
] ]
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
@ -1089,6 +1427,7 @@ class TracebackErrorLocationCaretTestBase:
f"Traceback (most recent call last):", f"Traceback (most recent call last):",
f" File \"{__file__}\", line {self.callable_line}, in get_exception", f" File \"{__file__}\", line {self.callable_line}, in get_exception",
f" callable()", f" callable()",
f" ~~~~~~~~^^",
f" File \"{__file__}\", line {f.__code__.co_firstlineno + 3}, in f", f" File \"{__file__}\", line {f.__code__.co_firstlineno + 3}, in f",
f" return 说明说明 / şçöğıĤellö", f" return 说明说明 / şçöğıĤellö",
f" ~~~~~~~~~^~~~~~~~~~~~", f" ~~~~~~~~~^~~~~~~~~~~~",
@ -1105,6 +1444,7 @@ class TracebackErrorLocationCaretTestBase:
f"Traceback (most recent call last):", f"Traceback (most recent call last):",
f" File \"{__file__}\", line {self.callable_line}, in get_exception", f" File \"{__file__}\", line {self.callable_line}, in get_exception",
f" callable()", f" callable()",
f" ~~~~~~~~^^",
f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f", f" File \"{__file__}\", line {f.__code__.co_firstlineno + 1}, in f",
f' return "✨🐍" + func_说明说明("📗🚛",', f' return "✨🐍" + func_说明说明("📗🚛",',
f" ^^^^^^^^^^^^^", f" ^^^^^^^^^^^^^",
@ -1127,6 +1467,7 @@ class TracebackErrorLocationCaretTestBase:
f"Traceback (most recent call last):", f"Traceback (most recent call last):",
f" File \"{__file__}\", line {self.callable_line}, in get_exception", f" File \"{__file__}\", line {self.callable_line}, in get_exception",
f" callable()", f" callable()",
f" ~~~~~~~~^^",
f" File \"{__file__}\", line {f.__code__.co_firstlineno + 8}, in f", f" File \"{__file__}\", line {f.__code__.co_firstlineno + 8}, in f",
f' return my_dct["✨🚛✨"]["说明"]["🐍"]["说明"]["🐍🐍"]', f' return my_dct["✨🚛✨"]["说明"]["🐍"]["说明"]["🐍🐍"]',
f" ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^", f" ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^",
@ -1141,6 +1482,7 @@ class TracebackErrorLocationCaretTestBase:
expected = ['Traceback (most recent call last):', expected = ['Traceback (most recent call last):',
f' File "{__file__}", line {self.callable_line}, in get_exception', f' File "{__file__}", line {self.callable_line}, in get_exception',
' callable()', ' callable()',
' ~~~~~~~~^^',
f' File "{__file__}", line {f.__code__.co_firstlineno + 1}, in f', f' File "{__file__}", line {f.__code__.co_firstlineno + 1}, in f',
' raise MemoryError()'] ' raise MemoryError()']
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
@ -1187,6 +1529,14 @@ class TracebackFormatMixin:
def some_exception(self): def some_exception(self):
raise KeyError('blah') raise KeyError('blah')
def _filter_debug_ranges(self, expected):
return [line for line in expected if not set(line.strip()) <= set("^~")]
def _maybe_filter_debug_ranges(self, expected):
if not self.DEBUG_RANGES:
return self._filter_debug_ranges(expected)
return expected
@cpython_only @cpython_only
def check_traceback_format(self, cleanup_func=None): def check_traceback_format(self, cleanup_func=None):
from _testcapi import traceback_print from _testcapi import traceback_print
@ -1199,6 +1549,11 @@ class TracebackFormatMixin:
cleanup_func(tb.tb_next) cleanup_func(tb.tb_next)
traceback_fmt = 'Traceback (most recent call last):\n' + \ traceback_fmt = 'Traceback (most recent call last):\n' + \
''.join(traceback.format_tb(tb)) ''.join(traceback.format_tb(tb))
# clear caret lines from traceback_fmt since internal API does
# not emit them
traceback_fmt = "\n".join(
self._filter_debug_ranges(traceback_fmt.splitlines())
) + "\n"
file_ = StringIO() file_ = StringIO()
traceback_print(tb, file_) traceback_print(tb, file_)
python_fmt = file_.getvalue() python_fmt = file_.getvalue()
@ -1291,12 +1646,16 @@ class TracebackFormatMixin:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
' f()\n' ' f()\n'
' ~^^\n'
f' File "{__file__}", line {lineno_f+1}, in f\n' f' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n' ' f()\n'
' ~^^\n'
f' File "{__file__}", line {lineno_f+1}, in f\n' f' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n' ' f()\n'
' ~^^\n'
f' File "{__file__}", line {lineno_f+1}, in f\n' f' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n' ' f()\n'
' ~^^\n'
# XXX: The following line changes depending on whether the tests # XXX: The following line changes depending on whether the tests
# are run through the interactive interpreter or with -m # are run through the interactive interpreter or with -m
# It also varies depending on the platform (stack size) # It also varies depending on the platform (stack size)
@ -1305,7 +1664,7 @@ class TracebackFormatMixin:
'RecursionError: maximum recursion depth exceeded\n' 'RecursionError: maximum recursion depth exceeded\n'
) )
expected = result_f.splitlines() expected = self._maybe_filter_debug_ranges(result_f.splitlines())
actual = stderr_f.getvalue().splitlines() actual = stderr_f.getvalue().splitlines()
# Check the output text matches expectations # Check the output text matches expectations
@ -1337,13 +1696,13 @@ class TracebackFormatMixin:
result_g = ( result_g = (
f' File "{__file__}", line {lineno_g+2}, in g\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
' [Previous line repeated 7 more times]\n' ' [Previous line repeated 7 more times]\n'
f' File "{__file__}", line {lineno_g+3}, in g\n' f' File "{__file__}", line {lineno_g+3}, in g\n'
' raise ValueError\n' ' raise ValueError\n'
@ -1353,11 +1712,10 @@ class TracebackFormatMixin:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n' f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n'
' g()\n' ' g()\n'
' ~^^\n'
) )
expected = (tb_line + result_g).splitlines() expected = self._maybe_filter_debug_ranges((tb_line + result_g).splitlines())
actual = stderr_g.getvalue().splitlines() actual = stderr_g.getvalue().splitlines()
if not self.DEBUG_RANGES:
expected = [line for line in expected if not set(line.strip()) == {"^"}]
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
# Check 2 different repetitive sections # Check 2 different repetitive sections
@ -1379,23 +1737,23 @@ class TracebackFormatMixin:
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n' f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n'
' h()\n' ' h()\n'
' ~^^\n'
f' File "{__file__}", line {lineno_h+2}, in h\n' f' File "{__file__}", line {lineno_h+2}, in h\n'
' return h(count-1)\n' ' return h(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
f' File "{__file__}", line {lineno_h+2}, in h\n' f' File "{__file__}", line {lineno_h+2}, in h\n'
' return h(count-1)\n' ' return h(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
f' File "{__file__}", line {lineno_h+2}, in h\n' f' File "{__file__}", line {lineno_h+2}, in h\n'
' return h(count-1)\n' ' return h(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
' [Previous line repeated 7 more times]\n' ' [Previous line repeated 7 more times]\n'
f' File "{__file__}", line {lineno_h+3}, in h\n' f' File "{__file__}", line {lineno_h+3}, in h\n'
' g()\n' ' g()\n'
' ~^^\n'
) )
expected = (result_h + result_g).splitlines() expected = self._maybe_filter_debug_ranges((result_h + result_g).splitlines())
actual = stderr_h.getvalue().splitlines() actual = stderr_h.getvalue().splitlines()
if not self.DEBUG_RANGES:
expected = [line for line in expected if not set(line.strip()) == {"^"}]
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
# Check the boundary conditions. First, test just below the cutoff. # Check the boundary conditions. First, test just below the cutoff.
@ -1409,26 +1767,25 @@ class TracebackFormatMixin:
result_g = ( result_g = (
f' File "{__file__}", line {lineno_g+2}, in g\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+3}, in g\n' f' File "{__file__}", line {lineno_g+3}, in g\n'
' raise ValueError\n' ' raise ValueError\n'
'ValueError\n' 'ValueError\n'
) )
tb_line = ( tb_line = (
'Traceback (most recent call last):\n' '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+80}, in _check_recursive_traceback_display\n'
' g(traceback._RECURSIVE_CUTOFF)\n' ' g(traceback._RECURSIVE_CUTOFF)\n'
' ~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
) )
expected = (tb_line + result_g).splitlines() expected = self._maybe_filter_debug_ranges((tb_line + result_g).splitlines())
actual = stderr_g.getvalue().splitlines() actual = stderr_g.getvalue().splitlines()
if not self.DEBUG_RANGES:
expected = [line for line in expected if not set(line.strip()) == {"^"}]
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
# Second, test just above the cutoff. # Second, test just above the cutoff.
@ -1442,13 +1799,13 @@ class TracebackFormatMixin:
result_g = ( result_g = (
f' File "{__file__}", line {lineno_g+2}, in g\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
f' File "{__file__}", line {lineno_g+2}, in g\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
' ^^^^^^^^^^\n' ' ~^^^^^^^^^\n'
' [Previous line repeated 1 more time]\n' ' [Previous line repeated 1 more time]\n'
f' File "{__file__}", line {lineno_g+3}, in g\n' f' File "{__file__}", line {lineno_g+3}, in g\n'
' raise ValueError\n' ' raise ValueError\n'
@ -1456,13 +1813,12 @@ class TracebackFormatMixin:
) )
tb_line = ( tb_line = (
'Traceback (most recent call last):\n' '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+112}, in _check_recursive_traceback_display\n'
' g(traceback._RECURSIVE_CUTOFF + 1)\n' ' g(traceback._RECURSIVE_CUTOFF + 1)\n'
' ~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n'
) )
expected = (tb_line + result_g).splitlines() expected = self._maybe_filter_debug_ranges((tb_line + result_g).splitlines())
actual = stderr_g.getvalue().splitlines() actual = stderr_g.getvalue().splitlines()
if not self.DEBUG_RANGES:
expected = [line for line in expected if not set(line.strip()) == {"^"}]
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
@requires_debug_ranges() @requires_debug_ranges()
@ -1942,6 +2298,7 @@ class BaseExceptionReportingTests:
f' + Exception Group Traceback (most recent call last):\n' f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
f' | exception_or_callable()\n' f' | exception_or_callable()\n'
f' | ~~~~~~~~~~~~~~~~~~~~~^^\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 1}, in exc\n'
f' | raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n' f' | raise ExceptionGroup("eg", [ValueError(1), TypeError(2)])\n'
f' | ExceptionGroup: eg (2 sub-exceptions)\n' f' | ExceptionGroup: eg (2 sub-exceptions)\n'
@ -1977,6 +2334,7 @@ class BaseExceptionReportingTests:
f' + Exception Group Traceback (most recent call last):\n' f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
f' | exception_or_callable()\n' f' | exception_or_callable()\n'
f' | ~~~~~~~~~~~~~~~~~~~~~^^\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 5}, in exc\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' | raise EG("eg2", [ValueError(3), TypeError(4)]) from e\n'
f' | ExceptionGroup: eg2 (2 sub-exceptions)\n' f' | ExceptionGroup: eg2 (2 sub-exceptions)\n'
@ -2028,6 +2386,7 @@ class BaseExceptionReportingTests:
f'Traceback (most recent call last):\n' f'Traceback (most recent call last):\n'
f' File "{__file__}", line {self.callable_line}, in get_exception\n' f' File "{__file__}", line {self.callable_line}, in get_exception\n'
f' exception_or_callable()\n' f' exception_or_callable()\n'
f' ~~~~~~~~~~~~~~~~~~~~~^^\n'
f' File "{__file__}", line {exc.__code__.co_firstlineno + 8}, in exc\n' f' File "{__file__}", line {exc.__code__.co_firstlineno + 8}, in exc\n'
f' raise ImportError(5)\n' f' raise ImportError(5)\n'
f'ImportError: 5\n') f'ImportError: 5\n')
@ -2074,6 +2433,7 @@ class BaseExceptionReportingTests:
f' + Exception Group Traceback (most recent call last):\n' f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
f' | exception_or_callable()\n' f' | exception_or_callable()\n'
f' | ~~~~~~~~~~~~~~~~~~~~~^^\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 11}, in exc\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 11}, in exc\n'
f' | raise EG("top", [VE(5)])\n' f' | raise EG("top", [VE(5)])\n'
f' | ExceptionGroup: top (1 sub-exception)\n' f' | ExceptionGroup: top (1 sub-exception)\n'
@ -2233,6 +2593,7 @@ class BaseExceptionReportingTests:
expected = (f' + Exception Group Traceback (most recent call last):\n' expected = (f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
f' | exception_or_callable()\n' f' | exception_or_callable()\n'
f' | ~~~~~~~~~~~~~~~~~~~~~^^\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 9}, in exc\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 9}, in exc\n'
f' | raise ExceptionGroup("nested", excs)\n' f' | raise ExceptionGroup("nested", excs)\n'
f' | ExceptionGroup: nested (2 sub-exceptions)\n' f' | ExceptionGroup: nested (2 sub-exceptions)\n'
@ -2284,6 +2645,7 @@ class BaseExceptionReportingTests:
expected = (f' + Exception Group Traceback (most recent call last):\n' expected = (f' + Exception Group Traceback (most recent call last):\n'
f' | File "{__file__}", line {self.callable_line}, in get_exception\n' f' | File "{__file__}", line {self.callable_line}, in get_exception\n'
f' | exception_or_callable()\n' f' | exception_or_callable()\n'
f' | ~~~~~~~~~~~~~~~~~~~~~^^\n'
f' | File "{__file__}", line {exc.__code__.co_firstlineno + 10}, in exc\n' f' | File "{__file__}", line {exc.__code__.co_firstlineno + 10}, in exc\n'
f' | raise ExceptionGroup("nested", excs)\n' f' | raise ExceptionGroup("nested", excs)\n'
f' | ExceptionGroup: nested (2 sub-exceptions)\n' f' | ExceptionGroup: nested (2 sub-exceptions)\n'
@ -2552,7 +2914,7 @@ class TestFrame(unittest.TestCase):
def test_lazy_lines(self): def test_lazy_lines(self):
linecache.clearcache() linecache.clearcache()
f = traceback.FrameSummary("f", 1, "dummy", lookup_line=False) f = traceback.FrameSummary("f", 1, "dummy", lookup_line=False)
self.assertEqual(None, f._line) self.assertEqual(None, f._lines)
linecache.lazycache("f", globals()) linecache.lazycache("f", globals())
self.assertEqual( self.assertEqual(
'"""Test cases for traceback module"""', '"""Test cases for traceback module"""',
@ -3143,6 +3505,7 @@ class TestTracebackException_ExceptionGroups(unittest.TestCase):
f' | Traceback (most recent call last):', f' | Traceback (most recent call last):',
f' | File "{__file__}", line {lno_g+9}, in _get_exception_group', f' | File "{__file__}", line {lno_g+9}, in _get_exception_group',
f' | f()', f' | f()',
f' | ~^^',
f' | File "{__file__}", line {lno_f+1}, in f', f' | File "{__file__}", line {lno_f+1}, in f',
f' | 1/0', f' | 1/0',
f' | ~^~', f' | ~^~',
@ -3151,6 +3514,7 @@ class TestTracebackException_ExceptionGroups(unittest.TestCase):
f' | Traceback (most recent call last):', f' | Traceback (most recent call last):',
f' | File "{__file__}", line {lno_g+13}, in _get_exception_group', f' | File "{__file__}", line {lno_g+13}, in _get_exception_group',
f' | g(42)', f' | g(42)',
f' | ~^^^^',
f' | File "{__file__}", line {lno_g+1}, in g', f' | File "{__file__}", line {lno_g+1}, in g',
f' | raise ValueError(v)', f' | raise ValueError(v)',
f' | ValueError: 42', f' | ValueError: 42',
@ -3159,6 +3523,7 @@ class TestTracebackException_ExceptionGroups(unittest.TestCase):
f' | Traceback (most recent call last):', f' | Traceback (most recent call last):',
f' | File "{__file__}", line {lno_g+20}, in _get_exception_group', f' | File "{__file__}", line {lno_g+20}, in _get_exception_group',
f' | g(24)', f' | g(24)',
f' | ~^^^^',
f' | File "{__file__}", line {lno_g+1}, in g', f' | File "{__file__}", line {lno_g+1}, in g',
f' | raise ValueError(v)', f' | raise ValueError(v)',
f' | ValueError: 24', f' | ValueError: 24',

View file

@ -1260,8 +1260,8 @@ class EnvironmentVariableTests(BaseTest):
b" File \"<string>\", line 1, in <module>", b" File \"<string>\", line 1, in <module>",
b' import sys, warnings; sys.stdout.write(str(sys.warnoptions)); warnings.w' b' import sys, warnings; sys.stdout.write(str(sys.warnoptions)); warnings.w'
b"arn('Message', DeprecationWarning)", b"arn('Message', DeprecationWarning)",
b' ^^^^^^^^^^' b' ~~~~~~~~~~'
b'^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^', b'~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^',
b"DeprecationWarning: Message"]) b"DeprecationWarning: Message"])
def test_default_filter_configuration(self): def test_default_filter_configuration(self):

View file

@ -274,7 +274,7 @@ class FrameSummary:
""" """
__slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno', __slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno',
'name', '_line', 'locals') 'name', '_lines', '_lines_dedented', 'locals')
def __init__(self, filename, lineno, name, *, lookup_line=True, def __init__(self, filename, lineno, name, *, lookup_line=True,
locals=None, line=None, locals=None, line=None,
@ -290,15 +290,16 @@ class FrameSummary:
""" """
self.filename = filename self.filename = filename
self.lineno = lineno self.lineno = lineno
self.end_lineno = lineno if end_lineno is None else end_lineno
self.colno = colno
self.end_colno = end_colno
self.name = name self.name = name
self._line = line self._lines = line
self._lines_dedented = None
if lookup_line: if lookup_line:
self.line self.line
self.locals = {k: _safe_string(v, 'local', func=repr) self.locals = {k: _safe_string(v, 'local', func=repr)
for k, v in locals.items()} if locals else None for k, v in locals.items()} if locals else None
self.end_lineno = end_lineno
self.colno = colno
self.end_colno = end_colno
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, FrameSummary): if isinstance(other, FrameSummary):
@ -323,19 +324,39 @@ class FrameSummary:
def __len__(self): def __len__(self):
return 4 return 4
def _set_lines(self):
if (
self._lines is None
and self.lineno is not None
and self.end_lineno is not None
):
lines = []
for lineno in range(self.lineno, self.end_lineno + 1):
# treat errors (empty string) and empty lines (newline) as the same
lines.append(linecache.getline(self.filename, lineno).rstrip())
self._lines = "\n".join(lines) + "\n"
@property @property
def _original_line(self): def _original_lines(self):
# Returns the line as-is from the source, without modifying whitespace. # Returns the line as-is from the source, without modifying whitespace.
self.line self._set_lines()
return self._line return self._lines
@property
def _dedented_lines(self):
# Returns _original_lines, but dedented
self._set_lines()
if self._lines_dedented is None and self._lines is not None:
self._lines_dedented = textwrap.dedent(self._lines)
return self._lines_dedented
@property @property
def line(self): def line(self):
if self._line is None: self._set_lines()
if self.lineno is None: if self._lines is None:
return None return None
self._line = linecache.getline(self.filename, self.lineno) # return only the first line, stripped
return self._line.strip() return self._lines.partition("\n")[0].strip()
def walk_stack(f): def walk_stack(f):
@ -487,56 +508,135 @@ class StackSummary(list):
filename = "<stdin>" filename = "<stdin>"
row.append(' File "{}", line {}, in {}\n'.format( row.append(' File "{}", line {}, in {}\n'.format(
filename, frame_summary.lineno, frame_summary.name)) filename, frame_summary.lineno, frame_summary.name))
if frame_summary.line: if frame_summary._dedented_lines and frame_summary._dedented_lines.strip():
stripped_line = frame_summary.line.strip()
row.append(' {}\n'.format(stripped_line))
line = frame_summary._original_line
orig_line_len = len(line)
frame_line_len = len(frame_summary.line.lstrip())
stripped_characters = orig_line_len - frame_line_len
if ( if (
frame_summary.colno is not None frame_summary.colno is None or
and frame_summary.end_colno is not None frame_summary.end_colno is None
): ):
start_offset = _byte_offset_to_character_offset( # only output first line if column information is missing
line, frame_summary.colno) row.append(textwrap.indent(frame_summary.line, ' ') + "\n")
end_offset = _byte_offset_to_character_offset( else:
line, frame_summary.end_colno) # get first and last line
code_segment = line[start_offset:end_offset] all_lines_original = frame_summary._original_lines.splitlines()
first_line = all_lines_original[0]
# assume all_lines_original has enough lines (since we constructed it)
last_line = all_lines_original[frame_summary.end_lineno - frame_summary.lineno]
# character index of the start/end of the instruction
start_offset = _byte_offset_to_character_offset(first_line, frame_summary.colno)
end_offset = _byte_offset_to_character_offset(last_line, frame_summary.end_colno)
all_lines = frame_summary._dedented_lines.splitlines()[
:frame_summary.end_lineno - frame_summary.lineno + 1
]
# adjust start/end offset based on dedent
dedent_characters = len(first_line) - len(all_lines[0])
start_offset = max(0, start_offset - dedent_characters)
end_offset = max(0, end_offset - dedent_characters)
# When showing this on a terminal, some of the non-ASCII characters
# might be rendered as double-width characters, so we need to take
# that into account when calculating the length of the line.
dp_start_offset = _display_width(all_lines[0], offset=start_offset)
dp_end_offset = _display_width(all_lines[-1], offset=end_offset)
# get exact code segment corresponding to the instruction
segment = "\n".join(all_lines)
segment = segment[start_offset:len(segment) - (len(all_lines[-1]) - end_offset)]
# attempt to parse for anchors
anchors = None anchors = None
if frame_summary.lineno == frame_summary.end_lineno: with suppress(Exception):
with suppress(Exception): anchors = _extract_caret_anchors_from_line_segment(segment)
anchors = _extract_caret_anchors_from_line_segment(code_segment)
else:
# Don't count the newline since the anchors only need to
# go up until the last character of the line.
end_offset = len(line.rstrip())
# show indicators if primary char doesn't span the frame line # only use carets if there are anchors or the carets do not span all lines
if end_offset - start_offset < len(stripped_line) or ( show_carets = False
anchors and anchors.right_start_offset - anchors.left_end_offset > 0): if anchors or all_lines[0][:start_offset].lstrip() or all_lines[-1][end_offset:].rstrip():
# When showing this on a terminal, some of the non-ASCII characters show_carets = True
# might be rendered as double-width characters, so we need to take
# that into account when calculating the length of the line.
dp_start_offset = _display_width(line, start_offset) + 1
dp_end_offset = _display_width(line, end_offset) + 1
row.append(' ') result = []
row.append(' ' * (dp_start_offset - stripped_characters))
if anchors: # only display first line, last line, and lines around anchor start/end
dp_left_end_offset = _display_width(code_segment, anchors.left_end_offset) significant_lines = {0, len(all_lines) - 1}
dp_right_start_offset = _display_width(code_segment, anchors.right_start_offset)
row.append(anchors.primary_char * dp_left_end_offset)
row.append(anchors.secondary_char * (dp_right_start_offset - dp_left_end_offset))
row.append(anchors.primary_char * (dp_end_offset - dp_start_offset - dp_right_start_offset))
else:
row.append('^' * (dp_end_offset - dp_start_offset))
row.append('\n') anchors_left_end_offset = 0
anchors_right_start_offset = 0
primary_char = "^"
secondary_char = "^"
if anchors:
anchors_left_end_offset = anchors.left_end_offset
anchors_right_start_offset = anchors.right_start_offset
# computed anchor positions do not take start_offset into account,
# so account for it here
if anchors.left_end_lineno == 0:
anchors_left_end_offset += start_offset
if anchors.right_start_lineno == 0:
anchors_right_start_offset += start_offset
# account for display width
anchors_left_end_offset = _display_width(
all_lines[anchors.left_end_lineno], offset=anchors_left_end_offset
)
anchors_right_start_offset = _display_width(
all_lines[anchors.right_start_lineno], offset=anchors_right_start_offset
)
primary_char = anchors.primary_char
secondary_char = anchors.secondary_char
significant_lines.update(
range(anchors.left_end_lineno - 1, anchors.left_end_lineno + 2)
)
significant_lines.update(
range(anchors.right_start_lineno - 1, anchors.right_start_lineno + 2)
)
# remove bad line numbers
significant_lines.discard(-1)
significant_lines.discard(len(all_lines))
def output_line(lineno):
"""output all_lines[lineno] along with carets"""
result.append(all_lines[lineno] + "\n")
if not show_carets:
return
num_spaces = len(all_lines[lineno]) - len(all_lines[lineno].lstrip())
carets = []
num_carets = dp_end_offset if lineno == len(all_lines) - 1 else _display_width(all_lines[lineno])
# compute caret character for each position
for col in range(num_carets):
if col < num_spaces or (lineno == 0 and col < dp_start_offset):
# before first non-ws char of the line, or before start of instruction
carets.append(' ')
elif anchors and (
lineno > anchors.left_end_lineno or
(lineno == anchors.left_end_lineno and col >= anchors_left_end_offset)
) and (
lineno < anchors.right_start_lineno or
(lineno == anchors.right_start_lineno and col < anchors_right_start_offset)
):
# within anchors
carets.append(secondary_char)
else:
carets.append(primary_char)
result.append("".join(carets) + "\n")
# display significant lines
sig_lines_list = sorted(significant_lines)
for i, lineno in enumerate(sig_lines_list):
if i:
linediff = lineno - sig_lines_list[i - 1]
if linediff == 2:
# 1 line in between - just output it
output_line(lineno - 1)
elif linediff > 2:
# > 1 line in between - abbreviate
result.append(f"...<{linediff - 1} lines>...\n")
output_line(lineno)
row.append(
textwrap.indent(textwrap.dedent("".join(result)), ' ', lambda line: True)
)
if frame_summary.locals: if frame_summary.locals:
for name, value in sorted(frame_summary.locals.items()): for name, value in sorted(frame_summary.locals.items()):
row.append(' {name} = {value}\n'.format(name=name, value=value)) row.append(' {name} = {value}\n'.format(name=name, value=value))
@ -599,7 +699,9 @@ def _byte_offset_to_character_offset(str, offset):
_Anchors = collections.namedtuple( _Anchors = collections.namedtuple(
"_Anchors", "_Anchors",
[ [
"left_end_lineno",
"left_end_offset", "left_end_offset",
"right_start_lineno",
"right_start_offset", "right_start_offset",
"primary_char", "primary_char",
"secondary_char", "secondary_char",
@ -608,59 +710,161 @@ _Anchors = collections.namedtuple(
) )
def _extract_caret_anchors_from_line_segment(segment): def _extract_caret_anchors_from_line_segment(segment):
"""
Given source code `segment` corresponding to a FrameSummary, determine:
- for binary ops, the location of the binary op
- for indexing and function calls, the location of the brackets.
`segment` is expected to be a valid Python expression.
"""
import ast import ast
try: try:
tree = ast.parse(segment) # Without parentheses, `segment` is parsed as a statement.
# Binary ops, subscripts, and calls are expressions, so
# we can wrap them with parentheses to parse them as
# (possibly multi-line) expressions.
# e.g. if we try to highlight the addition in
# x = (
# a +
# b
# )
# then we would ast.parse
# a +
# b
# which is not a valid statement because of the newline.
# Adding brackets makes it a valid expression.
# (
# a +
# b
# )
# Line locations will be different than the original,
# which is taken into account later on.
tree = ast.parse(f"(\n{segment}\n)")
except SyntaxError: except SyntaxError:
return None return None
if len(tree.body) != 1: if len(tree.body) != 1:
return None return None
normalize = lambda offset: _byte_offset_to_character_offset(segment, offset) lines = segment.splitlines()
def normalize(lineno, offset):
"""Get character index given byte offset"""
return _byte_offset_to_character_offset(lines[lineno], offset)
def next_valid_char(lineno, col):
"""Gets the next valid character index in `lines`, if
the current location is not valid. Handles empty lines.
"""
while lineno < len(lines) and col >= len(lines[lineno]):
col = 0
lineno += 1
assert lineno < len(lines) and col < len(lines[lineno])
return lineno, col
def increment(lineno, col):
"""Get the next valid character index in `lines`."""
col += 1
lineno, col = next_valid_char(lineno, col)
return lineno, col
def nextline(lineno, col):
"""Get the next valid character at least on the next line"""
col = 0
lineno += 1
lineno, col = next_valid_char(lineno, col)
return lineno, col
def increment_until(lineno, col, stop):
"""Get the next valid non-"\\#" character that satisfies the `stop` predicate"""
while True:
ch = lines[lineno][col]
if ch in "\\#":
lineno, col = nextline(lineno, col)
elif not stop(ch):
lineno, col = increment(lineno, col)
else:
break
return lineno, col
def setup_positions(expr, force_valid=True):
"""Get the lineno/col position of the end of `expr`. If `force_valid` is True,
forces the position to be a valid character (e.g. if the position is beyond the
end of the line, move to the next line)
"""
# -2 since end_lineno is 1-indexed and because we added an extra
# bracket + newline to `segment` when calling ast.parse
lineno = expr.end_lineno - 2
col = normalize(lineno, expr.end_col_offset)
return next_valid_char(lineno, col) if force_valid else (lineno, col)
statement = tree.body[0] statement = tree.body[0]
match statement: match statement:
case ast.Expr(expr): case ast.Expr(expr):
match expr: match expr:
case ast.BinOp(): case ast.BinOp():
operator_start = normalize(expr.left.end_col_offset) # ast gives these locations for BinOp subexpressions
operator_end = normalize(expr.right.col_offset) # ( left_expr ) + ( right_expr )
operator_str = segment[operator_start:operator_end] # left^^^^^ right^^^^^
operator_offset = len(operator_str) - len(operator_str.lstrip()) lineno, col = setup_positions(expr.left)
left_anchor = expr.left.end_col_offset + operator_offset # First operator character is the first non-space/')' character
right_anchor = left_anchor + 1 lineno, col = increment_until(lineno, col, lambda x: not x.isspace() and x != ')')
# binary op is 1 or 2 characters long, on the same line,
# before the right subexpression
right_col = col + 1
if ( if (
operator_offset + 1 < len(operator_str) right_col < len(lines[lineno])
and not operator_str[operator_offset + 1].isspace() and (
# operator char should not be in the right subexpression
expr.right.lineno - 2 > lineno or
right_col < normalize(expr.right.lineno - 2, expr.right.col_offset)
)
and not (ch := lines[lineno][right_col]).isspace()
and ch not in "\\#"
): ):
right_anchor += 1 right_col += 1
while left_anchor < len(segment) and ((ch := segment[left_anchor]).isspace() or ch in ")#"): # right_col can be invalid since it is exclusive
left_anchor += 1 return _Anchors(lineno, col, lineno, right_col)
right_anchor += 1
return _Anchors(normalize(left_anchor), normalize(right_anchor))
case ast.Subscript(): case ast.Subscript():
left_anchor = normalize(expr.value.end_col_offset) # ast gives these locations for value and slice subexpressions
right_anchor = normalize(expr.slice.end_col_offset + 1) # ( value_expr ) [ slice_expr ]
while left_anchor < len(segment) and ((ch := segment[left_anchor]).isspace() or ch != "["): # value^^^^^ slice^^^^^
left_anchor += 1 # subscript^^^^^^^^^^^^^^^^^^^^
while right_anchor < len(segment) and ((ch := segment[right_anchor]).isspace() or ch != "]"):
right_anchor += 1 # find left bracket
if right_anchor < len(segment): left_lineno, left_col = setup_positions(expr.value)
right_anchor += 1 left_lineno, left_col = increment_until(left_lineno, left_col, lambda x: x == '[')
return _Anchors(left_anchor, right_anchor) # find right bracket (final character of expression)
right_lineno, right_col = setup_positions(expr, force_valid=False)
return _Anchors(left_lineno, left_col, right_lineno, right_col)
case ast.Call():
# ast gives these locations for function call expressions
# ( func_expr ) (args, kwargs)
# func^^^^^
# call^^^^^^^^^^^^^^^^^^^^^^^^
# find left bracket
left_lineno, left_col = setup_positions(expr.func)
left_lineno, left_col = increment_until(left_lineno, left_col, lambda x: x == '(')
# find right bracket (final character of expression)
right_lineno, right_col = setup_positions(expr, force_valid=False)
return _Anchors(left_lineno, left_col, right_lineno, right_col)
return None return None
_WIDE_CHAR_SPECIFIERS = "WF" _WIDE_CHAR_SPECIFIERS = "WF"
def _display_width(line, offset): def _display_width(line, offset=None):
"""Calculate the extra amount of width space the given source """Calculate the extra amount of width space the given source
code segment might take if it were to be displayed on a fixed code segment might take if it were to be displayed on a fixed
width output device. Supports wide unicode characters and emojis.""" width output device. Supports wide unicode characters and emojis."""
if offset is None:
offset = len(line)
# Fast track for ASCII-only strings # Fast track for ASCII-only strings
if line.isascii(): if line.isascii():
return offset return offset

View file

@ -0,0 +1 @@
Display multiple lines with ``traceback`` when errors span multiple lines.