Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions Grammar/python.gram
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,8 @@ invalid_expression:
_PyPegen_check_legacy_stmt(p, a) ? NULL : p->tokens[p->mark-1]->level == 0 ? NULL :
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "invalid syntax. Perhaps you forgot a comma?") }
| a=disjunction 'if' b=disjunction !('else'|':') { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "expected 'else' after 'if' expression") }
| a='lambda' [lambda_params] b=':' &(FSTRING_MIDDLE | fstring_replacement_field) {
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "f-string: lambda expressions are not allowed without parentheses") }

invalid_named_expression(memo):
| a=expression ':=' expression {
Expand Down Expand Up @@ -1358,14 +1360,23 @@ invalid_kvpair:
invalid_starred_expression:
| a='*' expression '=' b=expression { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "cannot assign to iterable argument unpacking") }
invalid_replacement_field:
| '{' a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: expression required before '='") }
| '{' a=':' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: expression required before ':'") }
| '{' a='!' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: expression required before '!'") }
| '{' a='}' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: empty expression not allowed") }
| '{' (yield_expr | star_expressions) "="? invalid_conversion_character
# We explicitly require either a conversion character or a format spec (or both) in order for this to not get too general
| '{' (yield_expr | star_expressions) "="? "!" NAME [':' fstring_format_spec*] !'}' { RAISE_SYNTAX_ERROR("f-string: expecting '}'") }
| '{' (yield_expr | star_expressions) "="? ':' fstring_format_spec* !'}' { RAISE_SYNTAX_ERROR("f-string: expecting '}'") }
| '{' a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '='") }
| '{' a='!' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '!'") }
| '{' a=':' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before ':'") }
| '{' a='}' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '}'") }
| '{' !(yield_expr | star_expressions) { RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting a valid expression after '{'")}
| '{' (yield_expr | star_expressions) !('=' | '!' | ':' | '}') {
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '=', or '!', or ':', or '}'") }
| '{' (yield_expr | star_expressions) '=' !('!' | ':' | '}') {
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '!', or ':', or '}'") }
| '{' (yield_expr | star_expressions) '='? invalid_conversion_character
| '{' (yield_expr | star_expressions) '='? ['!' NAME] !(':' | '}') {
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting ':' or '}'") }
| '{' (yield_expr | star_expressions) '='? ['!' NAME] ':' fstring_format_spec* !'}' {
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '}', or format specs") }
| '{' (yield_expr | star_expressions) '='? ['!' NAME] !'}' {
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '}'") }

invalid_conversion_character:
| a="!" &(':'|'}') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: missed conversion character") }
| a="!" !NAME { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: invalid conversion character") }
| '!' &(':' | '}') { RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: missing conversion character") }
| '!' !NAME { RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: invalid conversion character") }
2 changes: 1 addition & 1 deletion Lib/test/test_cmd_line_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ def test_syntaxerror_multi_line_fstring(self):
[
b' foo = f"""{}',
b' ^',
b'SyntaxError: f-string: empty expression not allowed',
b'SyntaxError: f-string: valid expression required before \'}\'',
],
)

Expand Down
73 changes: 45 additions & 28 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,15 +693,13 @@ def test_format_specifier_expressions(self):
self.assertEqual(f'{10:#{3 != {4:5} and width}x}', ' 0xa')
self.assertEqual(f'result: {value:{width:{0}}.{precision:1}}', 'result: 12.35')

self.assertAllRaise(SyntaxError,
"""f-string: invalid conversion character 'r{"': """
"""expected 's', 'r', or 'a'""",
self.assertAllRaise(SyntaxError, "f-string: expecting ':' or '}'",
["""f'{"s"!r{":10"}}'""",

# This looks like a nested format spec.
])

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

def test_missing_expression(self):
self.assertAllRaise(SyntaxError, 'f-string: empty expression not allowed',
self.assertAllRaise(SyntaxError,
"f-string: valid expression required before '}'",
["f'{}'",
"f'{ }'"
"f' {} '",
Expand All @@ -736,8 +735,8 @@ def test_missing_expression(self):
"f'''{\t\f\r\n}'''",
])

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

self.assertAllRaise(SyntaxError, "f-string: expression required before ':'",
self.assertAllRaise(SyntaxError,
"f-string: valid expression required before ':'",
["f'{:}'",
"f'{ :!}'",
"f'{:2}'",
"f'''{\t\f\r\n:a}'''",
"f'{:'",
])

self.assertAllRaise(SyntaxError, "f-string: expression required before '='",
self.assertAllRaise(SyntaxError,
"f-string: valid expression required before '='",
["f'{=}'",
"f'{ =}'",
"f'{ =:}'",
Expand All @@ -784,21 +785,18 @@ def test_missing_expression(self):
def test_parens_in_expressions(self):
self.assertEqual(f'{3,}', '(3,)')

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

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

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

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

self.assertAllRaise(SyntaxError, 'empty expression not allowed',
self.assertAllRaise(SyntaxError,
"f-string: valid expression required before '}'",
["f'{\n}'",
])

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

# lambda doesn't work without parens, because the colon
# makes the parser think it's a format_spec
self.assertAllRaise(SyntaxError, 'invalid syntax',
# makes the parser think it's a format_spec
# emit warning if we can match a format_spec
self.assertAllRaise(SyntaxError,
"f-string: lambda expressions are not allowed "
"without parentheses",
["f'{lambda x:x}'",
"f'{lambda :x}'",
"f'{lambda *arg, :x}'",
"f'{1, lambda:x}'",
])

# but don't emit the paren warning in general cases
self.assertAllRaise(SyntaxError,
"f-string: expecting a valid expression after '{'",
["f'{lambda x:}'",
"f'{lambda :}'",
"f'{+ lambda:None}'",
])

def test_valid_prefixes(self):
Expand Down Expand Up @@ -1185,7 +1198,7 @@ def test_conversions(self):
"f'{3!g'",
])

self.assertAllRaise(SyntaxError, 'f-string: missed conversion character',
self.assertAllRaise(SyntaxError, 'f-string: missing conversion character',
["f'{3!}'",
"f'{3!:'",
"f'{3!:}'",
Expand Down Expand Up @@ -1244,8 +1257,7 @@ def test_mismatched_braces(self):
])

self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
["f'{3:{{>10}'",
"f'{3'",
["f'{3'",
"f'{3!'",
"f'{3:'",
"f'{3!s'",
Expand All @@ -1261,6 +1273,11 @@ def test_mismatched_braces(self):
"f'{i='", # See gh-93418.
])

self.assertAllRaise(SyntaxError,
"f-string: expecting a valid expression after '{'",
["f'{3:{{>10}'",
])

# But these are just normal strings.
self.assertEqual(f'{"{"}', '{')
self.assertEqual(f'{"}"}', '}')
Expand Down Expand Up @@ -1481,7 +1498,8 @@ def test_walrus(self):
self.assertEqual(x, 10)

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

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

def test_syntax_error_for_starred_expressions(self):
error_msg = re.escape("can't use starred expression here")
with self.assertRaisesRegex(SyntaxError, error_msg):
with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"):
compile("f'{*a}'", "?", "exec")

error_msg = re.escape("invalid syntax")
with self.assertRaisesRegex(SyntaxError, error_msg):
with self.assertRaisesRegex(SyntaxError,
"f-string: expecting a valid expression after '{'"):
Comment thread
sunmy2019 marked this conversation as resolved.
compile("f'{**a}'", "?", "exec")

if __name__ == '__main__':
Expand Down
Loading