https://github.com/python/cpython/commit/7b12a415c85d88eeb22a806b2bc9472d471b8e60
commit: 7b12a415c85d88eeb22a806b2bc9472d471b8e60
branch: main
author: sobolevn <[email protected]>
committer: sobolevn <[email protected]>
date: 2026-10-25T20:35:59Z
summary:
gh-152631: Fix `ast.parse` on lazy imports with older `feature_version`
(#152632)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-01-07.gh-issue-152631.LKgCM_.rst
M Grammar/python.gram
M Lib/test/test_ast/test_ast.py
M Parser/parser.c
diff --git a/Grammar/python.gram b/Grammar/python.gram
index f1800ca0597e77..a3312753f5bde8 100644
--- a/Grammar/python.gram
+++ b/Grammar/python.gram
@@ -218,15 +218,25 @@ import_stmt[stmt_ty](memo):
# -----------------
import_name[stmt_ty]:
- | lazy="lazy"? 'import' a=dotted_as_names { _PyAST_Import(a, lazy ? 1 : 0,
EXTRA) }
+ | "lazy" 'import' a=dotted_as_names {
+ CHECK_VERSION(stmt_ty, 15, "Lazy imports are",
+ _PyAST_Import(a, 1, EXTRA)) }
+ | 'import' a=dotted_as_names { _PyAST_Import(a, 0, EXTRA) }
# note below: the ('.' | '...') is necessary because '...' is tokenized as
ELLIPSIS
import_from[stmt_ty]:
| invalid_import_from
- | lazy="lazy"? 'from' a=('.' | '...')* b=dotted_name 'import'
c=import_from_targets {
- _PyPegen_checked_from_import(p, a, b, c, lazy, EXTRA) }
- | lazy="lazy"? 'from' a=('.' | '...')+ 'import' b=import_from_targets {
- _PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), lazy ? 1 : 0,
EXTRA) }
+ | lazy="lazy" 'from' a=('.' | '...')* b=dotted_name 'import'
c=import_from_targets {
+ CHECK_VERSION(stmt_ty, 15, "Lazy imports are",
+ _PyPegen_checked_from_import(p, a, b, c, lazy, EXTRA)) }
+ | "lazy" 'from' a=('.' | '...')+ 'import' b=import_from_targets {
+ CHECK_VERSION(stmt_ty, 15, "Lazy imports are",
+ _PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), 1, EXTRA)) }
+ | 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
+ _PyPegen_checked_from_import(p, a, b, c, NULL, EXTRA) }
+ | 'from' a=('.' | '...')+ 'import' b=import_from_targets {
+ _PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), 0, EXTRA) }
+
import_from_targets[asdl_alias_seq*]:
| '(' a=import_from_as_names [','] ')' { a }
| import_from_as_names !','
diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py
index 08d4cfb9ae6c8a..095bc5832f6aca 100644
--- a/Lib/test/test_ast/test_ast.py
+++ b/Lib/test/test_ast/test_ast.py
@@ -1031,6 +1031,19 @@ def test_type_params_default_feature_version(self):
with self.assertRaises(SyntaxError):
ast.parse(sample, feature_version=(3, 12))
+ def test_lazy_imports_feature_version(self):
+ samples = [
+ "lazy import os",
+ "lazy from .. import name",
+ "lazy from mod import some",
+ ]
+ for sample in samples:
+ with self.subTest(sample):
+ ast.parse(sample)
+ ast.parse(sample, feature_version=(3, 15))
+ with self.assertRaises(SyntaxError):
+ ast.parse(sample, feature_version=(3, 14))
+
def test_invalid_major_feature_version(self):
with self.assertRaises(ValueError):
ast.parse('pass', feature_version=(2, 7))
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-01-07.gh-issue-152631.LKgCM_.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-01-07.gh-issue-152631.LKgCM_.rst
new file mode 100644
index 00000000000000..51208108f10647
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-29-23-01-07.gh-issue-152631.LKgCM_.rst
@@ -0,0 +1,2 @@
+Fix :func:`ast.parse` ability to parse lazy imports with *feature_version*
+older than 3.15, now it properly raises :exc:`SyntaxError`.
diff --git a/Parser/parser.c b/Parser/parser.c
index fd94f979d0e868..2300314a77888f 100644
--- a/Parser/parser.c
+++ b/Parser/parser.c
@@ -3485,7 +3485,7 @@ import_stmt_rule(Parser *p)
return _res;
}
-// import_name: "lazy"? 'import' dotted_as_names
+// import_name: "lazy" 'import' dotted_as_names | 'import' dotted_as_names
static stmt_ty
import_name_rule(Parser *p)
{
@@ -3507,24 +3507,60 @@ import_name_rule(Parser *p)
UNUSED(_start_lineno); // Only used by EXTRA macro
int _start_col_offset = p->tokens[_mark]->col_offset;
UNUSED(_start_col_offset); // Only used by EXTRA macro
- { // "lazy"? 'import' dotted_as_names
+ { // "lazy" 'import' dotted_as_names
if (p->error_indicator) {
p->level--;
return NULL;
}
- D(fprintf(stderr, "%*c> import_name[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "\"lazy\"? 'import' dotted_as_names"));
- Token * _keyword;
+ D(fprintf(stderr, "%*c> import_name[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "\"lazy\" 'import' dotted_as_names"));
+ expr_ty _keyword;
+ Token * _keyword_1;
asdl_alias_seq* a;
- void *lazy;
if (
- (lazy = _PyPegen_expect_soft_keyword(p, "lazy"),
!p->error_indicator) // "lazy"?
+ (_keyword = _PyPegen_expect_soft_keyword(p, "lazy")) //
soft_keyword='"lazy"'
&&
+ (_keyword_1 = _PyPegen_expect_token(p, 500)) // token='import'
+ &&
+ (a = dotted_as_names_rule(p)) // dotted_as_names
+ )
+ {
+ D(fprintf(stderr, "%*c+ import_name[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "\"lazy\" 'import' dotted_as_names"));
+ Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
+ if (_token == NULL) {
+ p->level--;
+ return NULL;
+ }
+ int _end_lineno = _token->end_lineno;
+ UNUSED(_end_lineno); // Only used by EXTRA macro
+ int _end_col_offset = _token->end_col_offset;
+ UNUSED(_end_col_offset); // Only used by EXTRA macro
+ _res = CHECK_VERSION ( stmt_ty , 15 , "Lazy imports are" ,
_PyAST_Import ( a , 1 , EXTRA ) );
+ if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
+ p->error_indicator = 1;
+ p->level--;
+ return NULL;
+ }
+ goto done;
+ }
+ p->mark = _mark;
+ D(fprintf(stderr, "%*c%s import_name[%d-%d]: %s failed!\n", p->level,
' ',
+ p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"\"lazy\" 'import' dotted_as_names"));
+ }
+ { // 'import' dotted_as_names
+ if (p->error_indicator) {
+ p->level--;
+ return NULL;
+ }
+ D(fprintf(stderr, "%*c> import_name[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "'import' dotted_as_names"));
+ Token * _keyword;
+ asdl_alias_seq* a;
+ if (
(_keyword = _PyPegen_expect_token(p, 500)) // token='import'
&&
(a = dotted_as_names_rule(p)) // dotted_as_names
)
{
- D(fprintf(stderr, "%*c+ import_name[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "\"lazy\"? 'import' dotted_as_names"));
+ D(fprintf(stderr, "%*c+ import_name[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "'import' dotted_as_names"));
Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
if (_token == NULL) {
p->level--;
@@ -3534,7 +3570,7 @@ import_name_rule(Parser *p)
UNUSED(_end_lineno); // Only used by EXTRA macro
int _end_col_offset = _token->end_col_offset;
UNUSED(_end_col_offset); // Only used by EXTRA macro
- _res = _PyAST_Import ( a , lazy ? 1 : 0 , EXTRA );
+ _res = _PyAST_Import ( a , 0 , EXTRA );
if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
p->error_indicator = 1;
p->level--;
@@ -3544,7 +3580,7 @@ import_name_rule(Parser *p)
}
p->mark = _mark;
D(fprintf(stderr, "%*c%s import_name[%d-%d]: %s failed!\n", p->level,
' ',
- p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"\"lazy\"? 'import' dotted_as_names"));
+ p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"'import' dotted_as_names"));
}
_res = NULL;
done:
@@ -3554,8 +3590,10 @@ import_name_rule(Parser *p)
// import_from:
// | invalid_import_from
-// | "lazy"? 'from' (('.' | '...'))* dotted_name 'import'
import_from_targets
-// | "lazy"? 'from' (('.' | '...'))+ 'import' import_from_targets
+// | "lazy" 'from' (('.' | '...'))* dotted_name 'import'
import_from_targets
+// | "lazy" 'from' (('.' | '...'))+ 'import' import_from_targets
+// | 'from' (('.' | '...'))* dotted_name 'import' import_from_targets
+// | 'from' (('.' | '...'))+ 'import' import_from_targets
static stmt_ty
import_from_rule(Parser *p)
{
@@ -3596,20 +3634,20 @@ import_from_rule(Parser *p)
D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level,
' ',
p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"invalid_import_from"));
}
- { // "lazy"? 'from' (('.' | '...'))* dotted_name 'import'
import_from_targets
+ { // "lazy" 'from' (('.' | '...'))* dotted_name 'import'
import_from_targets
if (p->error_indicator) {
p->level--;
return NULL;
}
- D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))* dotted_name 'import'
import_from_targets"));
+ D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "\"lazy\" 'from' (('.' | '...'))* dotted_name 'import'
import_from_targets"));
Token * _keyword;
Token * _keyword_1;
asdl_seq * a;
expr_ty b;
asdl_alias_seq* c;
- void *lazy;
+ expr_ty lazy;
if (
- (lazy = _PyPegen_expect_soft_keyword(p, "lazy"),
!p->error_indicator) // "lazy"?
+ (lazy = _PyPegen_expect_soft_keyword(p, "lazy")) //
soft_keyword='"lazy"'
&&
(_keyword = _PyPegen_expect_token(p, 501)) // token='from'
&&
@@ -3622,7 +3660,7 @@ import_from_rule(Parser *p)
(c = import_from_targets_rule(p)) // import_from_targets
)
{
- D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))* dotted_name
'import' import_from_targets"));
+ D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "\"lazy\" 'from' (('.' | '...'))* dotted_name
'import' import_from_targets"));
Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
if (_token == NULL) {
p->level--;
@@ -3632,7 +3670,7 @@ import_from_rule(Parser *p)
UNUSED(_end_lineno); // Only used by EXTRA macro
int _end_col_offset = _token->end_col_offset;
UNUSED(_end_col_offset); // Only used by EXTRA macro
- _res = _PyPegen_checked_from_import ( p , a , b , c , lazy , EXTRA
);
+ _res = CHECK_VERSION ( stmt_ty , 15 , "Lazy imports are" ,
_PyPegen_checked_from_import ( p , a , b , c , lazy , EXTRA ) );
if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
p->error_indicator = 1;
p->level--;
@@ -3642,22 +3680,109 @@ import_from_rule(Parser *p)
}
p->mark = _mark;
D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level,
' ',
- p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"\"lazy\"? 'from' (('.' | '...'))* dotted_name 'import' import_from_targets"));
+ p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"\"lazy\" 'from' (('.' | '...'))* dotted_name 'import' import_from_targets"));
}
- { // "lazy"? 'from' (('.' | '...'))+ 'import' import_from_targets
+ { // "lazy" 'from' (('.' | '...'))+ 'import' import_from_targets
if (p->error_indicator) {
p->level--;
return NULL;
}
- D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))+ 'import'
import_from_targets"));
- Token * _keyword;
+ D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "\"lazy\" 'from' (('.' | '...'))+ 'import'
import_from_targets"));
+ expr_ty _keyword;
Token * _keyword_1;
+ Token * _keyword_2;
asdl_seq * a;
asdl_alias_seq* b;
- void *lazy;
if (
- (lazy = _PyPegen_expect_soft_keyword(p, "lazy"),
!p->error_indicator) // "lazy"?
+ (_keyword = _PyPegen_expect_soft_keyword(p, "lazy")) //
soft_keyword='"lazy"'
+ &&
+ (_keyword_1 = _PyPegen_expect_token(p, 501)) // token='from'
+ &&
+ (a = _loop1_18_rule(p)) // (('.' | '...'))+
+ &&
+ (_keyword_2 = _PyPegen_expect_token(p, 500)) // token='import'
&&
+ (b = import_from_targets_rule(p)) // import_from_targets
+ )
+ {
+ D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "\"lazy\" 'from' (('.' | '...'))+ 'import'
import_from_targets"));
+ Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
+ if (_token == NULL) {
+ p->level--;
+ return NULL;
+ }
+ int _end_lineno = _token->end_lineno;
+ UNUSED(_end_lineno); // Only used by EXTRA macro
+ int _end_col_offset = _token->end_col_offset;
+ UNUSED(_end_col_offset); // Only used by EXTRA macro
+ _res = CHECK_VERSION ( stmt_ty , 15 , "Lazy imports are" ,
_PyAST_ImportFrom ( NULL , b , _PyPegen_seq_count_dots ( a ) , 1 , EXTRA ) );
+ if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
+ p->error_indicator = 1;
+ p->level--;
+ return NULL;
+ }
+ goto done;
+ }
+ p->mark = _mark;
+ D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level,
' ',
+ p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"\"lazy\" 'from' (('.' | '...'))+ 'import' import_from_targets"));
+ }
+ { // 'from' (('.' | '...'))* dotted_name 'import' import_from_targets
+ if (p->error_indicator) {
+ p->level--;
+ return NULL;
+ }
+ D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "'from' (('.' | '...'))* dotted_name 'import'
import_from_targets"));
+ Token * _keyword;
+ Token * _keyword_1;
+ asdl_seq * a;
+ expr_ty b;
+ asdl_alias_seq* c;
+ if (
+ (_keyword = _PyPegen_expect_token(p, 501)) // token='from'
+ &&
+ (a = _loop0_17_rule(p)) // (('.' | '...'))*
+ &&
+ (b = dotted_name_rule(p)) // dotted_name
+ &&
+ (_keyword_1 = _PyPegen_expect_token(p, 500)) // token='import'
+ &&
+ (c = import_from_targets_rule(p)) // import_from_targets
+ )
+ {
+ D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "'from' (('.' | '...'))* dotted_name 'import'
import_from_targets"));
+ Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
+ if (_token == NULL) {
+ p->level--;
+ return NULL;
+ }
+ int _end_lineno = _token->end_lineno;
+ UNUSED(_end_lineno); // Only used by EXTRA macro
+ int _end_col_offset = _token->end_col_offset;
+ UNUSED(_end_col_offset); // Only used by EXTRA macro
+ _res = _PyPegen_checked_from_import ( p , a , b , c , NULL , EXTRA
);
+ if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
+ p->error_indicator = 1;
+ p->level--;
+ return NULL;
+ }
+ goto done;
+ }
+ p->mark = _mark;
+ D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level,
' ',
+ p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'from'
(('.' | '...'))* dotted_name 'import' import_from_targets"));
+ }
+ { // 'from' (('.' | '...'))+ 'import' import_from_targets
+ if (p->error_indicator) {
+ p->level--;
+ return NULL;
+ }
+ D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "'from' (('.' | '...'))+ 'import' import_from_targets"));
+ Token * _keyword;
+ Token * _keyword_1;
+ asdl_seq * a;
+ asdl_alias_seq* b;
+ if (
(_keyword = _PyPegen_expect_token(p, 501)) // token='from'
&&
(a = _loop1_18_rule(p)) // (('.' | '...'))+
@@ -3667,7 +3792,7 @@ import_from_rule(Parser *p)
(b = import_from_targets_rule(p)) // import_from_targets
)
{
- D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))+ 'import'
import_from_targets"));
+ D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "'from' (('.' | '...'))+ 'import'
import_from_targets"));
Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
if (_token == NULL) {
p->level--;
@@ -3677,7 +3802,7 @@ import_from_rule(Parser *p)
UNUSED(_end_lineno); // Only used by EXTRA macro
int _end_col_offset = _token->end_col_offset;
UNUSED(_end_col_offset); // Only used by EXTRA macro
- _res = _PyAST_ImportFrom ( NULL , b , _PyPegen_seq_count_dots ( a
) , lazy ? 1 : 0 , EXTRA );
+ _res = _PyAST_ImportFrom ( NULL , b , _PyPegen_seq_count_dots ( a
) , 0 , EXTRA );
if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
p->error_indicator = 1;
p->level--;
@@ -3687,7 +3812,7 @@ import_from_rule(Parser *p)
}
p->mark = _mark;
D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level,
' ',
- p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"\"lazy\"? 'from' (('.' | '...'))+ 'import' import_from_targets"));
+ p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'from'
(('.' | '...'))+ 'import' import_from_targets"));
}
_res = NULL;
done:
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]