Skip to content

Commit c200694

Browse files
committed
Fix 6 test_fstring expectedFailure tests
- Add Unknown(char) variant to FormatType for proper error messages on unrecognized format codes (test_errors) - Strip comments from f-string debug text in compile.rs (test_debug_conversion) - Map ruff SyntaxError messages to match CPython in vm_new.rs: InvalidDeleteTarget, LineContinuationError, UnclosedStringError, OtherError(bytes mixing), OtherError(keyword identifier), FStringError(UnterminatedString/UnterminatedTripleQuotedString), and backtick-to-quote replacement for FStringError messages
1 parent ed11e00 commit c200694

File tree

4 files changed

+121
-11
lines changed

4 files changed

+121
-11
lines changed

Lib/test/test_fstring.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,6 @@ def test_ast_compile_time_concat(self):
587587
exec(c)
588588
self.assertEqual(x[0], 'foo3')
589589

590-
@unittest.expectedFailure # TODO: RUSTPYTHON
591590
def test_compile_time_concat_errors(self):
592591
self.assertAllRaise(SyntaxError,
593592
'cannot mix bytes and nonbytes literals',
@@ -600,7 +599,6 @@ def test_literal(self):
600599
self.assertEqual(f'a', 'a')
601600
self.assertEqual(f' ', ' ')
602601

603-
@unittest.expectedFailure # TODO: RUSTPYTHON
604602
def test_unterminated_string(self):
605603
self.assertAllRaise(SyntaxError, 'unterminated string',
606604
[r"""f'{"x'""",
@@ -1051,7 +1049,6 @@ def test_backslashes_in_expression_part(self):
10511049
["f'{\n}'",
10521050
])
10531051

1054-
@unittest.expectedFailure # TODO: RUSTPYTHON
10551052
def test_invalid_backslashes_inside_fstring_context(self):
10561053
# All of these variations are invalid python syntax,
10571054
# so they are also invalid in f-strings as well.
@@ -1418,7 +1415,6 @@ def test_assignment(self):
14181415
"f'{x}' = x",
14191416
])
14201417

1421-
@unittest.expectedFailure # TODO: RUSTPYTHON
14221418
def test_del(self):
14231419
self.assertAllRaise(SyntaxError, 'invalid syntax',
14241420
["del f''",
@@ -1524,7 +1520,6 @@ def test_str_format_differences(self):
15241520
self.assertEqual('{d[a]}'.format(d=d), 'string')
15251521
self.assertEqual('{d[0]}'.format(d=d), 'integer')
15261522

1527-
@unittest.expectedFailure # TODO: RUSTPYTHON
15281523
def test_errors(self):
15291524
# see issue 26287
15301525
self.assertAllRaise(TypeError, 'unsupported',
@@ -1567,7 +1562,6 @@ def test_backslash_char(self):
15671562
self.assertEqual(eval('f"\\\n"'), '')
15681563
self.assertEqual(eval('f"\\\r"'), '')
15691564

1570-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: '1+2 = # my comment\n 3' != '1+2 = \n 3'
15711565
def test_debug_conversion(self):
15721566
x = 'A string'
15731567
self.assertEqual(f'{x=}', 'x=' + repr(x))

crates/codegen/src/compile.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8450,7 +8450,12 @@ impl Compiler {
84508450
if let Some(ast::DebugText { leading, trailing }) = &fstring_expr.debug_text {
84518451
let range = fstring_expr.expression.range();
84528452
let source = self.source_file.slice(range);
8453-
let text = [leading, source, trailing].concat();
8453+
let text = [
8454+
strip_fstring_debug_comments(leading).as_str(),
8455+
source,
8456+
strip_fstring_debug_comments(trailing).as_str(),
8457+
]
8458+
.concat();
84548459

84558460
self.emit_load_const(ConstantData::Str { value: text.into() });
84568461
element_count += 1;
@@ -8786,6 +8791,27 @@ impl ToU32 for usize {
87868791
}
87878792
}
87888793

8794+
/// Strip Python comments from f-string debug text (leading/trailing around `=`).
8795+
/// A comment starts with `#` and extends to the end of the line.
8796+
/// The newline character itself is preserved.
8797+
fn strip_fstring_debug_comments(text: &str) -> String {
8798+
let mut result = String::with_capacity(text.len());
8799+
let mut in_comment = false;
8800+
for ch in text.chars() {
8801+
if in_comment {
8802+
if ch == '\n' {
8803+
in_comment = false;
8804+
result.push(ch);
8805+
}
8806+
} else if ch == '#' {
8807+
in_comment = true;
8808+
} else {
8809+
result.push(ch);
8810+
}
8811+
}
8812+
result
8813+
}
8814+
87898815
#[cfg(test)]
87908816
mod ruff_tests {
87918817
use super::*;

crates/common/src/format.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ pub enum FormatType {
149149
GeneralFormat(Case),
150150
FixedPoint(Case),
151151
Percentage,
152+
Unknown(char),
152153
}
153154

154155
impl From<&FormatType> for char {
@@ -170,6 +171,7 @@ impl From<&FormatType> for char {
170171
FormatType::FixedPoint(Case::Lower) => 'f',
171172
FormatType::FixedPoint(Case::Upper) => 'F',
172173
FormatType::Percentage => '%',
174+
FormatType::Unknown(c) => *c,
173175
}
174176
}
175177
}
@@ -194,6 +196,7 @@ impl FormatParse for FormatType {
194196
Some('g') => (Some(Self::GeneralFormat(Case::Lower)), chars.as_wtf8()),
195197
Some('G') => (Some(Self::GeneralFormat(Case::Upper)), chars.as_wtf8()),
196198
Some('%') => (Some(Self::Percentage), chars.as_wtf8()),
199+
Some(c) => (Some(Self::Unknown(c)), chars.as_wtf8()),
197200
_ => (None, text),
198201
}
199202
}
@@ -429,7 +432,8 @@ impl FormatSpec {
429432
| FormatType::FixedPoint(_)
430433
| FormatType::GeneralFormat(_)
431434
| FormatType::Exponent(_)
432-
| FormatType::Percentage,
435+
| FormatType::Percentage
436+
| FormatType::Number(_),
433437
) => 3,
434438
None => 3,
435439
_ => panic!("Separators only valid for numbers!"),
@@ -475,6 +479,7 @@ impl FormatSpec {
475479
let first_letter = (input.to_string().as_bytes()[0] as char).to_uppercase();
476480
Ok(first_letter.collect::<String>() + &input.to_string()[1..])
477481
}
482+
Some(FormatType::Unknown(c)) => Err(FormatSpecError::UnknownFormatCode(*c, "int")),
478483
_ => Err(FormatSpecError::InvalidFormatSpecifier),
479484
}
480485
}
@@ -496,7 +501,8 @@ impl FormatSpec {
496501
| Some(FormatType::Hex(_))
497502
| Some(FormatType::String)
498503
| Some(FormatType::Character)
499-
| Some(FormatType::Number(Case::Upper)) => {
504+
| Some(FormatType::Number(Case::Upper))
505+
| Some(FormatType::Unknown(_)) => {
500506
let ch = char::from(self.format_type.as_ref().unwrap());
501507
Err(FormatSpecError::UnknownFormatCode(ch, "float"))
502508
}
@@ -609,6 +615,7 @@ impl FormatSpec {
609615
Some(float) => return self.format_float(float),
610616
_ => Err(FormatSpecError::UnableToConvert),
611617
},
618+
Some(FormatType::Unknown(c)) => Err(FormatSpecError::UnknownFormatCode(c, "int")),
612619
None => self.format_int_radix(magnitude, 10),
613620
}?;
614621
let format_sign = self.sign.unwrap_or(FormatSign::Minus);
@@ -707,7 +714,8 @@ impl FormatSpec {
707714
| Some(FormatType::String)
708715
| Some(FormatType::Character)
709716
| Some(FormatType::Number(Case::Upper))
710-
| Some(FormatType::Percentage) => {
717+
| Some(FormatType::Percentage)
718+
| Some(FormatType::Unknown(_)) => {
711719
let ch = char::from(self.format_type.as_ref().unwrap());
712720
Err(FormatSpecError::UnknownFormatCode(ch, "complex"))
713721
}

crates/vm/src/vm/vm_new.rs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,11 +503,52 @@ impl VirtualMachine {
503503
}
504504
let mut narrow_caret = false;
505505
match error {
506+
#[cfg(feature = "parser")]
507+
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
508+
error:
509+
ruff_python_parser::ParseErrorType::FStringError(
510+
ruff_python_parser::InterpolatedStringErrorType::UnterminatedString,
511+
)
512+
| ruff_python_parser::ParseErrorType::Lexical(
513+
ruff_python_parser::LexicalErrorType::FStringError(
514+
ruff_python_parser::InterpolatedStringErrorType::UnterminatedString,
515+
),
516+
),
517+
..
518+
}) => {
519+
msg = "unterminated f-string literal".to_owned();
520+
}
521+
#[cfg(feature = "parser")]
522+
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
523+
error:
524+
ruff_python_parser::ParseErrorType::FStringError(
525+
ruff_python_parser::InterpolatedStringErrorType::UnterminatedTripleQuotedString,
526+
)
527+
| ruff_python_parser::ParseErrorType::Lexical(
528+
ruff_python_parser::LexicalErrorType::FStringError(
529+
ruff_python_parser::InterpolatedStringErrorType::UnterminatedTripleQuotedString,
530+
),
531+
),
532+
..
533+
}) => {
534+
msg = "unterminated triple-quoted f-string literal".to_owned();
535+
}
506536
#[cfg(feature = "parser")]
507537
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
508538
error:
509539
ruff_python_parser::ParseErrorType::FStringError(_)
510-
| ruff_python_parser::ParseErrorType::UnexpectedExpressionToken,
540+
| ruff_python_parser::ParseErrorType::Lexical(
541+
ruff_python_parser::LexicalErrorType::FStringError(_),
542+
),
543+
..
544+
}) => {
545+
// Replace backticks with single quotes to match CPython's error messages
546+
msg = msg.replace('`', "'");
547+
msg.insert_str(0, "invalid syntax: ");
548+
}
549+
#[cfg(feature = "parser")]
550+
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
551+
error: ruff_python_parser::ParseErrorType::UnexpectedExpressionToken,
511552
..
512553
}) => msg.insert_str(0, "invalid syntax: "),
513554
#[cfg(feature = "parser")]
@@ -532,6 +573,47 @@ impl VirtualMachine {
532573
msg = "invalid syntax".to_owned();
533574
narrow_caret = true;
534575
}
576+
#[cfg(feature = "parser")]
577+
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
578+
error: ruff_python_parser::ParseErrorType::InvalidDeleteTarget,
579+
..
580+
}) => {
581+
msg = "invalid syntax".to_owned();
582+
}
583+
#[cfg(feature = "parser")]
584+
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
585+
error:
586+
ruff_python_parser::ParseErrorType::Lexical(
587+
ruff_python_parser::LexicalErrorType::LineContinuationError,
588+
),
589+
..
590+
}) => {
591+
msg = "unexpected character after line continuation".to_owned();
592+
}
593+
#[cfg(feature = "parser")]
594+
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
595+
error:
596+
ruff_python_parser::ParseErrorType::Lexical(
597+
ruff_python_parser::LexicalErrorType::UnclosedStringError,
598+
),
599+
..
600+
}) => {
601+
msg = "unterminated string".to_owned();
602+
}
603+
#[cfg(feature = "parser")]
604+
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
605+
error: ruff_python_parser::ParseErrorType::OtherError(s),
606+
..
607+
}) if s.eq_ignore_ascii_case("bytes literal cannot be mixed with non-bytes literals") => {
608+
msg = "cannot mix bytes and nonbytes literals".to_owned();
609+
}
610+
#[cfg(feature = "parser")]
611+
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
612+
error: ruff_python_parser::ParseErrorType::OtherError(s),
613+
..
614+
}) if s.starts_with("Expected an identifier, but found a keyword") => {
615+
msg = "invalid syntax".to_owned();
616+
}
535617
_ => {}
536618
}
537619
if syntax_error_type.is(self.ctx.exceptions.tab_error) {

0 commit comments

Comments
 (0)