Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Grammar/python.gram
Original file line number Diff line number Diff line change
Expand Up @@ -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*]:
Expand Down Expand Up @@ -1443,6 +1444,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") }
Expand Down
8 changes: 5 additions & 3 deletions Include/internal/pycore_pyerrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
91 changes: 91 additions & 0 deletions Lib/test/test_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -2872,6 +2872,13 @@ def check_warning(self, code, errtext, filename="<testcase>", mode="exec"):
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."""
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
compile(source, filename, mode)
self.assertEqual(caught, [])

def test_return_in_finally(self):
source = textwrap.dedent("""
def f():
Expand Down Expand Up @@ -2942,6 +2949,74 @@ def test_break_and_continue_in_finally(self):
""")
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):
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):

Expand Down Expand Up @@ -3618,6 +3693,22 @@ def inner():
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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``).
66 changes: 62 additions & 4 deletions Parser/action_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -1973,11 +1974,59 @@ _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(sizeof(char *) * (count + 1));
if (buf == NULL) {
PyErr_NoMemory();
return -1;
}
memset(buf, '.', sizeof(char *) * 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);
free(buf);
if (msg == NULL) {
return -1;
}

return _PyErr_EmitSyntaxWarning(msg, p->tok->filename,
module->lineno, module->col_offset,
module->end_lineno, module->end_col_offset,
p->tok->module);
}

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,
Expand All @@ -1991,6 +2040,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);
}
Expand Down
Loading
Loading