Skip to content

Commit ef9c43a

Browse files
authored
Merge pull request #67 from sunmy2019/capture-more-errors-in-fstring-replacement-field
capture more errors in fstring replacement field
2 parents 7fe556e + 91faa31 commit ef9c43a

File tree

6 files changed

+2078
-1329
lines changed

6 files changed

+2078
-1329
lines changed

Grammar/python.gram

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,8 @@ invalid_expression:
11401140
_PyPegen_check_legacy_stmt(p, a) ? NULL : p->tokens[p->mark-1]->level == 0 ? NULL :
11411141
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "invalid syntax. Perhaps you forgot a comma?") }
11421142
| a=disjunction 'if' b=disjunction !('else'|':') { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "expected 'else' after 'if' expression") }
1143+
| a='lambda' [lambda_params] b=':' &(FSTRING_MIDDLE | fstring_replacement_field) {
1144+
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "f-string: lambda expressions are not allowed without parentheses") }
11431145

11441146
invalid_named_expression(memo):
11451147
| a=expression ':=' expression {
@@ -1358,14 +1360,23 @@ invalid_kvpair:
13581360
invalid_starred_expression:
13591361
| a='*' expression '=' b=expression { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "cannot assign to iterable argument unpacking") }
13601362
invalid_replacement_field:
1361-
| '{' a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: expression required before '='") }
1362-
| '{' a=':' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: expression required before ':'") }
1363-
| '{' a='!' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: expression required before '!'") }
1364-
| '{' a='}' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: empty expression not allowed") }
1365-
| '{' (yield_expr | star_expressions) "="? invalid_conversion_character
1366-
# We explicitly require either a conversion character or a format spec (or both) in order for this to not get too general
1367-
| '{' (yield_expr | star_expressions) "="? "!" NAME [':' fstring_format_spec*] !'}' { RAISE_SYNTAX_ERROR("f-string: expecting '}'") }
1368-
| '{' (yield_expr | star_expressions) "="? ':' fstring_format_spec* !'}' { RAISE_SYNTAX_ERROR("f-string: expecting '}'") }
1363+
| '{' a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '='") }
1364+
| '{' a='!' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '!'") }
1365+
| '{' a=':' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before ':'") }
1366+
| '{' a='}' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '}'") }
1367+
| '{' !(yield_expr | star_expressions) { RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting a valid expression after '{'")}
1368+
| '{' (yield_expr | star_expressions) !('=' | '!' | ':' | '}') {
1369+
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '=', or '!', or ':', or '}'") }
1370+
| '{' (yield_expr | star_expressions) '=' !('!' | ':' | '}') {
1371+
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '!', or ':', or '}'") }
1372+
| '{' (yield_expr | star_expressions) '='? invalid_conversion_character
1373+
| '{' (yield_expr | star_expressions) '='? ['!' NAME] !(':' | '}') {
1374+
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting ':' or '}'") }
1375+
| '{' (yield_expr | star_expressions) '='? ['!' NAME] ':' fstring_format_spec* !'}' {
1376+
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '}', or format specs") }
1377+
| '{' (yield_expr | star_expressions) '='? ['!' NAME] !'}' {
1378+
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '}'") }
1379+
13691380
invalid_conversion_character:
1370-
| a="!" &(':'|'}') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: missed conversion character") }
1371-
| a="!" !NAME { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: invalid conversion character") }
1381+
| '!' &(':' | '}') { RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: missing conversion character") }
1382+
| '!' !NAME { RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: invalid conversion character") }

Lib/test/test_cmd_line_script.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ def test_syntaxerror_multi_line_fstring(self):
638638
[
639639
b' foo = f"""{}',
640640
b' ^',
641-
b'SyntaxError: f-string: empty expression not allowed',
641+
b'SyntaxError: f-string: valid expression required before \'}\'',
642642
],
643643
)
644644

Lib/test/test_fstring.py

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -693,15 +693,13 @@ def test_format_specifier_expressions(self):
693693
self.assertEqual(f'{10:#{3 != {4:5} and width}x}', ' 0xa')
694694
self.assertEqual(f'result: {value:{width:{0}}.{precision:1}}', 'result: 12.35')
695695

696-
self.assertAllRaise(SyntaxError,
697-
"""f-string: invalid conversion character 'r{"': """
698-
"""expected 's', 'r', or 'a'""",
696+
self.assertAllRaise(SyntaxError, "f-string: expecting ':' or '}'",
699697
["""f'{"s"!r{":10"}}'""",
700-
701698
# This looks like a nested format spec.
702699
])
703700

704-
self.assertAllRaise(SyntaxError, "f-string: invalid syntax",
701+
self.assertAllRaise(SyntaxError,
702+
"f-string: expecting a valid expression after '{'",
705703
[# Invalid syntax inside a nested spec.
706704
"f'{4:{/5}}'",
707705
])
@@ -724,7 +722,8 @@ def __format__(self, spec):
724722
self.assertEqual(f'{x} {x}', '1 2')
725723

726724
def test_missing_expression(self):
727-
self.assertAllRaise(SyntaxError, 'f-string: empty expression not allowed',
725+
self.assertAllRaise(SyntaxError,
726+
"f-string: valid expression required before '}'",
728727
["f'{}'",
729728
"f'{ }'"
730729
"f' {} '",
@@ -736,8 +735,8 @@ def test_missing_expression(self):
736735
"f'''{\t\f\r\n}'''",
737736
])
738737

739-
# Different error messages are raised when a specifier ('!', ':' or '=') is used after an empty expression
740-
self.assertAllRaise(SyntaxError, "f-string: expression required before '!'",
738+
self.assertAllRaise(SyntaxError,
739+
"f-string: valid expression required before '!'",
741740
["f'{!r}'",
742741
"f'{ !r}'",
743742
"f'{!}'",
@@ -758,15 +757,17 @@ def test_missing_expression(self):
758757
"f'{ !xr:a}'",
759758
])
760759

761-
self.assertAllRaise(SyntaxError, "f-string: expression required before ':'",
760+
self.assertAllRaise(SyntaxError,
761+
"f-string: valid expression required before ':'",
762762
["f'{:}'",
763763
"f'{ :!}'",
764764
"f'{:2}'",
765765
"f'''{\t\f\r\n:a}'''",
766766
"f'{:'",
767767
])
768768

769-
self.assertAllRaise(SyntaxError, "f-string: expression required before '='",
769+
self.assertAllRaise(SyntaxError,
770+
"f-string: valid expression required before '='",
770771
["f'{=}'",
771772
"f'{ =}'",
772773
"f'{ =:}'",
@@ -784,21 +785,18 @@ def test_missing_expression(self):
784785
def test_parens_in_expressions(self):
785786
self.assertEqual(f'{3,}', '(3,)')
786787

787-
# Add these because when an expression is evaluated, parens
788-
# are added around it. But we shouldn't go from an invalid
789-
# expression to a valid one. The added parens are just
790-
# supposed to allow whitespace (including newlines).
791-
self.assertAllRaise(SyntaxError, 'invalid syntax',
788+
self.assertAllRaise(SyntaxError,
789+
"f-string: expecting a valid expression after '{'",
792790
["f'{,}'",
793-
"f'{,}'", # this is (,), which is an error
794791
])
795792

796793
self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'",
797794
["f'{3)+(4}'",
798795
])
799796

800797
def test_newlines_before_syntax_error(self):
801-
self.assertAllRaise(SyntaxError, "invalid syntax",
798+
self.assertAllRaise(SyntaxError,
799+
"f-string: expecting a valid expression after '{'",
802800
["f'{.}'", "\nf'{.}'", "\n\nf'{.}'"])
803801

804802
def test_backslashes_in_string_part(self):
@@ -885,7 +883,8 @@ def test_backslashes_in_expression_part(self):
885883
self.assertEqual(f'{"\N{LEFT CURLY BRACKET}"}', '{')
886884
self.assertEqual(rf'{"\N{LEFT CURLY BRACKET}"}', '{')
887885

888-
self.assertAllRaise(SyntaxError, 'empty expression not allowed',
886+
self.assertAllRaise(SyntaxError,
887+
"f-string: valid expression required before '}'",
889888
["f'{\n}'",
890889
])
891890

@@ -930,9 +929,23 @@ def test_lambda(self):
930929
self.assertEqual(f'{(lambda y:x*y)("8"):10}', "88888 ")
931930

932931
# lambda doesn't work without parens, because the colon
933-
# makes the parser think it's a format_spec
934-
self.assertAllRaise(SyntaxError, 'invalid syntax',
932+
# makes the parser think it's a format_spec
933+
# emit warning if we can match a format_spec
934+
self.assertAllRaise(SyntaxError,
935+
"f-string: lambda expressions are not allowed "
936+
"without parentheses",
935937
["f'{lambda x:x}'",
938+
"f'{lambda :x}'",
939+
"f'{lambda *arg, :x}'",
940+
"f'{1, lambda:x}'",
941+
])
942+
943+
# but don't emit the paren warning in general cases
944+
self.assertAllRaise(SyntaxError,
945+
"f-string: expecting a valid expression after '{'",
946+
["f'{lambda x:}'",
947+
"f'{lambda :}'",
948+
"f'{+ lambda:None}'",
936949
])
937950

938951
def test_valid_prefixes(self):
@@ -1185,7 +1198,7 @@ def test_conversions(self):
11851198
"f'{3!g'",
11861199
])
11871200

1188-
self.assertAllRaise(SyntaxError, 'f-string: missed conversion character',
1201+
self.assertAllRaise(SyntaxError, 'f-string: missing conversion character',
11891202
["f'{3!}'",
11901203
"f'{3!:'",
11911204
"f'{3!:}'",
@@ -1244,8 +1257,7 @@ def test_mismatched_braces(self):
12441257
])
12451258

12461259
self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
1247-
["f'{3:{{>10}'",
1248-
"f'{3'",
1260+
["f'{3'",
12491261
"f'{3!'",
12501262
"f'{3:'",
12511263
"f'{3!s'",
@@ -1261,6 +1273,11 @@ def test_mismatched_braces(self):
12611273
"f'{i='", # See gh-93418.
12621274
])
12631275

1276+
self.assertAllRaise(SyntaxError,
1277+
"f-string: expecting a valid expression after '{'",
1278+
["f'{3:{{>10}'",
1279+
])
1280+
12641281
# But these are just normal strings.
12651282
self.assertEqual(f'{"{"}', '{')
12661283
self.assertEqual(f'{"}"}', '}')
@@ -1481,7 +1498,8 @@ def test_walrus(self):
14811498
self.assertEqual(x, 10)
14821499

14831500
def test_invalid_syntax_error_message(self):
1484-
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
1501+
with self.assertRaisesRegex(SyntaxError,
1502+
"f-string: expecting '=', or '!', or ':', or '}'"):
14851503
compile("f'{a $ b}'", "?", "exec")
14861504

14871505
def test_with_two_commas_in_format_specifier(self):
@@ -1505,12 +1523,11 @@ def test_with_an_underscore_and_a_comma_in_format_specifier(self):
15051523
f'{1:_,}'
15061524

15071525
def test_syntax_error_for_starred_expressions(self):
1508-
error_msg = re.escape("can't use starred expression here")
1509-
with self.assertRaisesRegex(SyntaxError, error_msg):
1526+
with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"):
15101527
compile("f'{*a}'", "?", "exec")
15111528

1512-
error_msg = re.escape("invalid syntax")
1513-
with self.assertRaisesRegex(SyntaxError, error_msg):
1529+
with self.assertRaisesRegex(SyntaxError,
1530+
"f-string: expecting a valid expression after '{'"):
15141531
compile("f'{**a}'", "?", "exec")
15151532

15161533
if __name__ == '__main__':

0 commit comments

Comments
 (0)