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
1 change: 1 addition & 0 deletions Include/internal/pycore_lazyimportobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ PyAPI_FUNC(PyObject *) _PyLazyImport_New(
extern PyObject *_PyLazyImport_LoadChild(
PyThreadState *tstate, PyObject *declaration, PyObject *name);
extern int _PyLazyImport_IsActive(PyObject *declaration);
extern int _PyLazyImport_ImportsModule(PyObject *placeholder, PyObject *name);

extern int _PyLazyImport_IsResolving(PyThreadState *tstate, PyObject *op);

Expand Down
16 changes: 10 additions & 6 deletions Lib/importlib/_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -1250,13 +1250,17 @@ def _find_and_load_unlocked(name, import_, *, lazy_submodule=False):
if parent_spec:
parent_spec._uninitialized_submodules.pop()
if parent:
# Set the module as an attribute on its parent.
# Set the module as an attribute on its parent, unless the parent binds
# that name to a lazy import that imports this module. Run eagerly, that
# statement would import this module first and then bind the name, so
# its binding wins.
parent_module = sys.modules[parent]
try:
setattr(parent_module, child, module)
except AttributeError:
msg = f"Cannot set an attribute on {parent!r} for child module {child!r}"
_warnings.warn(msg, ImportWarning)
if not _imp._lazily_binds_child(parent_module, child, name):
try:
setattr(parent_module, child, module)
except AttributeError:
msg = f"Cannot set an attribute on {parent!r} for child module {child!r}"
_warnings.warn(msg, ImportWarning)
# Set attributes to lazy submodules on the module.
try:
_imp._set_lazy_attributes(module, name)
Expand Down
1 change: 0 additions & 1 deletion Lib/test/lazy_imports_all_exclude.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,3 @@ test_subprocess
test_symtable
test_tools
test_type_annotations
test_unittest
62 changes: 62 additions & 0 deletions Lib/test/test_lazy_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2639,6 +2639,68 @@ def test_version_submodule_does_not_overwrite(self):
"Copyright (c) 2001-2022 Python Software Foundation.",
)

def test_submodule_imported_elsewhere_keeps_lazy_from_binding(self):
"""Importing a submodule must not overwrite the parent's lazy
from-import of it: eagerly, that statement imported the submodule
itself and kept its own binding."""
import test.test_lazy_import.data.lazy_from_own_submodule as pkg
self.assertIsInstance(pkg.__dict__["thing"], types.LazyImportType)
import test.test_lazy_import.data.lazy_from_own_submodule.thing
self.assertEqual(pkg.thing, "the object")

def test_submodule_imported_elsewhere_keeps_from_binding_in_all_mode(self):
"""The same, with an eager-spelled from-import made lazy by the
'all' mode."""
import importlib
sys.set_lazy_imports("all")
import test.test_lazy_import.data.eager_from_own_submodule as pkg
importlib.import_module(
"test.test_lazy_import.data.eager_from_own_submodule.thing")
self.assertEqual(pkg.thing, "the object")

def test_submodule_imported_elsewhere_keeps_lazy_import_of_its_child(self):
"""A lazy import of a module inside the submodule imports the
submodule too, so its binding is kept as well."""
import test.test_lazy_import.data.lazy_import_inside_submodule as pkg
import test.test_lazy_import.data.lazy_import_inside_submodule.thing
self.assertEqual(
pkg.thing.__name__,
"test.test_lazy_import.data.lazy_import_inside_submodule.thing.inner",
)

def test_submodule_imported_elsewhere_keeps_lazy_from_package_import(self):
"""A lazy "from . import sub" imports the submodule too, so its
binding is kept, and resolves to the submodule."""
import test.test_lazy_import.data.lazy_from_package_import_submodule as pkg
import test.test_lazy_import.data.lazy_from_package_import_submodule.thing
self.assertIsInstance(pkg.__dict__["thing"], types.LazyImportType)
self.assertIs(
pkg.thing,
sys.modules[
"test.test_lazy_import.data.lazy_from_package_import_submodule.thing"],
)

def test_submodule_overwrites_lazy_binding_from_elsewhere(self):
"""A lazy binding of another module's name does not import the
submodule, so the submodule's import overwrites it, as it would
overwrite the eager binding."""
import test.test_lazy_import.data.lazy_from_other_module as pkg
import test.test_lazy_import.data.lazy_from_other_module.letters
self.assertIs(
pkg.letters,
sys.modules["test.test_lazy_import.data.lazy_from_other_module.letters"],
)

def test_submodule_overwrites_lazy_binding_from_a_sibling(self):
"""A sibling whose name starts with the submodule's name is another
module: pkg.things is not inside pkg.thing."""
import test.test_lazy_import.data.lazy_from_prefixed_submodule as pkg
import test.test_lazy_import.data.lazy_from_prefixed_submodule.thing
self.assertIs(
pkg.thing,
sys.modules["test.test_lazy_import.data.lazy_from_prefixed_submodule.thing"],
)


class ModuleVariableNameCollisionTests(unittest.TestCase):
"""Tests for name collision between a submodule and a variable."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .thing import thing
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
thing = "the object"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lazy from string import ascii_letters as letters
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
letters = "the submodule's own"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lazy from .thing import thing
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
thing = "the object"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lazy from . import thing
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lazy from .things import value as thing
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
value = "from things"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lazy import test.test_lazy_import.data.lazy_import_inside_submodule.thing.inner as thing
7 changes: 7 additions & 0 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -2752,6 +2752,13 @@ TESTSUBDIRS= idlelib/idle_test \
test/test_lazy_import/data/pkg \
test/test_lazy_import/data/badsyntax \
test/test_lazy_import/data/circular_import_pkg \
test/test_lazy_import/data/eager_from_own_submodule \
test/test_lazy_import/data/lazy_from_other_module \
test/test_lazy_import/data/lazy_from_own_submodule \
test/test_lazy_import/data/lazy_from_package_import_submodule \
test/test_lazy_import/data/lazy_from_prefixed_submodule \
test/test_lazy_import/data/lazy_import_inside_submodule \
test/test_lazy_import/data/lazy_import_inside_submodule/thing \
test/test_lazy_import/data/lazypkg \
test/test_lazy_import/data/metasyntactic \
test/test_lazy_import/data/metasyntactic/foo \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a :ref:`lazy <lazy-imports>` ``from .sub import sub`` in a package being
replaced by the submodule when ``pkg.sub`` was first imported elsewhere. The
name now resolves to the object the statement imports, as it does without
``lazy``.
46 changes: 46 additions & 0 deletions Objects/lazyimportobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,52 @@ _PyLazyImport_LoadChild(PyThreadState *tstate, PyObject *declaration,
return result;
}

// Return 1 if the module target is name or one of its submodules, 0 if not,
// -1 with an exception set on error.
static int
lazy_import_is_within(PyObject *target, PyObject *name)
{
Py_ssize_t length = PyUnicode_GET_LENGTH(name);
Py_ssize_t target_length = PyUnicode_GET_LENGTH(target);
if (target_length < length) {
return 0;
}
Py_ssize_t matches = PyUnicode_Tailmatch(target, name, 0, length, -1);
if (matches <= 0) {
return (int)matches;
}
return target_length == length ||
PyUnicode_READ_CHAR(target, length) == '.';
}

// Return 1 if resolving placeholder imports the module name or one of its
// submodules, 0 if not, -1 with an exception set on error. Run eagerly,
// such a statement would already have imported name.
int
_PyLazyImport_ImportsModule(PyObject *placeholder, PyObject *name)
{
assert(PyLazyImport_CheckExact(placeholder));
PyLazyImportObject *root = PyLazyImportObject_CAST(placeholder);
PyLazyImportObject *projection = NULL;
while (PyLazyImport_CheckExact(root->lz_from)) {
projection = root;
root = PyLazyImportObject_CAST(root->lz_from);
}
int rc = lazy_import_is_within(root->lz_from, name);
if (rc != 0 || projection == NULL) {
return rc;
}
// "from pkg import child" imports pkg.child when it is a submodule.
PyObject *child = PyUnicode_FromFormat("%U.%U", root->lz_from,
projection->lz_attr);
if (child == NULL) {
return -1;
}
rc = lazy_import_is_within(child, name);
Py_DECREF(child);
return rc;
}

int
_PyLazyImport_IsActive(PyObject *declaration)
{
Expand Down
51 changes: 50 additions & 1 deletion Python/clinic/import.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions Python/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -5570,6 +5570,38 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
Py_RETURN_NONE;
}

/*[clinic input]
_imp._lazily_binds_child -> bool
parent: object
child: unicode
name: unicode
/
Return whether the module parent binds child to a lazy import of name.

A lazy import of a module inside name counts too: resolving either one
imports name.
[clinic start generated code]*/

static int
_imp__lazily_binds_child_impl(PyObject *module, PyObject *parent,
PyObject *child, PyObject *name)
/*[clinic end generated code: output=f5db314652b57fca input=c9da3c4d4d883490]*/
{
if (!PyModule_Check(parent)) {
return 0;
}
PyObject *bound;
if (PyDict_GetItemRef(_PyModule_GetDict(parent), child, &bound) < 0) {
return -1;
}
int result = 0;
if (bound != NULL && PyLazyImport_CheckExact(bound)) {
result = _PyLazyImport_ImportsModule(bound, name);
}
Py_XDECREF(bound);
return result;
}

PyDoc_STRVAR(doc_imp,
"(Extremely) low-level import machinery bits as used by importlib.");

Expand All @@ -5594,6 +5626,7 @@ static PyMethodDef imp_methods[] = {
_IMP__FIX_CO_FILENAME_METHODDEF
_IMP_SOURCE_HASH_METHODDEF
_IMP__SET_LAZY_ATTRIBUTES_METHODDEF
_IMP__LAZILY_BINDS_CHILD_METHODDEF
{NULL, NULL} /* sentinel */
};

Expand Down
Loading