Skip to content

Commit 1ef61cf

Browse files
pablogsallysnikolaouisidenticalmgmacias95sunmy2019
authored
gh-102856: Initial implementation of PEP 701 (#102855)
Co-authored-by: Lysandros Nikolaou <[email protected]> Co-authored-by: Batuhan Taskaya <[email protected]> Co-authored-by: Marta Gómez Macías <[email protected]> Co-authored-by: sunmy2019 <[email protected]>
1 parent a6b07b5 commit 1ef61cf

27 files changed

+6425
-4139
lines changed

Doc/library/token-list.inc

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Grammar/Tokens

+4
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,17 @@ ATEQUAL '@='
5353
RARROW '->'
5454
ELLIPSIS '...'
5555
COLONEQUAL ':='
56+
EXCLAMATION '!'
5657

5758
OP
5859
AWAIT
5960
ASYNC
6061
TYPE_IGNORE
6162
TYPE_COMMENT
6263
SOFT_KEYWORD
64+
FSTRING_START
65+
FSTRING_MIDDLE
66+
FSTRING_END
6367
ERRORTOKEN
6468

6569
# These aren't used by the C tokenizer but are needed for tokenize.py

Grammar/python.gram

+48-6
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ yield_stmt[stmt_ty]: y=yield_expr { _PyAST_Expr(y, EXTRA) }
194194

195195
assert_stmt[stmt_ty]: 'assert' a=expression b=[',' z=expression { z }] { _PyAST_Assert(a, b, EXTRA) }
196196

197-
import_stmt[stmt_ty]:
197+
import_stmt[stmt_ty]:
198198
| invalid_import
199199
| import_name
200200
| import_from
@@ -415,8 +415,8 @@ try_stmt[stmt_ty]:
415415
| invalid_try_stmt
416416
| 'try' &&':' b=block f=finally_block { _PyAST_Try(b, NULL, NULL, f, EXTRA) }
417417
| 'try' &&':' b=block ex[asdl_excepthandler_seq*]=except_block+ el=[else_block] f=[finally_block] { _PyAST_Try(b, ex, el, f, EXTRA) }
418-
| 'try' &&':' b=block ex[asdl_excepthandler_seq*]=except_star_block+ el=[else_block] f=[finally_block] {
419-
CHECK_VERSION(stmt_ty, 11, "Exception groups are",
418+
| 'try' &&':' b=block ex[asdl_excepthandler_seq*]=except_star_block+ el=[else_block] f=[finally_block] {
419+
CHECK_VERSION(stmt_ty, 11, "Exception groups are",
420420
_PyAST_TryStar(b, ex, el, f, EXTRA)) }
421421

422422

@@ -807,7 +807,7 @@ atom[expr_ty]:
807807
| 'True' { _PyAST_Constant(Py_True, NULL, EXTRA) }
808808
| 'False' { _PyAST_Constant(Py_False, NULL, EXTRA) }
809809
| 'None' { _PyAST_Constant(Py_None, NULL, EXTRA) }
810-
| &STRING strings
810+
| &(STRING|FSTRING_START) strings
811811
| NUMBER
812812
| &'(' (tuple | group | genexp)
813813
| &'[' (list | listcomp)
@@ -877,7 +877,26 @@ lambda_param[arg_ty]: a=NAME { _PyAST_arg(a->v.Name.id, NULL, NULL, EXTRA) }
877877
# LITERALS
878878
# ========
879879

880-
strings[expr_ty] (memo): a=STRING+ { _PyPegen_concatenate_strings(p, a) }
880+
fstring_middle[expr_ty]:
881+
| fstring_replacement_field
882+
| t=FSTRING_MIDDLE { _PyPegen_constant_from_token(p, t) }
883+
fstring_replacement_field[expr_ty]:
884+
| '{' a=(yield_expr | star_expressions) debug_expr="="? conversion=[fstring_conversion] format=[fstring_full_format_spec] '}' {
885+
_PyPegen_formatted_value(p, a, debug_expr, conversion, format, EXTRA)
886+
}
887+
| invalid_replacement_field
888+
fstring_conversion[expr_ty]:
889+
| conv_token="!" conv=NAME { _PyPegen_check_fstring_conversion(p, conv_token, conv) }
890+
fstring_full_format_spec[expr_ty]:
891+
| ':' spec=fstring_format_spec* { spec ? _PyAST_JoinedStr((asdl_expr_seq*)spec, EXTRA) : NULL }
892+
fstring_format_spec[expr_ty]:
893+
| t=FSTRING_MIDDLE { _PyPegen_constant_from_token(p, t) }
894+
| fstring_replacement_field
895+
fstring[expr_ty]:
896+
| a=FSTRING_START b=fstring_middle* c=FSTRING_END { _PyPegen_joined_str(p, a, (asdl_expr_seq*)b, c) }
897+
898+
string[expr_ty]: s[Token*]=STRING { _PyPegen_constant_from_string(p, s) }
899+
strings[expr_ty] (memo): a[asdl_expr_seq*]=(fstring|string)+ { _PyPegen_concatenate_strings(p, a, EXTRA) }
881900

882901
list[expr_ty]:
883902
| '[' a=[star_named_expressions] ']' { _PyAST_List(a, Load, EXTRA) }
@@ -1118,6 +1137,8 @@ invalid_expression:
11181137
_PyPegen_check_legacy_stmt(p, a) ? NULL : p->tokens[p->mark-1]->level == 0 ? NULL :
11191138
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "invalid syntax. Perhaps you forgot a comma?") }
11201139
| a=disjunction 'if' b=disjunction !('else'|':') { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "expected 'else' after 'if' expression") }
1140+
| a='lambda' [lambda_params] b=':' &(FSTRING_MIDDLE | fstring_replacement_field) {
1141+
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "f-string: lambda expressions are not allowed without parentheses") }
11211142

11221143
invalid_named_expression(memo):
11231144
| a=expression ':=' expression {
@@ -1241,7 +1262,7 @@ invalid_group:
12411262
invalid_import:
12421263
| a='import' dotted_name 'from' dotted_name {
12431264
RAISE_SYNTAX_ERROR_STARTING_FROM(a, "Did you mean to use 'from ... import ...' instead?") }
1244-
1265+
12451266
invalid_import_from_targets:
12461267
| import_from_as_names ',' NEWLINE {
12471268
RAISE_SYNTAX_ERROR("trailing comma not allowed without surrounding parentheses") }
@@ -1335,3 +1356,24 @@ invalid_kvpair:
13351356
| expression a=':' &('}'|',') {RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "expression expected after dictionary key and ':'") }
13361357
invalid_starred_expression:
13371358
| a='*' expression '=' b=expression { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "cannot assign to iterable argument unpacking") }
1359+
invalid_replacement_field:
1360+
| '{' a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '='") }
1361+
| '{' a='!' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '!'") }
1362+
| '{' a=':' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before ':'") }
1363+
| '{' a='}' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '}'") }
1364+
| '{' !(yield_expr | star_expressions) { RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting a valid expression after '{'")}
1365+
| '{' (yield_expr | star_expressions) !('=' | '!' | ':' | '}') {
1366+
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '=', or '!', or ':', or '}'") }
1367+
| '{' (yield_expr | star_expressions) '=' !('!' | ':' | '}') {
1368+
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '!', or ':', or '}'") }
1369+
| '{' (yield_expr | star_expressions) '='? invalid_conversion_character
1370+
| '{' (yield_expr | star_expressions) '='? ['!' NAME] !(':' | '}') {
1371+
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting ':' or '}'") }
1372+
| '{' (yield_expr | star_expressions) '='? ['!' NAME] ':' fstring_format_spec* !'}' {
1373+
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '}', or format specs") }
1374+
| '{' (yield_expr | star_expressions) '='? ['!' NAME] !'}' {
1375+
PyErr_Occurred() ? NULL : RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: expecting '}'") }
1376+
1377+
invalid_conversion_character:
1378+
| '!' &(':' | '}') { RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: missing conversion character") }
1379+
| '!' !NAME { RAISE_SYNTAX_ERROR_ON_NEXT_TOKEN("f-string: invalid conversion character") }

Include/internal/pycore_token.h

+14-8
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,18 @@ extern "C" {
6767
#define RARROW 51
6868
#define ELLIPSIS 52
6969
#define COLONEQUAL 53
70-
#define OP 54
71-
#define AWAIT 55
72-
#define ASYNC 56
73-
#define TYPE_IGNORE 57
74-
#define TYPE_COMMENT 58
75-
#define SOFT_KEYWORD 59
76-
#define ERRORTOKEN 60
77-
#define N_TOKENS 64
70+
#define EXCLAMATION 54
71+
#define OP 55
72+
#define AWAIT 56
73+
#define ASYNC 57
74+
#define TYPE_IGNORE 58
75+
#define TYPE_COMMENT 59
76+
#define SOFT_KEYWORD 60
77+
#define FSTRING_START 61
78+
#define FSTRING_MIDDLE 62
79+
#define FSTRING_END 63
80+
#define ERRORTOKEN 64
81+
#define N_TOKENS 68
7882
#define NT_OFFSET 256
7983

8084
/* Special definitions for cooperation with parser */
@@ -86,6 +90,8 @@ extern "C" {
8690
(x) == NEWLINE || \
8791
(x) == INDENT || \
8892
(x) == DEDENT)
93+
#define ISSTRINGLIT(x) ((x) == STRING || \
94+
(x) == FSTRING_MIDDLE)
8995

9096

9197
// Symbols exported for test_peg_generator

Lib/test/test_ast.py

-5
Original file line numberDiff line numberDiff line change
@@ -774,11 +774,6 @@ def test_parenthesized_with_feature_version(self):
774774
ast.parse('with (CtxManager() as example): ...', feature_version=(3, 8))
775775
ast.parse('with CtxManager() as example: ...', feature_version=(3, 8))
776776

777-
def test_debug_f_string_feature_version(self):
778-
ast.parse('f"{x=}"', feature_version=(3, 8))
779-
with self.assertRaises(SyntaxError):
780-
ast.parse('f"{x=}"', feature_version=(3, 7))
781-
782777
def test_assignment_expression_feature_version(self):
783778
ast.parse('(x := 0)', feature_version=(3, 8))
784779
with self.assertRaises(SyntaxError):

Lib/test/test_cmd_line_script.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -636,9 +636,9 @@ def test_syntaxerror_multi_line_fstring(self):
636636
self.assertEqual(
637637
stderr.splitlines()[-3:],
638638
[
639-
b' foo"""',
640-
b' ^',
641-
b'SyntaxError: f-string: empty expression not allowed',
639+
b' foo = f"""{}',
640+
b' ^',
641+
b'SyntaxError: f-string: valid expression required before \'}\'',
642642
],
643643
)
644644

Lib/test/test_eof.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from test import support
55
from test.support import os_helper
66
from test.support import script_helper
7+
from test.support import warnings_helper
78
import unittest
89

910
class EOFTestCase(unittest.TestCase):
@@ -36,10 +37,11 @@ def test_EOFS_with_file(self):
3637
rc, out, err = script_helper.assert_python_failure(file_name)
3738
self.assertIn(b'unterminated triple-quoted string literal (detected at line 3)', err)
3839

40+
@warnings_helper.ignore_warnings(category=SyntaxWarning)
3941
def test_eof_with_line_continuation(self):
4042
expect = "unexpected EOF while parsing (<string>, line 1)"
4143
try:
42-
compile('"\\xhh" \\', '<string>', 'exec', dont_inherit=True)
44+
compile('"\\Xhh" \\', '<string>', 'exec')
4345
except SyntaxError as msg:
4446
self.assertEqual(str(msg), expect)
4547
else:

Lib/test/test_exceptions.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def ckmsg(src, msg):
155155

156156
ckmsg(s, "'continue' not properly in loop")
157157
ckmsg("continue\n", "'continue' not properly in loop")
158+
ckmsg("f'{6 0}'", "invalid syntax. Perhaps you forgot a comma?")
158159

159160
def testSyntaxErrorMissingParens(self):
160161
def ckmsg(src, msg, exception=SyntaxError):
@@ -227,7 +228,7 @@ def testSyntaxErrorOffset(self):
227228
check('Python = "\u1e54\xfd\u0163\u0125\xf2\xf1" +', 1, 20)
228229
check(b'# -*- coding: cp1251 -*-\nPython = "\xcf\xb3\xf2\xee\xed" +',
229230
2, 19, encoding='cp1251')
230-
check(b'Python = "\xcf\xb3\xf2\xee\xed" +', 1, 18)
231+
check(b'Python = "\xcf\xb3\xf2\xee\xed" +', 1, 10)
231232
check('x = "a', 1, 5)
232233
check('lambda x: x = 2', 1, 1)
233234
check('f{a + b + c}', 1, 2)

0 commit comments

Comments
 (0)