Skip to content

Commit d061327

Browse files
committed
Raise SyntaxWarning on from . lazy import x
1 parent 3a0dae0 commit d061327

6 files changed

Lines changed: 155 additions & 8 deletions

File tree

Grammar/python.gram

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ import_name[stmt_ty]:
231231
import_from[stmt_ty]:
232232
| invalid_import_from
233233
| lazy="lazy"? 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
234-
_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) }
235235
| lazy="lazy"? 'from' a=('.' | '...')+ 'import' b=import_from_targets {
236236
_PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), lazy ? 1 : 0, EXTRA) }
237237
import_from_targets[asdl_alias_seq*]:

Lib/test/test_syntax.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2872,6 +2872,13 @@ 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+
with warnings.catch_warnings(record=True) as caught:
2878+
warnings.simplefilter("always")
2879+
compile(source, filename, mode)
2880+
self.assertEqual(caught, [])
2881+
28752882
def test_return_in_finally(self):
28762883
source = textwrap.dedent("""
28772884
def f():
@@ -2942,6 +2949,74 @@ def test_break_and_continue_in_finally(self):
29422949
""")
29432950
self.check_warning(source, f"'{kw}' in a 'finally' block")
29442951

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

29463021
class SyntaxErrorTestCase(unittest.TestCase):
29473022

@@ -3620,6 +3695,18 @@ def inner():
36203695

36213696
self._check_error("""\
36223697
from os lazy import path
3698+
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
3699+
self._check_error("""\
3700+
from os.path lazy import join
3701+
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
3702+
self._check_error("""\
3703+
from .mod lazy import join
3704+
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
3705+
self._check_error("""\
3706+
from ..mod lazy import join
3707+
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
3708+
self._check_error("""\
3709+
from ...mod lazy import join
36233710
""", "use 'lazy from ... ' instead of 'from ... lazy import'")
36243711

36253712
def test_lazy_import_valid_cases(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
Fix :exc:`SyntaxError` error message for ``from x lazy import y``.
2+
Raise :exc:`SyntaxWarning` on ``from . lazy import x``.

Parser/action_helpers.c

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1973,11 +1973,59 @@ _PyPegen_concatenate_strings(Parser *p, asdl_expr_seq *strings,
19731973
col_offset, end_lineno, end_col_offset, arena);
19741974
}
19751975

1976+
static int
1977+
_warn_relative_import_of_lazy(Parser *p, asdl_seq *dots, expr_ty module)
1978+
{
1979+
// Warn about `from . lazy import x`: the whitespace between the dots and
1980+
// the module name is insignificant, so this is parsed exactly like
1981+
// `from .lazy import x` (an import of the relative module "lazy"), but it
1982+
// is most likely a transposition of `lazy from . import x` (PEP 810).
1983+
if (p->call_invalid_rules) {
1984+
return 0;
1985+
}
1986+
1987+
// Only fire if there is whitespace between the last dot and the name,
1988+
// i.e. not for the common `from .lazy import x` spelling.
1989+
Token *last_dot = asdl_seq_GET_UNTYPED(dots, asdl_seq_LEN(dots) - 1);
1990+
if (
1991+
last_dot->end_lineno == module->lineno
1992+
&& last_dot->end_col_offset == module->col_offset
1993+
) {
1994+
return 0;
1995+
}
1996+
1997+
int count = _PyPegen_seq_count_dots(dots);
1998+
char *buf = malloc(count + 1);
1999+
if (buf == NULL) {
2000+
PyErr_NoMemory();
2001+
return -1;
2002+
}
2003+
memset(buf, '.', count);
2004+
buf[count] = '\0';
2005+
2006+
PyObject *msg = PyUnicode_FromFormat(
2007+
"'from %s lazy import' is the same as 'from %slazy import'; "
2008+
"did you mean 'lazy from %s import'?",
2009+
buf, buf, buf);
2010+
free(buf);
2011+
if (msg == NULL) {
2012+
return -1;
2013+
}
2014+
2015+
return _PyErr_EmitSyntaxWarning(msg, p->tok->filename,
2016+
module->lineno, module->col_offset,
2017+
module->end_lineno, module->end_col_offset,
2018+
p->tok->module);
2019+
}
2020+
19762021
stmt_ty
1977-
_PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq * names,
1978-
int level, expr_ty lazy_token, int lineno,
1979-
int col_offset, int end_lineno, int end_col_offset,
1980-
PyArena *arena) {
2022+
_PyPegen_checked_from_import(Parser *p, asdl_seq *dots, expr_ty module_name,
2023+
asdl_alias_seq *names, expr_ty lazy_token, int lineno,
2024+
int col_offset, int end_lineno, int end_col_offset,
2025+
PyArena *arena)
2026+
{
2027+
identifier module = module_name->v.Name.id;
2028+
int level = _PyPegen_seq_count_dots(dots);
19812029
if (level == 0 && PyUnicode_CompareWithASCIIString(module, "__future__") == 0) {
19822030
if (lazy_token) {
19832031
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(lazy_token,
@@ -1991,6 +2039,15 @@ _PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq * na
19912039
}
19922040
}
19932041
}
2042+
else if (
2043+
level > 0
2044+
&& lazy_token == NULL
2045+
&& PyUnicode_CompareWithASCIIString(module, "lazy") == 0
2046+
) {
2047+
if (_warn_relative_import_of_lazy(p, dots, module_name) < 0) {
2048+
return NULL;
2049+
}
2050+
}
19942051
return _PyAST_ImportFrom(module, names, level, lazy_token ? 1 : 0, lineno,
19952052
col_offset, end_lineno, end_col_offset, arena);
19962053
}

Parser/parser.c

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Parser/pegen.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,10 @@ mod_ty _PyPegen_make_module(Parser *, asdl_stmt_seq *);
366366
void *_PyPegen_arguments_parsing_error(Parser *, expr_ty);
367367
expr_ty _PyPegen_get_last_comprehension_item(comprehension_ty comprehension);
368368
void *_PyPegen_nonparen_genexp_in_call(Parser *p, expr_ty args, asdl_comprehension_seq *comprehensions);
369-
stmt_ty _PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq *,
370-
int, expr_ty, int, int, int, int, PyArena *);
369+
stmt_ty _PyPegen_checked_from_import(Parser *p, asdl_seq *dots, expr_ty module_name,
370+
asdl_alias_seq *names, expr_ty lazy_token, int lineno,
371+
int col_offset, int end_lineno, int end_col_offset,
372+
PyArena *arena);
371373
asdl_stmt_seq* _PyPegen_register_stmts(Parser *p, asdl_stmt_seq* stmts);
372374
stmt_ty _PyPegen_register_stmt(Parser *p, stmt_ty s);
373375

0 commit comments

Comments
 (0)