Skip to content

Commit 4ffd6c2

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

6 files changed

Lines changed: 157 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
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: 62 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()
@@ -1973,11 +1974,59 @@ _PyPegen_concatenate_strings(Parser *p, asdl_expr_seq *strings,
19731974
col_offset, end_lineno, end_col_offset, arena);
19741975
}
19751976

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

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)