Align more error messages with CPython 3.14.5 (#7988)

* Cannot assign to True/False/None

* cannot use assignment expressions with ...

* cannot assign to function call/expression
This commit is contained in:
Shahar Naveh authored and GitHub committed 2026-05-28 11:19:03 +09:00
1 parent f347ae4fbb
commit fbdf7c1052
5 files changed
+181 -95

No files matched your search

+1 -1
View File
@@ -2525,7 +2525,7 @@ Traceback (most recent call last):
...
SyntaxError: 'yield from' outside function
>>> def f(): x = yield = y # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE
>>> def f(): x = yield = y
Traceback (most recent call last):
...
SyntaxError: assignment to yield expression not possible
+1 -1
View File
@@ -154,7 +154,7 @@ Verify re-use of tuples (a side benefit of using genexps over listcomps)
Verify that syntax error's are raised for genexps used as lvalues
>>> (y for y in (1,2)) = 10 # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE
>>> (y for y in (1,2)) = 10
Traceback (most recent call last):
...
SyntaxError: cannot assign to generator expression
-1
View File
@@ -90,7 +90,6 @@ class NamedExpressionInvalidTest(unittest.TestCase):
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {})
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_15(self):
code = """(lambda: x := 1)"""
+43 -43
View File
@@ -31,7 +31,7 @@ Errors from set_context():
Traceback (most recent call last):
SyntaxError: invalid syntax
>>> None = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> None = 1
Traceback (most recent call last):
SyntaxError: cannot assign to None
@@ -39,11 +39,11 @@ SyntaxError: cannot assign to None
Traceback (most recent call last):
SyntaxError: invalid syntax
>>> True = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> True = 1
Traceback (most recent call last):
SyntaxError: cannot assign to True
>>> (True := 1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> (True := 1)
Traceback (most recent call last):
SyntaxError: cannot use assignment expressions with True
@@ -79,7 +79,7 @@ SyntaxError: cannot delete __debug__
Traceback (most recent call last):
SyntaxError: cannot assign to function call here. Maybe you meant '==' instead of '='?
>>> yield = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> yield = 1
Traceback (most recent call last):
SyntaxError: assignment to yield expression not possible
@@ -91,23 +91,23 @@ SyntaxError: cannot delete function call
Traceback (most recent call last):
SyntaxError: cannot assign to expression here. Maybe you meant '==' instead of '='?
>>> (x for x in x) = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> (x for x in x) = 1
Traceback (most recent call last):
SyntaxError: cannot assign to generator expression
>>> 1 = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> 1 = 1
Traceback (most recent call last):
SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='?
>>> "abc" = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> "abc" = 1
Traceback (most recent call last):
SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='?
>>> b"" = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> b"" = 1
Traceback (most recent call last):
SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='?
>>> ... = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> ... = 1
Traceback (most recent call last):
SyntaxError: cannot assign to ellipsis here. Maybe you meant '==' instead of '='?
@@ -124,7 +124,7 @@ them.
Traceback (most recent call last):
SyntaxError: cannot assign to literal
>>> (a, True, c) = (1, 2, 3) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> (a, True, c) = (1, 2, 3)
Traceback (most recent call last):
SyntaxError: cannot assign to True
@@ -132,7 +132,7 @@ SyntaxError: cannot assign to True
Traceback (most recent call last):
SyntaxError: cannot assign to __debug__
>>> (a, *True, c) = (1, 2, 3) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> (a, *True, c) = (1, 2, 3)
Traceback (most recent call last):
SyntaxError: cannot assign to True
@@ -140,19 +140,19 @@ SyntaxError: cannot assign to True
Traceback (most recent call last):
SyntaxError: cannot assign to __debug__
>>> [a, b, c + 1] = [1, 2, 3] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> [a, b, c + 1] = [1, 2, 3]
Traceback (most recent call last):
SyntaxError: cannot assign to expression
>>> [a, b[1], c + 1] = [1, 2, 3] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> [a, b[1], c + 1] = [1, 2, 3]
Traceback (most recent call last):
SyntaxError: cannot assign to expression
>>> [a, b.c.d, c + 1] = [1, 2, 3] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> [a, b.c.d, c + 1] = [1, 2, 3]
Traceback (most recent call last):
SyntaxError: cannot assign to expression
>>> a if 1 else b = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> a if 1 else b = 1
Traceback (most recent call last):
SyntaxError: cannot assign to conditional expression
@@ -188,15 +188,15 @@ SyntaxError: expected expression before 'if', but statement is given
Traceback (most recent call last):
SyntaxError: invalid syntax
>>> True = True = 3 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> True = True = 3
Traceback (most recent call last):
SyntaxError: cannot assign to True
>>> x = y = True = z = 3 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> x = y = True = z = 3
Traceback (most recent call last):
SyntaxError: cannot assign to True
>>> x = y = yield = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> x = y = yield = 1
Traceback (most recent call last):
SyntaxError: assignment to yield expression not possible
@@ -215,31 +215,31 @@ SyntaxError: 'list' is an illegal expression for augmented assignment
Invalid targets in `for` loops and `with` statements should also
produce a specialized error message
>>> for a() in b: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> for a() in b: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call
>>> for (a, b()) in b: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> for (a, b()) in b: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call
>>> for [a, b()] in b: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> for [a, b()] in b: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call
>>> for (*a, b, c+1) in b: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> for (*a, b, c+1) in b: pass
Traceback (most recent call last):
SyntaxError: cannot assign to expression
>>> for (x, *(y, z.d())) in b: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> for (x, *(y, z.d())) in b: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call
>>> for a, b() in c: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> for a, b() in c: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call
>>> for a, b, (c + 1, d()): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> for a, b, (c + 1, d()): pass
Traceback (most recent call last):
SyntaxError: cannot assign to expression
@@ -251,27 +251,27 @@ SyntaxError: invalid syntax
Traceback (most recent call last):
SyntaxError: invalid syntax
>>> with a as b(): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> with a as b(): pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call
>>> with a as (b, c()): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> with a as (b, c()): pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call
>>> with a as [b, c()]: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> with a as [b, c()]: pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call
>>> with a as (*b, c, d+1): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> with a as (*b, c, d+1): pass
Traceback (most recent call last):
SyntaxError: cannot assign to expression
>>> with a as (x, *(y, z.d())): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> with a as (x, *(y, z.d())): pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call
>>> with a as b, c as d(): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> with a as b, c as d(): pass
Traceback (most recent call last):
SyntaxError: cannot assign to function call
@@ -293,11 +293,11 @@ SyntaxError: 'in' expected after for-loop variables
Traceback (most recent call last):
SyntaxError: 'in' expected after for-loop variables
>>> [x for x() in a] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> [x for x() in a]
Traceback (most recent call last):
SyntaxError: cannot assign to function call
>>> [x for a, b, (c + 1, d()) in y] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> [x for a, b, (c + 1, d()) in y]
Traceback (most recent call last):
SyntaxError: cannot assign to expression
@@ -305,11 +305,11 @@ SyntaxError: cannot assign to expression
Traceback (most recent call last):
SyntaxError: 'in' expected after for-loop variables
>>> [x for x+1 in y] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> [x for x+1 in y]
Traceback (most recent call last):
SyntaxError: cannot assign to expression
>>> [x for x+1, x() in y] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> [x for x+1, x() in y]
Traceback (most recent call last):
SyntaxError: cannot assign to expression
@@ -334,19 +334,19 @@ SyntaxError: invalid syntax. Is this intended to be part of the string?
# produce special error messages regarding missing
# parentheses, but about missing commas instead
>>> [1, 2 3] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> [1, 2 3]
Traceback (most recent call last):
SyntaxError: invalid syntax. Perhaps you forgot a comma?
>>> {1, 2 3} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> {1, 2 3}
Traceback (most recent call last):
SyntaxError: invalid syntax. Perhaps you forgot a comma?
>>> {1:2, 2:5 3:12} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> {1:2, 2:5 3:12}
Traceback (most recent call last):
SyntaxError: invalid syntax. Perhaps you forgot a comma?
>>> (1, 2 3) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> (1, 2 3)
Traceback (most recent call last):
SyntaxError: invalid syntax. Perhaps you forgot a comma?
@@ -2114,19 +2114,19 @@ SyntaxError: cannot use subscript as import target
# Check that we don't raise a "cannot use name as import target" error
# if there is an error in an unrelated statement after ';'
>>> import a as b; None = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> import a as b; None = 1
Traceback (most recent call last):
SyntaxError: cannot assign to None
>>> import a, b as c; d = 1; None = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> import a, b as c; d = 1; None = 1
Traceback (most recent call last):
SyntaxError: cannot assign to None
>>> from a import b as c; None = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> from a import b as c; None = 1
Traceback (most recent call last):
SyntaxError: cannot assign to None
>>> from a import b, c as d; e = 1; None = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE
>>> from a import b, c as d; e = 1; None = 1
Traceback (most recent call last):
SyntaxError: cannot assign to None
+136 -49
View File
@@ -1,4 +1,9 @@
pub use ruff_python_ast::token::TokenKind;
use ruff_python_parser::{LexicalErrorType, ParseErrorType};
use ruff_source_file::{PositionEncoding, SourceFile, SourceFileBuilder, SourceLocation};
use ruff_text_size::TextSlice;
use thiserror::Error;
use rustpython_codegen::{compile, symboltable};
pub use rustpython_codegen::compile::CompileOpts;
@@ -9,20 +14,19 @@ pub use ruff_python_ast as ast;
pub use ruff_python_parser as parser;
pub use rustpython_codegen as codegen;
pub use rustpython_compiler_core as core;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CompileErrorType {
#[error(transparent)]
Codegen(#[from] codegen::error::CodegenErrorType),
#[error(transparent)]
Parse(#[from] parser::ParseErrorType),
Parse(#[from] ParseErrorType),
}
#[derive(Error, Debug)]
pub struct ParseError {
#[source]
pub error: parser::ParseErrorType,
pub error: ParseErrorType,
pub raw_location: ruff_text_size::TextRange,
pub location: SourceLocation,
pub end_location: SourceLocation,
@@ -54,57 +58,140 @@ impl CompileError {
// For EOF errors (unclosed brackets), find the unclosed bracket position
// and adjust both the error location and message
let mut is_unclosed_bracket = false;
let (error_type, location, end_location) = if matches!(
&error.error,
parser::ParseErrorType::Lexical(parser::LexicalErrorType::Eof)
) {
if let Some((bracket_char, bracket_offset)) = find_unclosed_bracket(source_text) {
let bracket_text_size = ruff_text_size::TextSize::new(bracket_offset as u32);
let loc = source_code.source_location(bracket_text_size, PositionEncoding::Utf8);
let end_loc = SourceLocation {
line: loc.line,
character_offset: loc.character_offset.saturating_add(1),
};
let msg = format!("'{bracket_char}' was never closed");
is_unclosed_bracket = true;
(parser::ParseErrorType::OtherError(msg), loc, end_loc)
} else {
let (error_type, location, end_location) = match &error.error {
ParseErrorType::Lexical(LexicalErrorType::Eof) => {
if let Some((bracket_char, bracket_offset)) = find_unclosed_bracket(source_text) {
let bracket_text_size = ruff_text_size::TextSize::new(bracket_offset as u32);
let loc =
source_code.source_location(bracket_text_size, PositionEncoding::Utf8);
let end_loc = SourceLocation {
line: loc.line,
character_offset: loc.character_offset.saturating_add(1),
};
let msg = format!("'{bracket_char}' was never closed");
is_unclosed_bracket = true;
(ParseErrorType::OtherError(msg), loc, end_loc)
} else {
let loc =
source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let end_loc =
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
(error.error, loc, end_loc)
}
}
ParseErrorType::Lexical(LexicalErrorType::IndentationError) => {
// For IndentationError, point the offset to the end of the line content
// instead of the beginning
let loc =
source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let end_loc =
let line_idx = loc.line.to_zero_indexed();
let line = source_text.split('\n').nth(line_idx).unwrap_or("");
let line_end_col = line.chars().count() + 1; // 1-indexed, past last char
let end_loc = SourceLocation {
line: loc.line,
character_offset: ruff_source_file::OneIndexed::new(line_end_col)
.unwrap_or(loc.character_offset),
};
(error.error, end_loc, end_loc)
}
ParseErrorType::ExpectedToken { expected, found }
if matches!((expected, found), (TokenKind::Comma, TokenKind::Int)) =>
{
let loc =
source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let mut end_loc =
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
// If the error range ends at the start of a new line (column 1),
// adjust it to the end of the previous line
if end_loc.character_offset.get() == 1 && end_loc.line > loc.line {
let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1);
end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8);
end_loc.character_offset = end_loc.character_offset.saturating_add(1);
}
let msg = "invalid syntax. Perhaps you forgot a comma?".into();
(ParseErrorType::OtherError(msg), loc, end_loc)
}
ParseErrorType::InvalidAssignmentTarget => {
let loc =
source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let mut end_loc =
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
// If the error range ends at the start of a new line (column 1),
// adjust it to the end of the previous line
if end_loc.character_offset.get() == 1 && end_loc.line > loc.line {
let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1);
end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8);
end_loc.character_offset = end_loc.character_offset.saturating_add(1);
}
let expr_str = source_file.source_text().slice(error.location);
let msg = parser::parse_expression(expr_str).map_or_else(
|_| match expr_str {
"yield" => "assignment to yield expression not possible".into(),
_ => format!("cannot assign to {expr_str}"),
},
|parsed| match *parsed.syntax().body {
ast::Expr::Call(_) => "cannot assign to function call".into(),
ast::Expr::BinOp(_) => "cannot assign to expression".into(),
ast::Expr::If(_) => "cannot assign to conditional expression".into(),
ast::Expr::Generator(_) => "cannot assign to generator expression".into(),
ast::Expr::StringLiteral(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::NumberLiteral(_) => {
"cannot assign to literal here. Maybe you meant '==' instead of '='?"
.into()
}
ast::Expr::EllipsisLiteral(_) => {
"cannot assign to ellipsis here. Maybe you meant '==' instead of '='?"
.into()
}
_ => format!("cannot assign to {expr_str}"),
},
);
(ParseErrorType::OtherError(msg), loc, end_loc)
}
ParseErrorType::InvalidNamedAssignmentTarget => {
let loc =
source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let mut end_loc =
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
// If the error range ends at the start of a new line (column 1),
// adjust it to the end of the previous line
if end_loc.character_offset.get() == 1 && end_loc.line > loc.line {
let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1);
end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8);
end_loc.character_offset = end_loc.character_offset.saturating_add(1);
}
let target = source_file.source_text().slice(error.location);
let msg = format!("cannot use assignment expressions with {target}");
(ParseErrorType::OtherError(msg), loc, end_loc)
}
_ => {
let loc =
source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let mut end_loc =
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
// If the error range ends at the start of a new line (column 1),
// adjust it to the end of the previous line
if end_loc.character_offset.get() == 1 && end_loc.line > loc.line {
let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1);
end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8);
end_loc.character_offset = end_loc.character_offset.saturating_add(1);
}
(error.error, loc, end_loc)
}
} else if matches!(
&error.error,
parser::ParseErrorType::Lexical(parser::LexicalErrorType::IndentationError)
) {
// For IndentationError, point the offset to the end of the line content
// instead of the beginning
let loc = source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let line_idx = loc.line.to_zero_indexed();
let line = source_text.split('\n').nth(line_idx).unwrap_or("");
let line_end_col = line.chars().count() + 1; // 1-indexed, past last char
let end_loc = SourceLocation {
line: loc.line,
character_offset: ruff_source_file::OneIndexed::new(line_end_col)
.unwrap_or(loc.character_offset),
};
(error.error, end_loc, end_loc)
} else {
let loc = source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let mut end_loc =
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
// If the error range ends at the start of a new line (column 1),
// adjust it to the end of the previous line
if end_loc.character_offset.get() == 1 && end_loc.line > loc.line {
let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1);
end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8);
end_loc.character_offset = end_loc.character_offset.saturating_add(1);
}
(error.error, loc, end_loc)
};
Self::Parse(ParseError {