Skip to content

Commit 57be545

Browse files
authored
gh-99103: Normalize specialized traceback anchors against the current line (GH-99145)
Automerge-Triggered-By: GH:isidentical
1 parent c95f554 commit 57be545

File tree

4 files changed

+53
-7
lines changed

4 files changed

+53
-7
lines changed

Lib/test/test_traceback.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,23 @@ def f_with_binary_operator():
559559
result_lines = self.get_exception(f_with_binary_operator)
560560
self.assertEqual(result_lines, expected_error.splitlines())
561561

562+
def test_caret_for_binary_operators_with_unicode(self):
563+
def f_with_binary_operator():
564+
áóí = 20
565+
return 10 + áóí / 0 + 30
566+
567+
lineno_f = f_with_binary_operator.__code__.co_firstlineno
568+
expected_error = (
569+
'Traceback (most recent call last):\n'
570+
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
571+
' callable()\n'
572+
f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n'
573+
' return 10 + áóí / 0 + 30\n'
574+
' ~~~~^~~\n'
575+
)
576+
result_lines = self.get_exception(f_with_binary_operator)
577+
self.assertEqual(result_lines, expected_error.splitlines())
578+
562579
def test_caret_for_binary_operators_two_char(self):
563580
def f_with_binary_operator():
564581
divisor = 20
@@ -593,6 +610,23 @@ def f_with_subscript():
593610
result_lines = self.get_exception(f_with_subscript)
594611
self.assertEqual(result_lines, expected_error.splitlines())
595612

613+
def test_caret_for_subscript_unicode(self):
614+
def f_with_subscript():
615+
some_dict = {'ó': {'á': {'í': {'theta': 1}}}}
616+
return some_dict['ó']['á']['í']['beta']
617+
618+
lineno_f = f_with_subscript.__code__.co_firstlineno
619+
expected_error = (
620+
'Traceback (most recent call last):\n'
621+
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
622+
' callable()\n'
623+
f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n'
624+
" return some_dict['ó']['á']['í']['beta']\n"
625+
' ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^\n'
626+
)
627+
result_lines = self.get_exception(f_with_subscript)
628+
self.assertEqual(result_lines, expected_error.splitlines())
629+
596630
def test_traceback_specialization_with_syntax_error(self):
597631
bytecode = compile("1 / 0 / 1 / 2\n", TESTFN, "exec")
598632

@@ -3356,7 +3390,7 @@ def func():
33563390

33573391
actual = self.get_suggestion(func)
33583392
self.assertNotIn("blech", actual)
3359-
3393+
33603394
def test_name_error_with_instance(self):
33613395
class A:
33623396
def __init__(self):

Lib/traceback.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -586,12 +586,15 @@ def _extract_caret_anchors_from_line_segment(segment):
586586
if len(tree.body) != 1:
587587
return None
588588

589+
normalize = lambda offset: _byte_offset_to_character_offset(segment, offset)
589590
statement = tree.body[0]
590591
match statement:
591592
case ast.Expr(expr):
592593
match expr:
593594
case ast.BinOp():
594-
operator_str = segment[expr.left.end_col_offset:expr.right.col_offset]
595+
operator_start = normalize(expr.left.end_col_offset)
596+
operator_end = normalize(expr.right.col_offset)
597+
operator_str = segment[operator_start:operator_end]
595598
operator_offset = len(operator_str) - len(operator_str.lstrip())
596599

597600
left_anchor = expr.left.end_col_offset + operator_offset
@@ -601,9 +604,11 @@ def _extract_caret_anchors_from_line_segment(segment):
601604
and not operator_str[operator_offset + 1].isspace()
602605
):
603606
right_anchor += 1
604-
return _Anchors(left_anchor, right_anchor)
607+
return _Anchors(normalize(left_anchor), normalize(right_anchor))
605608
case ast.Subscript():
606-
return _Anchors(expr.value.end_col_offset, expr.slice.end_col_offset + 1)
609+
subscript_start = normalize(expr.value.end_col_offset)
610+
subscript_end = normalize(expr.slice.end_col_offset + 1)
611+
return _Anchors(subscript_start, subscript_end)
607612

608613
return None
609614

@@ -1044,7 +1049,7 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
10441049
self = frame.f_locals['self']
10451050
if hasattr(self, wrong_name):
10461051
return f"self.{wrong_name}"
1047-
1052+
10481053
# Compute closest match
10491054

10501055
if len(d) > _MAX_CANDIDATE_ITEMS:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix the error reporting positions of specialized traceback anchors when the
2+
source line contains Unicode characters.

Python/traceback.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -700,8 +700,13 @@ extract_anchors_from_line(PyObject *filename, PyObject *line,
700700

701701
done:
702702
if (res > 0) {
703-
*left_anchor += start_offset;
704-
*right_anchor += start_offset;
703+
// Normalize the AST offsets to byte offsets and adjust them with the
704+
// start of the actual line (instead of the source code segment).
705+
assert(segment != NULL);
706+
assert(*left_anchor >= 0);
707+
assert(*right_anchor >= 0);
708+
*left_anchor = _PyPegen_byte_offset_to_character_offset(segment, *left_anchor) + start_offset;
709+
*right_anchor = _PyPegen_byte_offset_to_character_offset(segment, *right_anchor) + start_offset;
705710
}
706711
Py_XDECREF(segment);
707712
if (arena) {

0 commit comments

Comments
 (0)