Skip to content

Commit b87386f

Browse files
authored
Update test_fstring from v3.14.3 and impl more (#7164)
* Update test_fstring from v3.14.3 * 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 * Fix clippy::sliced_string_as_bytes warning --------- Co-authored-by: CPython Developers <>
1 parent e3c533a commit b87386f

File tree

5 files changed

+498
-44
lines changed

5 files changed

+498
-44
lines changed

Lib/test/test_fstring.py

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ def test_ast_line_numbers_multiline_fstring(self):
383383
self.assertEqual(t.body[0].value.values[1].value.col_offset, 11)
384384
self.assertEqual(t.body[0].value.values[1].value.end_col_offset, 16)
385385

386-
@unittest.expectedFailure # TODO: RUSTPYTHON
386+
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 4 != 5
387387
def test_ast_line_numbers_with_parentheses(self):
388388
expr = """
389389
x = (
@@ -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'""",
@@ -609,7 +607,7 @@ def test_unterminated_string(self):
609607
r"""f'{("x}'""",
610608
])
611609

612-
@unittest.expectedFailure # TODO: RUSTPYTHON
610+
@unittest.expectedFailure # TODO: RUSTPYTHON
613611
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
614612
def test_mismatched_parens(self):
615613
self.assertAllRaise(SyntaxError, r"closing parenthesis '\}' "
@@ -632,24 +630,35 @@ def test_mismatched_parens(self):
632630
r"does not match opening parenthesis '\('",
633631
["f'{a(4}'",
634632
])
635-
self.assertRaises(SyntaxError, eval, "f'{" + "("*500 + "}'")
633+
self.assertRaises(SyntaxError, eval, "f'{" + "("*20 + "}'")
636634

637-
@unittest.expectedFailure # TODO: RUSTPYTHON
635+
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: No exception raised
638636
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
639637
def test_fstring_nested_too_deeply(self):
640-
self.assertAllRaise(SyntaxError,
641-
"f-string: expressions nested too deeply",
642-
['f"{1+2:{1+2:{1+1:{1}}}}"'])
638+
def raises_syntax_or_memory_error(txt):
639+
try:
640+
eval(txt)
641+
except SyntaxError:
642+
pass
643+
except MemoryError:
644+
pass
645+
except Exception as ex:
646+
self.fail(f"Should raise SyntaxError or MemoryError, not {type(ex)}")
647+
else:
648+
self.fail("No exception raised")
649+
650+
raises_syntax_or_memory_error('f"{1+2:{1+2:{1+1:{1}}}}"')
643651

644652
def create_nested_fstring(n):
645653
if n == 0:
646654
return "1+1"
647655
prev = create_nested_fstring(n-1)
648656
return f'f"{{{prev}}}"'
649657

650-
self.assertAllRaise(SyntaxError,
651-
"too many nested f-strings",
652-
[create_nested_fstring(160)])
658+
raises_syntax_or_memory_error(create_nested_fstring(160))
659+
raises_syntax_or_memory_error("f'{" + "("*100 + "}'")
660+
raises_syntax_or_memory_error("f'{" + "("*1000 + "}'")
661+
raises_syntax_or_memory_error("f'{" + "("*10_000 + "}'")
653662

654663
def test_syntax_error_in_nested_fstring(self):
655664
# See gh-104016 for more information on this crash
@@ -692,7 +701,7 @@ def test_double_braces(self):
692701
["f'{ {{}} }'", # dict in a set
693702
])
694703

695-
@unittest.expectedFailure # TODO: RUSTPYTHON
704+
@unittest.expectedFailure # TODO: RUSTPYTHON
696705
def test_compile_time_concat(self):
697706
x = 'def'
698707
self.assertEqual('abc' f'## {x}ghi', 'abc## defghi')
@@ -730,7 +739,7 @@ def test_compile_time_concat(self):
730739
['''f'{3' f"}"''', # can't concat to get a valid f-string
731740
])
732741

733-
@unittest.expectedFailure # TODO: RUSTPYTHON
742+
@unittest.expectedFailure # TODO: RUSTPYTHON
734743
def test_comments(self):
735744
# These aren't comments, since they're in strings.
736745
d = {'#': 'hash'}
@@ -807,7 +816,7 @@ def build_fstr(n, extra=''):
807816
s = "f'{1}' 'x' 'y'" * 1024
808817
self.assertEqual(eval(s), '1xy' * 1024)
809818

810-
@unittest.expectedFailure # TODO: RUSTPYTHON
819+
@unittest.expectedFailure # TODO: RUSTPYTHON
811820
def test_format_specifier_expressions(self):
812821
width = 10
813822
precision = 4
@@ -841,7 +850,6 @@ def test_format_specifier_expressions(self):
841850
"""f'{"s"!{"r"}}'""",
842851
])
843852

844-
@unittest.expectedFailure # TODO: RUSTPYTHON
845853
def test_custom_format_specifier(self):
846854
class CustomFormat:
847855
def __format__(self, format_spec):
@@ -863,7 +871,7 @@ def __format__(self, spec):
863871
x = X()
864872
self.assertEqual(f'{x} {x}', '1 2')
865873

866-
@unittest.expectedFailure # TODO: RUSTPYTHON
874+
@unittest.expectedFailure # TODO: RUSTPYTHON
867875
def test_missing_expression(self):
868876
self.assertAllRaise(SyntaxError,
869877
"f-string: valid expression required before '}'",
@@ -926,7 +934,7 @@ def test_missing_expression(self):
926934
"\xa0",
927935
])
928936

929-
@unittest.expectedFailure # TODO: RUSTPYTHON
937+
@unittest.expectedFailure # TODO: RUSTPYTHON
930938
def test_parens_in_expressions(self):
931939
self.assertEqual(f'{3,}', '(3,)')
932940

@@ -939,13 +947,12 @@ def test_parens_in_expressions(self):
939947
["f'{3)+(4}'",
940948
])
941949

942-
@unittest.expectedFailure # TODO: RUSTPYTHON
950+
@unittest.expectedFailure # TODO: RUSTPYTHON
943951
def test_newlines_before_syntax_error(self):
944952
self.assertAllRaise(SyntaxError,
945953
"f-string: expecting a valid expression after '{'",
946954
["f'{.}'", "\nf'{.}'", "\n\nf'{.}'"])
947955

948-
@unittest.expectedFailure # TODO: RUSTPYTHON
949956
def test_backslashes_in_string_part(self):
950957
self.assertEqual(f'\t', '\t')
951958
self.assertEqual(r'\t', '\\t')
@@ -1004,7 +1011,7 @@ def test_backslashes_in_string_part(self):
10041011
self.assertEqual(fr'\N{AMPERSAND}', '\\Nspam')
10051012
self.assertEqual(f'\\\N{AMPERSAND}', '\\&')
10061013

1007-
@unittest.expectedFailure # TODO: RUSTPYTHON
1014+
@unittest.expectedFailure # TODO: RUSTPYTHON
10081015
def test_misformed_unicode_character_name(self):
10091016
# These test are needed because unicode names are parsed
10101017
# differently inside f-strings.
@@ -1024,7 +1031,7 @@ def test_misformed_unicode_character_name(self):
10241031
r"'\N{GREEK CAPITAL LETTER DELTA'",
10251032
])
10261033

1027-
@unittest.expectedFailure # TODO: RUSTPYTHON
1034+
@unittest.expectedFailure # TODO: RUSTPYTHON
10281035
def test_backslashes_in_expression_part(self):
10291036
self.assertEqual(f"{(
10301037
1 +
@@ -1040,7 +1047,6 @@ def test_backslashes_in_expression_part(self):
10401047
["f'{\n}'",
10411048
])
10421049

1043-
@unittest.expectedFailure # TODO: RUSTPYTHON
10441050
def test_invalid_backslashes_inside_fstring_context(self):
10451051
# All of these variations are invalid python syntax,
10461052
# so they are also invalid in f-strings as well.
@@ -1075,7 +1081,7 @@ def test_newlines_in_expressions(self):
10751081
self.assertEqual(rf'''{3+
10761082
4}''', '7')
10771083

1078-
@unittest.expectedFailure # TODO: RUSTPYTHON
1084+
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "f-string: expecting a valid expression after '{'" does not match "invalid syntax (<string>, line 1)"
10791085
def test_lambda(self):
10801086
x = 5
10811087
self.assertEqual(f'{(lambda y:x*y)("8")!r}', "'88888'")
@@ -1118,7 +1124,6 @@ def test_roundtrip_raw_quotes(self):
11181124
self.assertEqual(fr'\'\"\'', '\\\'\\"\\\'')
11191125
self.assertEqual(fr'\"\'\"\'', '\\"\\\'\\"\\\'')
11201126

1121-
@unittest.expectedFailure # TODO: RUSTPYTHON
11221127
def test_fstring_backslash_before_double_bracket(self):
11231128
deprecated_cases = [
11241129
(r"f'\{{\}}'", '\\{\\}'),
@@ -1138,7 +1143,6 @@ def test_fstring_backslash_before_double_bracket(self):
11381143
self.assertEqual(fr'\}}{1+1}', '\\}2')
11391144
self.assertEqual(fr'{1+1}\}}', '2\\}')
11401145

1141-
@unittest.expectedFailure # TODO: RUSTPYTHON
11421146
def test_fstring_backslash_before_double_bracket_warns_once(self):
11431147
with self.assertWarns(SyntaxWarning) as w:
11441148
eval(r"f'\{{'")
@@ -1288,6 +1292,7 @@ def test_nested_fstrings(self):
12881292
self.assertEqual(f'{f"{0}"*3}', '000')
12891293
self.assertEqual(f'{f"{y}"*3}', '555')
12901294

1295+
@unittest.expectedFailure # TODO: RUSTPYTHON
12911296
def test_invalid_string_prefixes(self):
12921297
single_quote_cases = ["fu''",
12931298
"uf''",
@@ -1312,7 +1317,7 @@ def test_invalid_string_prefixes(self):
13121317
"Bf''",
13131318
"BF''",]
13141319
double_quote_cases = [case.replace("'", '"') for case in single_quote_cases]
1315-
self.assertAllRaise(SyntaxError, 'invalid syntax',
1320+
self.assertAllRaise(SyntaxError, 'prefixes are incompatible',
13161321
single_quote_cases + double_quote_cases)
13171322

13181323
def test_leading_trailing_spaces(self):
@@ -1342,7 +1347,7 @@ def test_equal_equal(self):
13421347

13431348
self.assertEqual(f'{0==1}', 'False')
13441349

1345-
@unittest.expectedFailure # TODO: RUSTPYTHON
1350+
@unittest.expectedFailure # TODO: RUSTPYTHON
13461351
def test_conversions(self):
13471352
self.assertEqual(f'{3.14:10.10}', ' 3.14')
13481353
self.assertEqual(f'{1.25!s:10.10}', '1.25 ')
@@ -1367,7 +1372,6 @@ def test_conversions(self):
13671372
self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
13681373
["f'{3!'",
13691374
"f'{3!s'",
1370-
"f'{3!g'",
13711375
])
13721376

13731377
self.assertAllRaise(SyntaxError, 'f-string: missing conversion character',
@@ -1408,14 +1412,13 @@ def test_assignment(self):
14081412
"f'{x}' = x",
14091413
])
14101414

1411-
@unittest.expectedFailure # TODO: RUSTPYTHON
14121415
def test_del(self):
14131416
self.assertAllRaise(SyntaxError, 'invalid syntax',
14141417
["del f''",
14151418
"del '' f''",
14161419
])
14171420

1418-
@unittest.expectedFailure # TODO: RUSTPYTHON
1421+
@unittest.expectedFailure # TODO: RUSTPYTHON
14191422
def test_mismatched_braces(self):
14201423
self.assertAllRaise(SyntaxError, "f-string: single '}' is not allowed",
14211424
["f'{{}'",
@@ -1514,7 +1517,6 @@ def test_str_format_differences(self):
15141517
self.assertEqual('{d[a]}'.format(d=d), 'string')
15151518
self.assertEqual('{d[0]}'.format(d=d), 'integer')
15161519

1517-
@unittest.expectedFailure # TODO: RUSTPYTHON
15181520
def test_errors(self):
15191521
# see issue 26287
15201522
self.assertAllRaise(TypeError, 'unsupported',
@@ -1557,7 +1559,6 @@ def test_backslash_char(self):
15571559
self.assertEqual(eval('f"\\\n"'), '')
15581560
self.assertEqual(eval('f"\\\r"'), '')
15591561

1560-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: '1+2 = # my comment\n 3' != '1+2 = \n 3'
15611562
def test_debug_conversion(self):
15621563
x = 'A string'
15631564
self.assertEqual(f'{x=}', 'x=' + repr(x))
@@ -1705,7 +1706,7 @@ def test_walrus(self):
17051706
self.assertEqual(f'{(x:=10)}', '10')
17061707
self.assertEqual(x, 10)
17071708

1708-
@unittest.expectedFailure # TODO: RUSTPYTHON
1709+
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "f-string: expecting '=', or '!', or ':', or '}'" does not match "invalid syntax (?, line 1)"
17091710
def test_invalid_syntax_error_message(self):
17101711
with self.assertRaisesRegex(SyntaxError,
17111712
"f-string: expecting '=', or '!', or ':', or '}'"):
@@ -1731,7 +1732,7 @@ def test_with_an_underscore_and_a_comma_in_format_specifier(self):
17311732
with self.assertRaisesRegex(ValueError, error_msg):
17321733
f'{1:_,}'
17331734

1734-
@unittest.expectedFailure # TODO: RUSTPYTHON
1735+
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "f-string: expecting a valid expression after '{'" does not match "invalid syntax (?, line 1)"
17351736
def test_syntax_error_for_starred_expressions(self):
17361737
with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"):
17371738
compile("f'{*a}'", "?", "exec")
@@ -1740,7 +1741,7 @@ def test_syntax_error_for_starred_expressions(self):
17401741
"f-string: expecting a valid expression after '{'"):
17411742
compile("f'{**a}'", "?", "exec")
17421743

1743-
@unittest.expectedFailure # TODO: RUSTPYTHON
1744+
@unittest.expectedFailure # TODO: RUSTPYTHON; -
17441745
def test_not_closing_quotes(self):
17451746
self.assertAllRaise(SyntaxError, "unterminated f-string literal", ['f"', "f'"])
17461747
self.assertAllRaise(SyntaxError, "unterminated triple-quoted f-string literal",
@@ -1760,7 +1761,7 @@ def test_not_closing_quotes(self):
17601761
except SyntaxError as e:
17611762
self.assertEqual(e.text, 'z = f"""')
17621763
self.assertEqual(e.lineno, 3)
1763-
@unittest.expectedFailure # TODO: RUSTPYTHON
1764+
@unittest.expectedFailure # TODO: RUSTPYTHON
17641765
def test_syntax_error_after_debug(self):
17651766
self.assertAllRaise(SyntaxError, "f-string: expecting a valid expression after '{'",
17661767
[
@@ -1788,7 +1789,6 @@ def test_debug_in_file(self):
17881789
self.assertEqual(stdout.decode('utf-8').strip().replace('\r\n', '\n').replace('\r', '\n'),
17891790
"3\n=3")
17901791

1791-
@unittest.expectedFailure # TODO: RUSTPYTHON
17921792
def test_syntax_warning_infinite_recursion_in_file(self):
17931793
with temp_cwd():
17941794
script = 'script.py'
@@ -1878,6 +1878,13 @@ def __format__(self, format):
18781878
# Test multiple format specs in same raw f-string
18791879
self.assertEqual(rf"{UnchangedFormat():\xFF} {UnchangedFormat():\n}", '\\xFF \\n')
18801880

1881+
def test_gh139516(self):
1882+
with temp_cwd():
1883+
script = 'script.py'
1884+
with open(script, 'wb') as f:
1885+
f.write('''def f(a): pass\nf"{f(a=lambda: 'à'\n)}"'''.encode())
1886+
assert_python_ok(script)
1887+
18811888

18821889
if __name__ == '__main__':
18831890
unittest.main()

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::*;

0 commit comments

Comments
 (0)