gh-150459: Fix SyntaxError message for from x lazy import y (#150877)

This commit is contained in:
sobolevn authored and GitHub committed 2026-07-04 14:17:58 +01:00
1 parent 832d557111
commit ec5f154e33
7 files changed
+651 -401

No files matched your search

+7 -1
View File
@@ -229,8 +229,9 @@ import_name[stmt_ty]:
# 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_future_import(p, b->v.Name.id, c, _PyPegen_seq_count_dots(a), lazy, EXTRA) }
_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) }
import_from_targets[asdl_alias_seq*]:
@@ -1445,6 +1446,11 @@ invalid_import_from_as_name:
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a,
"cannot use %s as import target", _PyPegen_get_expr_name(a)) }
invalid_import_from:
| 'from' ('.' | '...')* dotted_name a="lazy" 'import' import_from_targets {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a,
"use 'lazy from ... ' instead of 'from ... lazy import'") }
invalid_import_from_targets:
| import_from_as_names ',' NEWLINE {
RAISE_SYNTAX_ERROR("trailing comma not allowed without surrounding parentheses") }
+5 -3
View File
@@ -123,9 +123,11 @@ extern void _PyErr_SetNone(PyThreadState *tstate, PyObject *exception);
extern PyObject* _PyErr_NoMemory(PyThreadState *tstate);
extern int _PyErr_EmitSyntaxWarning(PyObject *msg, PyObject *filename, int lineno, int col_offset,
int end_lineno, int end_col_offset,
PyObject *module);
// Export for test_peg_generator
PyAPI_FUNC(int) _PyErr_EmitSyntaxWarning(PyObject *msg, PyObject *filename,
int lineno, int col_offset,
int end_lineno, int end_col_offset,
PyObject *module);
extern void _PyErr_RaiseSyntaxError(PyObject *msg, PyObject *filename, int lineno, int col_offset,
int end_lineno, int end_col_offset);
+93
View File
@@ -2872,6 +2872,14 @@ class SyntaxWarningTest(unittest.TestCase):
with self.assertWarnsRegex(SyntaxWarning, errtext):
compile(code, filename, mode)
def check_no_warning(self, code, filename="<testcase>", mode="exec"):
"""Check that compiling code does not raise any warnings."""
import warnings
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
compile(code, filename, mode)
self.assertEqual(caught, [])
def test_return_in_finally(self):
source = textwrap.dedent("""
def f():
@@ -2942,6 +2950,75 @@ class SyntaxWarningTest(unittest.TestCase):
""")
self.check_warning(source, f"'{kw}' in a 'finally' block")
def test_from_lazy_imports(self):
# gh-150459
self.check_warning(
"from . lazy import x",
"did you mean 'lazy from . import'?",
)
self.check_warning(
"from . lazy import x as y",
"did you mean 'lazy from . import'?",
)
self.check_warning(
"from . lazy import *",
"did you mean 'lazy from . import'?",
)
self.check_warning(
"from .. lazy import x",
"did you mean 'lazy from .. import'?",
)
self.check_warning(
"from ... lazy import x",
"did you mean 'lazy from ... import'?",
)
self.check_warning(
"from .... lazy import x",
"did you mean 'lazy from .... import'?",
)
self.check_warning(
"from . \\\n lazy import x",
"did you mean 'lazy from . import'?",
)
self.check_warning(
"from .\\\nlazy import x",
"did you mean 'lazy from . import'?",
)
self.check_warning(
"from .\tlazy import x",
"did you mean 'lazy from . import'?",
)
def test_not_from_lazy_imports(self):
self.check_no_warning("from .lazy import x")
self.check_no_warning("from .lazy import *")
self.check_no_warning("from ..lazy import x")
self.check_no_warning("from ...lazy import x")
self.check_no_warning("from .lazy.sub import x")
self.check_no_warning("from ..lazy.sub import x")
self.check_no_warning("from ...lazy.sub import x")
self.check_no_warning("from . lazier import x")
self.check_no_warning("from . lazy_module import x")
self.check_no_warning("from . lazy.sub import x")
self.check_no_warning("from . sub.lazy import x")
self.check_no_warning("from lazy import x")
self.check_no_warning("from lazy.sub import x")
self.check_no_warning("lazy from . lazy import x")
self.check_no_warning("from . import lazy")
def test_from_lazy_imports_as_error(self):
import warnings
with warnings.catch_warnings():
warnings.simplefilter("error", SyntaxWarning)
with self.assertRaisesRegex(
SyntaxError,
re.escape("did you mean 'lazy from . import'?"),
) as cm:
compile("from . lazy import x", "<test>", "exec")
self.assertEqual(cm.exception.lineno, 1)
self.assertEqual(cm.exception.offset, 8)
self.assertEqual(cm.exception.end_offset, 12)
class SyntaxErrorTestCase(unittest.TestCase):
@@ -3662,6 +3739,22 @@ def outer():
lazy from collections import deque
""", "lazy from ... import not allowed inside functions")
self._check_error("""\
from os lazy import path
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
self._check_error("""\
from os.path lazy import join
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
self._check_error("""\
from .mod lazy import join
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
self._check_error("""\
from ..mod lazy import join
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
self._check_error("""\
from ...mod lazy import join
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
def test_lazy_import_valid_cases(self):
"""Test that lazy imports work at module level."""
# These should compile without errors
@@ -0,0 +1,3 @@
Fix :exc:`SyntaxError` error message for ``from x lazy import y``.
Raise :exc:`SyntaxWarning` on ``from . lazy import x``
(with whitespace between the dots and a module named ``lazy``).
+67 -4
View File
@@ -2,6 +2,7 @@
#include "pycore_pystate.h" // _PyInterpreterState_GET()
#include "pycore_runtime.h" // _PyRuntime
#include "pycore_unicodeobject.h" // _PyUnicode_InternImmortal()
#include "pycore_pyerrors.h" // _PyErr_EmitSyntaxWarning()
#include "pegen.h"
#include "string_parser.h" // _PyPegen_decode_string()
@@ -2007,11 +2008,64 @@ _PyPegen_concatenate_strings(Parser *p, asdl_expr_seq *strings,
col_offset, end_lineno, end_col_offset, arena);
}
static int
_warn_relative_import_of_lazy(Parser *p, asdl_seq *dots, expr_ty module)
{
// Warn about `from . lazy import x`: the whitespace between the dots and
// the module name is insignificant, so this is parsed exactly like
// `from .lazy import x` (an import of the relative module "lazy"), but it
// is most likely a transposition of `lazy from . import x` (PEP 810).
if (p->call_invalid_rules) {
return 0;
}
// Only fire if there is whitespace between the last dot and the name,
// i.e. not for the common `from .lazy import x` spelling.
Token *last_dot = asdl_seq_GET_UNTYPED(dots, asdl_seq_LEN(dots) - 1);
if (
last_dot->end_lineno == module->lineno
&& last_dot->end_col_offset == module->col_offset
) {
return 0;
}
int count = _PyPegen_seq_count_dots(dots);
char *buf = PyMem_RawMalloc(count + 1);
if (buf == NULL) {
PyErr_NoMemory();
return -1;
}
memset(buf, '.', count);
buf[count] = '\0';
PyObject *msg = PyUnicode_FromFormat(
"'from %s lazy import' is the same as 'from %slazy import'; "
"did you mean 'lazy from %s import'?",
buf, buf, buf);
PyMem_RawFree(buf);
if (msg == NULL) {
return -1;
}
int res = _PyErr_EmitSyntaxWarning(msg,
p->tok->filename,
module->lineno,
module->col_offset + 1,
module->end_lineno,
module->end_col_offset + 1,
p->tok->module);
Py_DECREF(msg);
return res;
}
stmt_ty
_PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq * names,
int level, expr_ty lazy_token, int lineno,
int col_offset, int end_lineno, int end_col_offset,
PyArena *arena) {
_PyPegen_checked_from_import(Parser *p, asdl_seq *dots, expr_ty module_name,
asdl_alias_seq *names, expr_ty lazy_token, int lineno,
int col_offset, int end_lineno, int end_col_offset,
PyArena *arena)
{
identifier module = module_name->v.Name.id;
int level = _PyPegen_seq_count_dots(dots);
if (level == 0 && PyUnicode_CompareWithASCIIString(module, "__future__") == 0) {
if (lazy_token) {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(lazy_token,
@@ -2025,6 +2079,15 @@ _PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq * na
}
}
}
else if (
level > 0
&& lazy_token == NULL
&& PyUnicode_CompareWithASCIIString(module, "lazy") == 0
) {
if (_warn_relative_import_of_lazy(p, dots, module_name) < 0) {
return NULL;
}
}
return _PyAST_ImportFrom(module, names, level, lazy_token ? 1 : 0, lineno,
col_offset, end_lineno, end_col_offset, arena);
}
+472 -391
View File
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -371,8 +371,10 @@ mod_ty _PyPegen_make_module(Parser *, asdl_stmt_seq *);
void *_PyPegen_arguments_parsing_error(Parser *, expr_ty);
expr_ty _PyPegen_get_last_comprehension_item(comprehension_ty comprehension);
void *_PyPegen_nonparen_genexp_in_call(Parser *p, expr_ty args, asdl_comprehension_seq *comprehensions);
stmt_ty _PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq *,
int, expr_ty, int, int, int, int, PyArena *);
stmt_ty _PyPegen_checked_from_import(Parser *p, asdl_seq *dots, expr_ty module_name,
asdl_alias_seq *names, expr_ty lazy_token, int lineno,
int col_offset, int end_lineno, int end_col_offset,
PyArena *arena);
asdl_stmt_seq* _PyPegen_register_stmts(Parser *p, asdl_stmt_seq* stmts);
stmt_ty _PyPegen_register_stmt(Parser *p, stmt_ty s);