Skip to content

Commit d79d365

Browse files
Port new specializations to traceback.py
Co-authored-by: Batuhan Taskaya <batuhanosmantaskaya@gmail.com>
1 parent 0a825ea commit d79d365

File tree

3 files changed

+106
-4
lines changed

3 files changed

+106
-4
lines changed

Doc/library/traceback.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ The output for the example would look similar to this:
473473
['Traceback (most recent call last):\n',
474474
' File "<doctest default[0]>", line 10, in <module>\n lumberjack()\n ^^^^^^^^^^^^\n',
475475
' File "<doctest default[0]>", line 4, in lumberjack\n bright_side_of_death()\n ^^^^^^^^^^^^^^^^^^^^^^\n',
476-
' File "<doctest default[0]>", line 7, in bright_side_of_death\n return tuple()[0]\n ^^^^^^^^^^\n',
476+
' File "<doctest default[0]>", line 7, in bright_side_of_death\n return tuple()[0]\n ~~~~~~~^^^\n',
477477
'IndexError: tuple index out of range\n']
478478
*** extract_tb:
479479
[<FrameSummary file <doctest...>, line 10 in <module>>,
@@ -482,7 +482,7 @@ The output for the example would look similar to this:
482482
*** format_tb:
483483
[' File "<doctest default[0]>", line 10, in <module>\n lumberjack()\n ^^^^^^^^^^^^\n',
484484
' File "<doctest default[0]>", line 4, in lumberjack\n bright_side_of_death()\n ^^^^^^^^^^^^^^^^^^^^^^\n',
485-
' File "<doctest default[0]>", line 7, in bright_side_of_death\n return tuple()[0]\n ^^^^^^^^^^\n']
485+
' File "<doctest default[0]>", line 7, in bright_side_of_death\n return tuple()[0]\n ~~~~~~~^^^\n']
486486
*** tb_lineno: 10
487487

488488

Lib/test/test_traceback.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,61 @@ def f_with_multiline():
406406
result_lines = self.get_exception(f_with_multiline)
407407
self.assertEqual(result_lines, expected_f.splitlines())
408408

409+
def test_caret_for_binary_operators(self):
410+
def f_with_binary_operator():
411+
divisor = 20
412+
return 10 + divisor / 0 + 30
413+
414+
lineno_f = f_with_binary_operator.__code__.co_firstlineno
415+
expected_error = (
416+
'Traceback (most recent call last):\n'
417+
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
418+
' callable()\n'
419+
' ^^^^^^^^^^\n'
420+
f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n'
421+
' return 10 + divisor / 0 + 30\n'
422+
' ~~~~~~~~^~~\n'
423+
)
424+
result_lines = self.get_exception(f_with_binary_operator)
425+
self.assertEqual(result_lines, expected_error.splitlines())
426+
427+
def test_caret_for_binary_operators_two_char(self):
428+
def f_with_binary_operator():
429+
divisor = 20
430+
return 10 + divisor // 0 + 30
431+
432+
lineno_f = f_with_binary_operator.__code__.co_firstlineno
433+
expected_error = (
434+
'Traceback (most recent call last):\n'
435+
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
436+
' callable()\n'
437+
' ^^^^^^^^^^\n'
438+
f' File "{__file__}", line {lineno_f+2}, in f_with_binary_operator\n'
439+
' return 10 + divisor // 0 + 30\n'
440+
' ~~~~~~~~^^~~\n'
441+
)
442+
result_lines = self.get_exception(f_with_binary_operator)
443+
self.assertEqual(result_lines, expected_error.splitlines())
444+
445+
def test_caret_for_subscript(self):
446+
def f_with_subscript():
447+
some_dict = {'x': {'y': None}}
448+
return some_dict['x']['y']['z']
449+
450+
lineno_f = f_with_subscript.__code__.co_firstlineno
451+
expected_error = (
452+
'Traceback (most recent call last):\n'
453+
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
454+
' callable()\n'
455+
' ^^^^^^^^^^\n'
456+
f' File "{__file__}", line {lineno_f+2}, in f_with_subscript\n'
457+
" return some_dict['x']['y']['z']\n"
458+
' ~~~~~~~~~~~~~~~~~~~^^^^^\n'
459+
)
460+
result_lines = self.get_exception(f_with_subscript)
461+
self.assertEqual(result_lines, expected_error.splitlines())
462+
463+
409464

410465
@cpython_only
411466
@requires_debug_ranges()
@@ -1615,7 +1670,7 @@ def f():
16151670
self.assertEqual(
16161671
output.getvalue().split('\n')[-5:],
16171672
[' x/0',
1618-
' ^^^',
1673+
' ~^~',
16191674
' x = 12',
16201675
'ZeroDivisionError: division by zero',
16211676
''])

Lib/traceback.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,9 +494,23 @@ def format(self):
494494
colno = _byte_offset_to_character_offset(frame._original_line, frame.colno)
495495
end_colno = _byte_offset_to_character_offset(frame._original_line, frame.end_colno)
496496

497+
try:
498+
anchors = _extract_caret_anchors_from_line_segment(
499+
frame._original_line[colno - 1:end_colno]
500+
)
501+
except Exception:
502+
anchors = None
503+
497504
row.append(' ')
498505
row.append(' ' * (colno - stripped_characters))
499-
row.append('^' * (end_colno - colno))
506+
507+
if anchors:
508+
row.append('~' * (anchors[0]))
509+
row.append('^' * (anchors[1] - anchors[0]))
510+
row.append('~' * (end_colno - colno - anchors[1]))
511+
else:
512+
row.append('^' * (end_colno - colno))
513+
500514
row.append('\n')
501515

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

522536

537+
def _extract_caret_anchors_from_line_segment(segment):
538+
import ast
539+
540+
try:
541+
tree = ast.parse(segment)
542+
except SyntaxError:
543+
return None
544+
545+
if len(tree.body) != 1:
546+
return None
547+
548+
statement = tree.body[0]
549+
match statement:
550+
case ast.Expr(expr):
551+
match expr:
552+
case ast.BinOp():
553+
operator_str = segment[expr.left.end_col_offset:expr.right.col_offset]
554+
operator_offset = len(operator_str) - len(operator_str.lstrip())
555+
556+
left_anchor = expr.left.end_col_offset + operator_offset
557+
right_anchor = left_anchor + 1
558+
if (
559+
operator_offset + 1 < len(operator_str)
560+
and not operator_str[operator_offset + 1].isspace()
561+
):
562+
right_anchor += 1
563+
return left_anchor, right_anchor
564+
case ast.Subscript():
565+
return expr.value.end_col_offset, expr.slice.end_col_offset + 1
566+
567+
return None
568+
569+
523570
class TracebackException:
524571
"""An exception ready for rendering.
525572

0 commit comments

Comments
 (0)