Skip to content

Commit 3e235e0

Browse files
authored
bpo-43950: support some multi-line expressions for PEP 657 (GH-27339)
This is basically something that I noticed up while fixing test runs for another issue. It is really common to have multiline calls, and when they fail the display is kind of weird since we omit the annotations. E.g; ``` $ ./python t.py Traceback (most recent call last): File "/home/isidentical/cpython/cpython/t.py", line 11, in <module> frame_1() ^^^^^^^^^ File "/home/isidentical/cpython/cpython/t.py", line 5, in frame_1 frame_2( File "/home/isidentical/cpython/cpython/t.py", line 2, in frame_2 return a / 0 / b / c ~~^~~ ZeroDivisionError: division by zero ``` This patch basically adds support for annotating the rest of the line, if the instruction covers multiple lines (start_line != end_line). Automerge-Triggered-By: GH:isidentical
1 parent 96cf5a6 commit 3e235e0

File tree

4 files changed

+63
-17
lines changed

4 files changed

+63
-17
lines changed

Lib/test/test_doctest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2835,6 +2835,7 @@ def test_unicode(): """
28352835
Traceback (most recent call last):
28362836
File ...
28372837
exec(compile(example.source, filename, "single",
2838+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28382839
File "<doctest foo-bär@baz[0]>", line 1, in <module>
28392840
raise Exception('clé')
28402841
^^^^^^^^^^^^^^^^^^^^^^

Lib/test/test_traceback.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,30 @@ def f_with_multiline():
429429
' ^^^^^^^^^^\n'
430430
f' File "{__file__}", line {lineno_f+1}, in f_with_multiline\n'
431431
' raise ValueError(\n'
432+
' ^^^^^^^^^^^^^^^^^'
433+
)
434+
result_lines = self.get_exception(f_with_multiline)
435+
self.assertEqual(result_lines, expected_f.splitlines())
436+
437+
def test_caret_multiline_expression_bin_op(self):
438+
# Make sure no carets are printed for expressions spanning multiple
439+
# lines.
440+
def f_with_multiline():
441+
return (
442+
1 /
443+
0 +
444+
2
445+
)
446+
447+
lineno_f = f_with_multiline.__code__.co_firstlineno
448+
expected_f = (
449+
'Traceback (most recent call last):\n'
450+
f' File "{__file__}", line {self.callable_line}, in get_exception\n'
451+
' callable()\n'
452+
' ^^^^^^^^^^\n'
453+
f' File "{__file__}", line {lineno_f+2}, in f_with_multiline\n'
454+
' 1 /\n'
455+
' ^^^'
432456
)
433457
result_lines = self.get_exception(f_with_multiline)
434458
self.assertEqual(result_lines, expected_f.splitlines())

Lib/traceback.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import itertools
55
import linecache
66
import sys
7+
from contextlib import suppress
78

89
__all__ = ['extract_stack', 'extract_tb', 'format_exception',
910
'format_exception_only', 'format_list', 'format_stack',
@@ -463,19 +464,20 @@ def format_frame(self, frame):
463464

464465
stripped_characters = len(frame._original_line) - len(frame.line.lstrip())
465466
if (
466-
frame.end_lineno == frame.lineno
467-
and frame.colno is not None
467+
frame.colno is not None
468468
and frame.end_colno is not None
469469
):
470470
colno = _byte_offset_to_character_offset(frame._original_line, frame.colno)
471471
end_colno = _byte_offset_to_character_offset(frame._original_line, frame.end_colno)
472472

473-
try:
474-
anchors = _extract_caret_anchors_from_line_segment(
475-
frame._original_line[colno - 1:end_colno - 1]
476-
)
477-
except Exception:
478-
anchors = None
473+
anchors = None
474+
if frame.lineno == frame.end_lineno:
475+
with suppress(Exception):
476+
anchors = _extract_caret_anchors_from_line_segment(
477+
frame._original_line[colno - 1:end_colno - 1]
478+
)
479+
else:
480+
end_colno = stripped_characters + len(frame.line.strip())
479481

480482
row.append(' ')
481483
row.append(' ' * (colno - stripped_characters))

Python/traceback.c

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -720,11 +720,11 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen
720720
&end_line, &end_col_byte_offset)) {
721721
goto done;
722722
}
723-
if (start_line != end_line) {
724-
goto done;
725-
}
726723

727-
if (start_col_byte_offset < 0 || end_col_byte_offset < 0) {
724+
if (start_line < 0 || end_line < 0
725+
|| start_col_byte_offset < 0
726+
|| end_col_byte_offset < 0)
727+
{
728728
goto done;
729729
}
730730

@@ -762,11 +762,30 @@ tb_displayline(PyTracebackObject* tb, PyObject *f, PyObject *filename, int linen
762762
char *primary_error_char = "^";
763763
char *secondary_error_char = primary_error_char;
764764

765-
int res = extract_anchors_from_line(filename, source_line, start_offset, end_offset,
766-
&left_end_offset, &right_start_offset,
767-
&primary_error_char, &secondary_error_char);
768-
if (res < 0 && ignore_source_errors() < 0) {
769-
goto done;
765+
if (start_line == end_line) {
766+
int res = extract_anchors_from_line(filename, source_line, start_offset, end_offset,
767+
&left_end_offset, &right_start_offset,
768+
&primary_error_char, &secondary_error_char);
769+
if (res < 0 && ignore_source_errors() < 0) {
770+
goto done;
771+
}
772+
}
773+
else {
774+
// If this is a multi-line expression, then we will highlight until
775+
// the last non-whitespace character.
776+
const char *source_line_str = PyUnicode_AsUTF8(source_line);
777+
if (!source_line_str) {
778+
goto done;
779+
}
780+
781+
Py_ssize_t i = PyUnicode_GET_LENGTH(source_line);
782+
while (--i >= 0) {
783+
if (!IS_WHITESPACE(source_line_str[i])) {
784+
break;
785+
}
786+
}
787+
788+
end_offset = i + 1;
770789
}
771790

772791
err = print_error_location_carets(f, truncation, start_offset, end_offset,

0 commit comments

Comments
 (0)