mirror of
https://github.com/python/cpython.git
synced 2026-10-28 10:25:05 +08:00
gh-150459: Fix SyntaxError message for from x lazy import y (#150877)
This commit is contained in:
1 parent
832d557111
commit
ec5f154e33
7 files changed
+651
-401
No files matched your search
+7
-1
@@ -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") }
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Generated
+472
-391
File diff suppressed because it is too large.
Load diff
+4
-2
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user