Skip to content

Commit 8c14386

Browse files
committed
Fix traceback
1 parent 305a091 commit 8c14386

File tree

12 files changed

+352
-97
lines changed

12 files changed

+352
-97
lines changed

Lib/test/test_exceptions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,7 +1745,6 @@ def __del__(self):
17451745
f"deallocator {obj_repr}")
17461746
self.assertIsNotNone(cm.unraisable.exc_traceback)
17471747

1748-
@unittest.expectedFailure # TODO: RUSTPYTHON
17491748
def test_unhandled(self):
17501749
# Check for sensible reporting of unhandled exceptions
17511750
for exc_type in (ValueError, BrokenStrException):
@@ -2283,7 +2282,6 @@ def test_multiline_not_highlighted(self):
22832282
class SyntaxErrorTests(unittest.TestCase):
22842283
maxDiff = None
22852284

2286-
@unittest.expectedFailure # TODO: RUSTPYTHON
22872285
@force_not_colorized
22882286
def test_range_of_offsets(self):
22892287
cases = [

Lib/test/test_syntax.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2293,7 +2293,6 @@ def test_expression_with_assignment(self):
22932293
offset=7
22942294
)
22952295

2296-
@unittest.expectedFailure # TODO: RUSTPYTHON
22972296
def test_curly_brace_after_primary_raises_immediately(self):
22982297
self._check_error("f{}", "invalid syntax", mode="single")
22992298

Lib/test/test_traceback.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ def syntax_error_bad_indentation2(self):
8888
def tokenizer_error_with_caret_range(self):
8989
compile("blech ( ", "?", "exec")
9090

91-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 11 != 14
9291
def test_caret(self):
9392
err = self.get_exception_format(self.syntax_error_with_caret,
9493
SyntaxError)
@@ -201,7 +200,6 @@ def f():
201200
finally:
202201
unlink(TESTFN)
203202

204-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 3 != 4
205203
def test_bad_indentation(self):
206204
err = self.get_exception_format(self.syntax_error_bad_indentation,
207205
IndentationError)
@@ -1797,7 +1795,6 @@ class TestKeywordTypoSuggestions(unittest.TestCase):
17971795
("for x im n:\n pass", "in"),
17981796
]
17991797

1800-
@unittest.expectedFailure # TODO: RUSTPYTHON
18011798
def test_keyword_suggestions_from_file(self):
18021799
with tempfile.TemporaryDirectory() as script_dir:
18031800
for i, (code, expected_kw) in enumerate(self.TYPO_CASES):
@@ -1808,7 +1805,6 @@ def test_keyword_suggestions_from_file(self):
18081805
stderr_text = stderr.decode('utf-8')
18091806
self.assertIn(f"Did you mean '{expected_kw}'", stderr_text)
18101807

1811-
@unittest.expectedFailure # TODO: RUSTPYTHON
18121808
def test_keyword_suggestions_from_command_string(self):
18131809
for code, expected_kw in self.TYPO_CASES:
18141810
with self.subTest(typo=expected_kw):
@@ -3352,7 +3348,6 @@ class MiscTracebackCases(unittest.TestCase):
33523348
# Check non-printing functions in traceback module
33533349
#
33543350

3355-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 0
33563351
def test_clear(self):
33573352
def outer():
33583353
middle()
@@ -3574,7 +3569,6 @@ def format_frame_summary(self, frame_summary, colorize=False):
35743569
f' File "{__file__}", line {lno}, in f\n 1/0\n'
35753570
)
35763571

3577-
@unittest.expectedFailure # TODO: RUSTPYTHON; Actual: _should_show_carets(13, 14, ['# this line will be used during rendering'], None)
35783572
def test_summary_should_show_carets(self):
35793573
# See: https://github.com/python/cpython/issues/122353
35803574

@@ -3731,7 +3725,6 @@ def test_context(self):
37313725
self.assertEqual(type(exc_obj).__name__, exc.exc_type_str)
37323726
self.assertEqual(str(exc_obj), str(exc))
37333727

3734-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 11 not greater than 1000
37353728
def test_long_context_chain(self):
37363729
def f():
37373730
try:
@@ -4059,7 +4052,6 @@ def test_exception_group_format_exception_onlyi_recursive(self):
40594052

40604053
self.assertEqual(formatted, expected)
40614054

4062-
@unittest.expectedFailure # TODO: RUSTPYTHON; Diff is 2265 characters long. Set self.maxDiff to None to see it.
40634055
def test_exception_group_format(self):
40644056
teg = traceback.TracebackException.from_exception(self.eg)
40654057

@@ -4841,19 +4833,15 @@ class PurePythonSuggestionFormattingTests(
48414833
traceback printing in traceback.py.
48424834
"""
48434835

4844-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "'bluch'" not found in "ImportError: cannot import name 'blach'"
48454836
def test_import_from_suggestions_underscored(self):
48464837
return super().test_import_from_suggestions_underscored()
48474838

4848-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "'bluch'" not found in "ImportError: cannot import name 'blech'"
48494839
def test_import_from_suggestions_non_string(self):
48504840
return super().test_import_from_suggestions_non_string()
48514841

4852-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "'bluchin'?" not found in "ImportError: cannot import name 'bluch'"
48534842
def test_import_from_suggestions(self):
48544843
return super().test_import_from_suggestions()
48554844

4856-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'Did you mean' not found in "AttributeError: 'A' object has no attribute 'blich'"
48574845
def test_attribute_error_inside_nested_getattr(self):
48584846
return super().test_attribute_error_inside_nested_getattr()
48594847

@@ -4969,7 +4957,6 @@ class MyList(list):
49694957
class TestColorizedTraceback(unittest.TestCase):
49704958
maxDiff = None
49714959

4972-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "y = \x1b[31mx['a']['b']\x1b[0m\x1b[1;31m['c']\x1b[0m" not found in 'Traceback (most recent call last):\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4764\x1b[0m, in \x1b[35mtest_colorized_traceback\x1b[0m\n \x1b[31mbar\x1b[0m\x1b[1;31m()\x1b[0m\n \x1b[31m~~~\x1b[0m\x1b[1;31m^^\x1b[0m\n bar = <function TestColorizedTraceback.test_colorized_traceback.<locals>.bar at 0xb57b09180>\n baz1 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz1 at 0xb57b09e00>\n baz2 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz2 at 0xb57b09cc0>\n e = TypeError("\'NoneType\' object is not subscriptable")\n foo = <function TestColorizedTraceback.test_colorized_traceback.<locals>.foo at 0xb57b08140>\n self = <test.test_traceback.TestColorizedTraceback testMethod=test_colorized_traceback>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4760\x1b[0m, in \x1b[35mbar\x1b[0m\n return baz1(1,\n 2,3\n ,4)\n baz1 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz1 at 0xb57b09e00>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4757\x1b[0m, in \x1b[35mbaz1\x1b[0m\n return baz2(1,2,3,4)\n args = (1, 2, 3, 4)\n baz2 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz2 at 0xb57b09cc0>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4754\x1b[0m, in \x1b[35mbaz2\x1b[0m\n return \x1b[31m(lambda *args: foo(*args))\x1b[0m\x1b[1;31m(1,2,3,4)\x1b[0m\n \x1b[31m~~~~~~~~~~~~~~~~~~~~~~~~~~\x1b[0m\x1b[1;31m^^^^^^^^^\x1b[0m\n args = (1, 2, 3, 4)\n foo = <function TestColorizedTraceback.test_colorized_traceback.<locals>.foo at 0xb57b08140>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4754\x1b[0m, in \x1b[35m<lambda>\x1b[0m\n return (lambda *args: \x1b[31mfoo\x1b[0m\x1b[1;31m(*args)\x1b[0m)(1,2,3,4)\n \x1b[31m~~~\x1b[0m\x1b[1;31m^^^^^^^\x1b[0m\n args = (1, 2, 3, 4)\n foo = <function TestColorizedTraceback.test_colorized_traceback.<locals>.foo at 0xb57b08140>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4751\x1b[0m, in \x1b[35mfoo\x1b[0m\n y = x[\'a\'][\'b\'][\x1b[1;31m\'c\'\x1b[0m]\n \x1b[1;31m^^^\x1b[0m\n args = (1, 2, 3, 4)\n x = {\'a\': {\'b\': None}}\n\x1b[1;35mTypeError\x1b[0m: \x1b[35m\'NoneType\' object is not subscriptable\x1b[0m\n'
49734960
def test_colorized_traceback(self):
49744961
def foo(*args):
49754962
x = {'a':{'b': None}}
@@ -5002,7 +4989,6 @@ def bar():
50024989
self.assertIn("return baz1(1,\n 2,3\n ,4)", lines)
50034990
self.assertIn(red + "bar" + reset + boldr + "()" + reset, lines)
50044991

5005-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ' File \x1b[35m"<string>"\x1b[0m, line \x1b[35m1\x1b[0m\n a \x1b[1;31m$\x1b[0m b\n \x1b[1;31m^\x1b[0m\n\x1b[1;35mSyntaxError\x1b[0m: \x1b[35minvalid syntax\x1b[0m\n' not found in 'Traceback (most recent call last):\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4782\x1b[0m, in \x1b[35mtest_colorized_syntax_error\x1b[0m\n \x1b[31mcompile\x1b[0m\x1b[1;31m("a $ b", "<string>", "exec")\x1b[0m\n \x1b[31m~~~~~~~\x1b[0m\x1b[1;31m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\x1b[0m\n e = SyntaxError(\'got unexpected token $\')\n self = <test.test_traceback.TestColorizedTraceback testMethod=test_colorized_syntax_error>\n File \x1b[35m"<string>"\x1b[0m, line \x1b[35m1\x1b[0m\n a \x1b[1;31m$\x1b[0m b\n \x1b[1;31m^\x1b[0m\n\x1b[1;35mSyntaxError\x1b[0m: \x1b[35mgot unexpected token $\x1b[0m\n'
50064992
def test_colorized_syntax_error(self):
50074993
try:
50084994
compile("a $ b", "<string>", "exec")
@@ -5053,7 +5039,6 @@ def expected(t, m, fn, l, f, E, e, z):
50535039
]
50545040
self.assertEqual(actual, expected(**colors))
50555041

5056-
@unittest.expectedFailure # TODO: RUSTPYTHON; Diff is 1795 characters long. Set self.maxDiff to None to see it.
50575042
def test_colorized_traceback_from_exception_group(self):
50585043
def foo():
50595044
exceptions = []

crates/codegen/src/compile.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,9 @@ impl Compiler {
493493
slice: &ast::Expr,
494494
ctx: ast::ExprContext,
495495
) -> CompileResult<()> {
496+
// Save the subscript expression's source range
497+
let subscript_range = self.current_source_range;
498+
496499
// 1. Check subscripter and index for Load context
497500
// 2. VISIT value
498501
// 3. Handle two-element slice specially
@@ -515,6 +518,8 @@ impl Compiler {
515518
"should_use_slice_optimization should only return true for ast::Expr::Slice"
516519
),
517520
};
521+
// Restore full subscript expression range before emitting
522+
self.set_source_range(subscript_range);
518523
match ctx {
519524
ast::ExprContext::Load => {
520525
emit!(self, Instruction::BinarySlice);
@@ -527,6 +532,8 @@ impl Compiler {
527532
} else {
528533
// VISIT(c, expr, e->v.Subscript.slice)
529534
self.compile_expression(slice)?;
535+
// Restore full subscript expression range before emitting
536+
self.set_source_range(subscript_range);
530537

531538
// Emit appropriate instruction based on context
532539
match ctx {
@@ -6603,7 +6610,8 @@ impl Compiler {
66036610
self.compile_expression(left)?;
66046611
self.compile_expression(right)?;
66056612

6606-
// Perform operation:
6613+
// Restore full expression range before emitting the operation
6614+
self.set_source_range(range);
66076615
self.compile_op(op, false);
66086616
}
66096617
ast::Expr::Subscript(ast::ExprSubscript {
@@ -6614,7 +6622,8 @@ impl Compiler {
66146622
ast::Expr::UnaryOp(ast::ExprUnaryOp { op, operand, .. }) => {
66156623
self.compile_expression(operand)?;
66166624

6617-
// Perform operation:
6625+
// Restore full expression range before emitting the operation
6626+
self.set_source_range(range);
66186627
match op {
66196628
ast::UnaryOp::UAdd => emit!(
66206629
self,

crates/compiler/src/lib.rs

Lines changed: 155 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,75 @@ pub enum CompileError {
4646
impl CompileError {
4747
pub fn from_ruff_parse_error(error: parser::ParseError, source_file: &SourceFile) -> Self {
4848
let source_code = source_file.to_source_code();
49-
let location = source_code.source_location(error.location.start(), PositionEncoding::Utf8);
50-
let mut end_location =
51-
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
52-
53-
// If the error range ends at the start of a new line (column 1),
54-
// adjust it to the end of the previous line
55-
if end_location.character_offset.get() == 1 && end_location.line > location.line {
56-
// Get the end of the previous line
57-
let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1);
58-
end_location = source_code.source_location(prev_line_end, PositionEncoding::Utf8);
59-
// Adjust column to be after the last character
60-
end_location.character_offset = end_location.character_offset.saturating_add(1);
61-
}
49+
let source_text = source_file.source_text();
50+
51+
// For EOF errors (unclosed brackets), find the unclosed bracket position
52+
// and adjust both the error location and message
53+
let (error_type, location, end_location) = if matches!(
54+
&error.error,
55+
parser::ParseErrorType::Lexical(parser::LexicalErrorType::Eof)
56+
) {
57+
if let Some((bracket_char, bracket_offset)) =
58+
find_unclosed_bracket(source_text)
59+
{
60+
let bracket_text_size = ruff_text_size::TextSize::new(bracket_offset as u32);
61+
let loc =
62+
source_code.source_location(bracket_text_size, PositionEncoding::Utf8);
63+
let end_loc = SourceLocation {
64+
line: loc.line,
65+
character_offset: loc.character_offset.saturating_add(1),
66+
};
67+
let msg = format!("'{}' was never closed", bracket_char);
68+
(
69+
parser::ParseErrorType::OtherError(msg),
70+
loc,
71+
end_loc,
72+
)
73+
} else {
74+
let loc = source_code
75+
.source_location(error.location.start(), PositionEncoding::Utf8);
76+
let end_loc = source_code
77+
.source_location(error.location.end(), PositionEncoding::Utf8);
78+
(error.error, loc, end_loc)
79+
}
80+
} else if matches!(
81+
&error.error,
82+
parser::ParseErrorType::Lexical(parser::LexicalErrorType::IndentationError)
83+
) {
84+
// For IndentationError, point the offset to the end of the line content
85+
// (matching CPython behavior) instead of the beginning
86+
let loc =
87+
source_code.source_location(error.location.start(), PositionEncoding::Utf8);
88+
let line_idx = loc.line.to_zero_indexed();
89+
let line = source_text.split('\n').nth(line_idx).unwrap_or("");
90+
let line_end_col = line.len() + 1; // 1-indexed, past last char (the newline)
91+
let end_loc = SourceLocation {
92+
line: loc.line,
93+
character_offset: ruff_source_file::OneIndexed::new(line_end_col)
94+
.unwrap_or(loc.character_offset),
95+
};
96+
(error.error, end_loc, end_loc)
97+
} else {
98+
let loc =
99+
source_code.source_location(error.location.start(), PositionEncoding::Utf8);
100+
let mut end_loc =
101+
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
102+
103+
// If the error range ends at the start of a new line (column 1),
104+
// adjust it to the end of the previous line
105+
if end_loc.character_offset.get() == 1 && end_loc.line > loc.line {
106+
let prev_line_end =
107+
error.location.end() - ruff_text_size::TextSize::from(1);
108+
end_loc =
109+
source_code.source_location(prev_line_end, PositionEncoding::Utf8);
110+
end_loc.character_offset = end_loc.character_offset.saturating_add(1);
111+
}
112+
113+
(error.error, loc, end_loc)
114+
};
62115

63116
Self::Parse(ParseError {
64-
error: error.error,
117+
error: error_type,
65118
raw_location: error.location,
66119
location,
67120
end_location,
@@ -102,6 +155,94 @@ impl CompileError {
102155
}
103156
}
104157

158+
/// Find the last unclosed opening bracket in source code.
159+
/// Returns the bracket character and its byte offset, or None if all brackets are balanced.
160+
fn find_unclosed_bracket(source: &str) -> Option<(char, usize)> {
161+
let mut stack: Vec<(char, usize)> = Vec::new();
162+
let mut in_string = false;
163+
let mut string_quote = '\0';
164+
let mut triple_quote = false;
165+
let mut escape_next = false;
166+
167+
let chars: Vec<char> = source.chars().collect();
168+
let mut i = 0;
169+
let mut byte_offset = 0;
170+
171+
while i < chars.len() {
172+
let ch = chars[i];
173+
let ch_len = ch.len_utf8();
174+
175+
if escape_next {
176+
escape_next = false;
177+
byte_offset += ch_len;
178+
i += 1;
179+
continue;
180+
}
181+
182+
if in_string {
183+
if ch == '\\' {
184+
escape_next = true;
185+
} else if triple_quote {
186+
if ch == string_quote
187+
&& i + 2 < chars.len()
188+
&& chars[i + 1] == string_quote
189+
&& chars[i + 2] == string_quote
190+
{
191+
in_string = false;
192+
byte_offset += ch_len * 3;
193+
i += 3;
194+
continue;
195+
}
196+
} else if ch == string_quote {
197+
in_string = false;
198+
}
199+
byte_offset += ch_len;
200+
i += 1;
201+
continue;
202+
}
203+
204+
// Check for comments
205+
if ch == '#' {
206+
// Skip to end of line
207+
while i < chars.len() && chars[i] != '\n' {
208+
byte_offset += chars[i].len_utf8();
209+
i += 1;
210+
}
211+
continue;
212+
}
213+
214+
// Check for string start
215+
if ch == '\'' || ch == '"' {
216+
string_quote = ch;
217+
if i + 2 < chars.len() && chars[i + 1] == ch && chars[i + 2] == ch {
218+
triple_quote = true;
219+
in_string = true;
220+
byte_offset += ch_len * 3;
221+
i += 3;
222+
continue;
223+
}
224+
triple_quote = false;
225+
in_string = true;
226+
byte_offset += ch_len;
227+
i += 1;
228+
continue;
229+
}
230+
231+
match ch {
232+
'(' | '[' | '{' => stack.push((ch, byte_offset)),
233+
')' | ']' | '}' => {
234+
stack.pop();
235+
}
236+
_ => {}
237+
}
238+
239+
byte_offset += ch_len;
240+
i += 1;
241+
}
242+
243+
stack.last().copied()
244+
}
245+
105246
/// Compile a given source code into a bytecode object.
106247
pub fn compile(
107248
source: &str,

0 commit comments

Comments
 (0)