Skip to content

Commit ec5f154

Browse files
authored
gh-150459: Fix SyntaxError message for from x lazy import y (#150877)
1 parent 832d557 commit ec5f154

7 files changed

Lines changed: 651 additions & 401 deletions

File tree

Grammar/python.gram

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,9 @@ import_name[stmt_ty]:
229229

230230
# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS
231231
import_from[stmt_ty]:
232+
| invalid_import_from
232233
| lazy="lazy"? 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
233-
_PyPegen_checked_future_import(p, b->v.Name.id, c, _PyPegen_seq_count_dots(a), lazy, EXTRA) }
234+
_PyPegen_checked_from_import(p, a, b, c, lazy, EXTRA) }
234235
| lazy="lazy"? 'from' a=('.' | '...')+ 'import' b=import_from_targets {
235236
_PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), lazy ? 1 : 0, EXTRA) }
236237
import_from_targets[asdl_alias_seq*]:
@@ -1445,6 +1446,11 @@ invalid_import_from_as_name:
14451446
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a,
14461447
"cannot use %s as import target", _PyPegen_get_expr_name(a)) }
14471448

1449+
invalid_import_from:
1450+
| 'from' ('.' | '...')* dotted_name a="lazy" 'import' import_from_targets {
1451+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a,
1452+
"use 'lazy from ... ' instead of 'from ... lazy import'") }
1453+
14481454
invalid_import_from_targets:
14491455
| import_from_as_names ',' NEWLINE {
14501456
RAISE_SYNTAX_ERROR("trailing comma not allowed without surrounding parentheses") }

Include/internal/pycore_pyerrors.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,11 @@ extern void _PyErr_SetNone(PyThreadState *tstate, PyObject *exception);
123123

124124
extern PyObject* _PyErr_NoMemory(PyThreadState *tstate);
125125

126-
extern int _PyErr_EmitSyntaxWarning(PyObject *msg, PyObject *filename, int lineno, int col_offset,
127-
int end_lineno, int end_col_offset,
128-
PyObject *module);
126+
// Export for test_peg_generator
127+
PyAPI_FUNC(int) _PyErr_EmitSyntaxWarning(PyObject *msg, PyObject *filename,
128+
int lineno, int col_offset,
129+
int end_lineno, int end_col_offset,
130+
PyObject *module);
129131
extern void _PyErr_RaiseSyntaxError(PyObject *msg, PyObject *filename, int lineno, int col_offset,
130132
int end_lineno, int end_col_offset);
131133

Lib/test/test_syntax.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2872,6 +2872,14 @@ def check_warning(self, code, errtext, filename="<testcase>", mode="exec"):
28722872
with self.assertWarnsRegex(SyntaxWarning, errtext):
28732873
compile(code, filename, mode)
28742874

2875+
def check_no_warning(self, code, filename="<testcase>", mode="exec"):
2876+
"""Check that compiling code does not raise any warnings."""
2877+
import warnings
2878+
with warnings.catch_warnings(record=True) as caught:
2879+
warnings.simplefilter("always")
2880+
compile(code, filename, mode)
2881+
self.assertEqual(caught, [])
2882+
28752883
def test_return_in_finally(self):
28762884
source = textwrap.dedent("""
28772885
def f():
@@ -2942,6 +2950,75 @@ def test_break_and_continue_in_finally(self):
29422950
""")
29432951
self.check_warning(source, f"'{kw}' in a 'finally' block")
29442952

2953+
def test_from_lazy_imports(self):
2954+
# gh-150459
2955+
self.check_warning(
2956+
"from . lazy import x",
2957+
"did you mean 'lazy from . import'?",
2958+
)
2959+
self.check_warning(
2960+
"from . lazy import x as y",
2961+
"did you mean 'lazy from . import'?",
2962+
)
2963+
self.check_warning(
2964+
"from . lazy import *",
2965+
"did you mean 'lazy from . import'?",
2966+
)
2967+
self.check_warning(
2968+
"from .. lazy import x",
2969+
"did you mean 'lazy from .. import'?",
2970+
)
2971+
self.check_warning(
2972+
"from ... lazy import x",
2973+
"did you mean 'lazy from ... import'?",
2974+
)
2975+
self.check_warning(
2976+
"from .... lazy import x",
2977+
"did you mean 'lazy from .... import'?",
2978+
)
2979+
self.check_warning(
2980+
"from . \\\n lazy import x",
2981+
"did you mean 'lazy from . import'?",
2982+
)
2983+
self.check_warning(
2984+
"from .\\\nlazy import x",
2985+
"did you mean 'lazy from . import'?",
2986+
)
2987+
self.check_warning(
2988+
"from .\tlazy import x",
2989+
"did you mean 'lazy from . import'?",
2990+
)
2991+
2992+
def test_not_from_lazy_imports(self):
2993+
self.check_no_warning("from .lazy import x")
2994+
self.check_no_warning("from .lazy import *")
2995+
self.check_no_warning("from ..lazy import x")
2996+
self.check_no_warning("from ...lazy import x")
2997+
self.check_no_warning("from .lazy.sub import x")
2998+
self.check_no_warning("from ..lazy.sub import x")
2999+
self.check_no_warning("from ...lazy.sub import x")
3000+
self.check_no_warning("from . lazier import x")
3001+
self.check_no_warning("from . lazy_module import x")
3002+
self.check_no_warning("from . lazy.sub import x")
3003+
self.check_no_warning("from . sub.lazy import x")
3004+
self.check_no_warning("from lazy import x")
3005+
self.check_no_warning("from lazy.sub import x")
3006+
self.check_no_warning("lazy from . lazy import x")
3007+
self.check_no_warning("from . import lazy")
3008+
3009+
def test_from_lazy_imports_as_error(self):
3010+
import warnings
3011+
with warnings.catch_warnings():
3012+
warnings.simplefilter("error", SyntaxWarning)
3013+
with self.assertRaisesRegex(
3014+
SyntaxError,
3015+
re.escape("did you mean 'lazy from . import'?"),
3016+
) as cm:
3017+
compile("from . lazy import x", "<test>", "exec")
3018+
self.assertEqual(cm.exception.lineno, 1)
3019+
self.assertEqual(cm.exception.offset, 8)
3020+
self.assertEqual(cm.exception.end_offset, 12)
3021+
29453022

29463023
class SyntaxErrorTestCase(unittest.TestCase):
29473024

@@ -3662,6 +3739,22 @@ def inner():
36623739
lazy from collections import deque
36633740
""", "lazy from ... import not allowed inside functions")
36643741

3742+
self._check_error("""\
3743+
from os lazy import path
3744+
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
3745+
self._check_error("""\
3746+
from os.path lazy import join
3747+
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
3748+
self._check_error("""\
3749+
from .mod lazy import join
3750+
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
3751+
self._check_error("""\
3752+
from ..mod lazy import join
3753+
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
3754+
self._check_error("""\
3755+
from ...mod lazy import join
3756+
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
3757+
36653758
def test_lazy_import_valid_cases(self):
36663759
"""Test that lazy imports work at module level."""
36673760
# These should compile without errors
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :exc:`SyntaxError` error message for ``from x lazy import y``.
2+
Raise :exc:`SyntaxWarning` on ``from . lazy import x``
3+
(with whitespace between the dots and a module named ``lazy``).

Parser/action_helpers.c

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "pycore_pystate.h" // _PyInterpreterState_GET()
33
#include "pycore_runtime.h" // _PyRuntime
44
#include "pycore_unicodeobject.h" // _PyUnicode_InternImmortal()
5+
#include "pycore_pyerrors.h" // _PyErr_EmitSyntaxWarning()
56

67
#include "pegen.h"
78
#include "string_parser.h" // _PyPegen_decode_string()
@@ -2007,11 +2008,64 @@ _PyPegen_concatenate_strings(Parser *p, asdl_expr_seq *strings,
20072008
col_offset, end_lineno, end_col_offset, arena);
20082009
}
20092010

2011+
static int
2012+
_warn_relative_import_of_lazy(Parser *p, asdl_seq *dots, expr_ty module)
2013+
{
2014+
// Warn about `from . lazy import x`: the whitespace between the dots and
2015+
// the module name is insignificant, so this is parsed exactly like
2016+
// `from .lazy import x` (an import of the relative module "lazy"), but it
2017+
// is most likely a transposition of `lazy from . import x` (PEP 810).
2018+
if (p->call_invalid_rules) {
2019+
return 0;
2020+
}
2021+
2022+
// Only fire if there is whitespace between the last dot and the name,
2023+
// i.e. not for the common `from .lazy import x` spelling.
2024+
Token *last_dot = asdl_seq_GET_UNTYPED(dots, asdl_seq_LEN(dots) - 1);
2025+
if (
2026+
last_dot->end_lineno == module->lineno
2027+
&& last_dot->end_col_offset == module->col_offset
2028+
) {
2029+
return 0;
2030+
}
2031+
2032+
int count = _PyPegen_seq_count_dots(dots);
2033+
char *buf = PyMem_RawMalloc(count + 1);
2034+
if (buf == NULL) {
2035+
PyErr_NoMemory();
2036+
return -1;
2037+
}
2038+
memset(buf, '.', count);
2039+
buf[count] = '\0';
2040+
2041+
PyObject *msg = PyUnicode_FromFormat(
2042+
"'from %s lazy import' is the same as 'from %slazy import'; "
2043+
"did you mean 'lazy from %s import'?",
2044+
buf, buf, buf);
2045+
PyMem_RawFree(buf);
2046+
if (msg == NULL) {
2047+
return -1;
2048+
}
2049+
2050+
int res = _PyErr_EmitSyntaxWarning(msg,
2051+
p->tok->filename,
2052+
module->lineno,
2053+
module->col_offset + 1,
2054+
module->end_lineno,
2055+
module->end_col_offset + 1,
2056+
p->tok->module);
2057+
Py_DECREF(msg);
2058+
return res;
2059+
}
2060+
20102061
stmt_ty
2011-
_PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq * names,
2012-
int level, expr_ty lazy_token, int lineno,
2013-
int col_offset, int end_lineno, int end_col_offset,
2014-
PyArena *arena) {
2062+
_PyPegen_checked_from_import(Parser *p, asdl_seq *dots, expr_ty module_name,
2063+
asdl_alias_seq *names, expr_ty lazy_token, int lineno,
2064+
int col_offset, int end_lineno, int end_col_offset,
2065+
PyArena *arena)
2066+
{
2067+
identifier module = module_name->v.Name.id;
2068+
int level = _PyPegen_seq_count_dots(dots);
20152069
if (level == 0 && PyUnicode_CompareWithASCIIString(module, "__future__") == 0) {
20162070
if (lazy_token) {
20172071
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(lazy_token,
@@ -2025,6 +2079,15 @@ _PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq * na
20252079
}
20262080
}
20272081
}
2082+
else if (
2083+
level > 0
2084+
&& lazy_token == NULL
2085+
&& PyUnicode_CompareWithASCIIString(module, "lazy") == 0
2086+
) {
2087+
if (_warn_relative_import_of_lazy(p, dots, module_name) < 0) {
2088+
return NULL;
2089+
}
2090+
}
20282091
return _PyAST_ImportFrom(module, names, level, lazy_token ? 1 : 0, lineno,
20292092
col_offset, end_lineno, end_col_offset, arena);
20302093
}

0 commit comments

Comments
 (0)