Skip to content

Commit

Permalink
bpo-43950: Specialize tracebacks for subscripts/binary ops (GH-27037)
Browse files Browse the repository at this point in the history
Co-authored-by: Ammar Askar <ammar@ammaraskar.com>
Co-authored-by: Pablo Galindo <pablogsal@gmail.com>
  • Loading branch information
3 people authored Jul 12, 2021
1 parent da2e673 commit 1890dd2
Show file tree
Hide file tree
Showing 4 changed files with 366 additions and 46 deletions.
4 changes: 2 additions & 2 deletions Doc/library/traceback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ The output for the example would look similar to this:
['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 7, in bright_side_of_death\n return tuple()[0]\n ^^^^^^^^^^\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:
[<FrameSummary file <doctest...>, line 10 in <module>>,
Expand All @@ -482,7 +482,7 @@ The output for the example would look similar to this:
*** 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 7, in bright_side_of_death\n return tuple()[0]\n ^^^^^^^^^^\n']
' File "<doctest default[0]>", line 7, in bright_side_of_death\n return tuple()[0]\n ~~~~~~~^^^\n']
*** tb_lineno: 10


Expand Down
82 changes: 80 additions & 2 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
requires_debug_ranges, has_no_debug_ranges)
from test.support.os_helper import TESTFN, unlink
from test.support.script_helper import assert_python_ok, assert_python_failure
import textwrap

import os
import textwrap
import traceback
from functools import partial


test_code = namedtuple('code', ['co_filename', 'co_name'])
Expand Down Expand Up @@ -406,6 +408,82 @@ def f_with_multiline():
result_lines = self.get_exception(f_with_multiline)
self.assertEqual(result_lines, expected_f.splitlines())

def test_caret_for_binary_operators(self):
def f_with_binary_operator():
divisor = 20
return 10 + divisor / 0 + 30

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'
' return 10 + divisor / 0 + 30\n'
' ~~~~~~~~^~~\n'
)
result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines())

def test_caret_for_binary_operators_two_char(self):
def f_with_binary_operator():
divisor = 20
return 10 + divisor // 0 + 30

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'
' return 10 + divisor // 0 + 30\n'
' ~~~~~~~~^^~~\n'
)
result_lines = self.get_exception(f_with_binary_operator)
self.assertEqual(result_lines, expected_error.splitlines())

def test_caret_for_subscript(self):
def f_with_subscript():
some_dict = {'x': {'y': None}}
return 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'
' ^^^^^^^^^^\n'
f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n'
" return some_dict['x']['y']['z']\n"
' ~~~~~~~~~~~~~~~~~~~^^^^^\n'
)
result_lines = self.get_exception(f_with_subscript)
self.assertEqual(result_lines, expected_error.splitlines())

def test_traceback_specialization_with_syntax_error(self):
bytecode = compile("1 / 0 / 1 / 2\n", TESTFN, "exec")

with open(TESTFN, "w") as file:
# make the file's contents invalid
file.write("1 $ 0 / 1 / 2\n")
self.addCleanup(unlink, TESTFN)

func = partial(exec, bytecode)
result_lines = self.get_exception(func)

lineno_f = bytecode.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 "{TESTFN}", line {lineno_f}, in <module>\n'
" 1 $ 0 / 1 / 2\n"
' ^^^^^\n'
)
self.assertEqual(result_lines, expected_error.splitlines())

@cpython_only
@requires_debug_ranges()
Expand Down Expand Up @@ -1615,7 +1693,7 @@ def f():
self.assertEqual(
output.getvalue().split('\n')[-5:],
[' x/0',
' ^^^',
' ~^~',
' x = 12',
'ZeroDivisionError: division by zero',
''])
Expand Down
60 changes: 59 additions & 1 deletion Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,9 +494,23 @@ def format(self):
colno = _byte_offset_to_character_offset(frame._original_line, frame.colno)
end_colno = _byte_offset_to_character_offset(frame._original_line, frame.end_colno)

try:
anchors = _extract_caret_anchors_from_line_segment(
frame._original_line[colno - 1:end_colno]
)
except Exception:
anchors = None

row.append(' ')
row.append(' ' * (colno - stripped_characters))
row.append('^' * (end_colno - colno))

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

if frame.locals:
Expand All @@ -520,6 +534,50 @@ def _byte_offset_to_character_offset(str, offset):
return len(as_utf8[:offset + 1].decode("utf-8"))


_Anchors = collections.namedtuple(
"_Anchors",
[
"left_end_offset",
"right_start_offset",
"primary_char",
"secondary_char",
],
defaults=["~", "^"]
)

def _extract_caret_anchors_from_line_segment(segment):
import ast

try:
tree = ast.parse(segment)
except SyntaxError:
return None

if len(tree.body) != 1:
return None

statement = tree.body[0]
match statement:
case ast.Expr(expr):
match expr:
case ast.BinOp():
operator_str = segment[expr.left.end_col_offset:expr.right.col_offset]
operator_offset = len(operator_str) - len(operator_str.lstrip())

left_anchor = expr.left.end_col_offset + operator_offset
right_anchor = left_anchor + 1
if (
operator_offset + 1 < len(operator_str)
and not operator_str[operator_offset + 1].isspace()
):
right_anchor += 1
return _Anchors(left_anchor, right_anchor)
case ast.Subscript():
return _Anchors(expr.value.end_col_offset, expr.slice.end_col_offset + 1)

return None


class TracebackException:
"""An exception ready for rendering.
Expand Down
Loading

0 comments on commit 1890dd2

Please sign in to comment.