Skip to content

Commit 00da645

Browse files
committed
Import a lazy submodule that a package attribute shadows
_Py_module_getattro_impl consults lazy_pending_submodules only once ordinary lookup has missed, so a `sub` bound by pkg/__init__.py answers the read that was meant to trigger a pending `import pkg.sub`, and the submodule is never imported. An eager `import pkg.sub` replaces that attribute with the submodule, so the two modes disagree. Bind the submodule on the package instead, at the two points where the eager import would have: when the package finishes loading, and at registration when it is already loaded. _imp._set_lazy_attributes() now does what its name, and the comment on lazy_pending_submodules, already describe. That needs to tell `import a.b`, whose last component must be a submodule, from `from a import b` and `import a.b as x`, whose last component may be an ordinary attribute. lazy_pending_submodules now records which of the two a pending name came from, and IMPORT_FROM retracts the claim for the `as` form before anything is resolved. The repro in pythongh-151208 was fixed by pythongh-155194; this covers the shapes that survived it -- a sibling or parent import rebinding the name, and a lazy submodule registered after its package is loaded.
1 parent fcf0411 commit 00da645

9 files changed

Lines changed: 196 additions & 111 deletions

File tree

‎Include/internal/pycore_import.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ extern _PyLazySubmoduleImportResult _PyImport_TryLoadLazySubmodule(
4949
extern PyObject * _PyImport_LazyImportModuleLevelObject(
5050
PyThreadState *tstate, PyObject *name, PyObject *builtins,
5151
PyObject *globals, PyObject *locals, PyObject *fromlist, int level);
52+
extern int _PyImport_UnmarkPendingSubmodule(
53+
PyThreadState *tstate, PyObject *parent, PyObject *child);
5254

5355

5456
#ifdef HAVE_DLOPEN

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io
44
import dis
5+
import importlib
56
import subprocess
67
import sys
78
import textwrap
@@ -2209,27 +2210,27 @@ def test_import_after_variable_wins(self):
22092210
]
22102211
self.assertIs(module_same_name_var_order2.bar, bar_mod)
22112212

2212-
def test_import_of_parent_keeps_pending_submodule(self):
2213-
"""`import pkg` should not discard a pending `import pkg.sub`."""
2214-
# gh-151208: both statements bind `pkg`, and the submodule is
2215-
# shadowed by an attribute of the package, so dropping the pending
2216-
# import leaves that attribute in place instead of the submodule.
2217-
from test.test_lazy_import.data import (
2218-
import_parent_after_submodule as mod)
2213+
# gh-151208: `import pkg.sub` imports the submodule even when the package
2214+
# binds its own `sub`, as the eager import does.
2215+
def check_shadowed_submodule(self, driver):
2216+
mod = importlib.import_module(f"test.test_lazy_import.data.{driver}")
22192217
pkg = mod.test.test_lazy_import.data.submodule_name_shadowed
2220-
sub = pkg.sub
2221-
self.assertIsInstance(sub, types.ModuleType)
2222-
self.assertIs(sub, sys.modules[
2218+
self.assertIsInstance(pkg.sub, types.ModuleType)
2219+
self.assertIs(pkg.sub, sys.modules[
22232220
"test.test_lazy_import.data.submodule_name_shadowed.sub"
22242221
])
22252222

2226-
def test_import_of_parent_keeps_pending_submodule_in_all_mode(self):
2227-
"""Plain imports made lazy by 'all' mode behave the same way."""
2228-
sys.set_lazy_imports("all")
2229-
from test.test_lazy_import.data import (
2230-
plain_import_parent_after_submodule as mod)
2231-
pkg = mod.test.test_lazy_import.data.submodule_name_shadowed
2232-
self.assertIsInstance(pkg.sub, types.ModuleType)
2223+
def test_shadowed_submodule_then_parent_import(self):
2224+
"""A later `import pkg` should not lose a pending `import pkg.sub`."""
2225+
self.check_shadowed_submodule("import_parent_after_submodule")
2226+
2227+
def test_shadowed_submodule_then_sibling_import(self):
2228+
"""A later `import pkg.other` should not lose it either."""
2229+
self.check_shadowed_submodule("import_sibling_after_submodule")
2230+
2231+
def test_shadowed_submodule_of_loaded_parent(self):
2232+
"""A pending submodule binds on a package that is already loaded."""
2233+
self.check_shadowed_submodule("lazy_submodule_of_loaded_parent")
22332234

22342235

22352236
class DeletedModuleReimportTests(unittest.TestCase):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lazy import test.test_lazy_import.data.submodule_name_shadowed.sub
2+
lazy import test.test_lazy_import.data.submodule_name_shadowed.other
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import test.test_lazy_import.data.submodule_name_shadowed
2+
lazy import test.test_lazy_import.data.submodule_name_shadowed.sub

‎Lib/test/test_lazy_import/data/plain_import_parent_after_submodule.py‎

Lines changed: 0 additions & 2 deletions
This file was deleted.

‎Lib/test/test_lazy_import/data/submodule_name_shadowed/other.py‎

Whitespace-only changes.
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Fix lazy imports dropping a pending submodule import when the package is
2-
imported again under its own name. ``import pkg.sub`` followed by
3-
``import pkg`` now still imports ``pkg.sub``, so an attribute of ``pkg``
4-
that shares the submodule's name no longer keeps it from being imported.
1+
Fix a lazy ``import pkg.sub`` never importing the submodule when
2+
``pkg/__init__.py`` binds its own attribute of that name. The attribute
3+
answered the read that was meant to trigger the import, where an eager
4+
``import pkg.sub`` replaces it with the submodule.

‎Python/ceval.c‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3360,6 +3360,10 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
33603360
if (from == NULL) {
33613361
return NULL;
33623362
}
3363+
if (_PyImport_UnmarkPendingSubmodule(tstate, from, name) < 0) {
3364+
Py_DECREF(from);
3365+
return NULL;
3366+
}
33633367
ret = _PyLazyImport_New(frame, d->lz_builtins, from, name);
33643368
Py_DECREF(from);
33653369
return ret;
@@ -3374,6 +3378,16 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
33743378
if (from == NULL) {
33753379
return NULL;
33763380
}
3381+
// Only the last component of the dotted name is in doubt; the
3382+
// ones above it must be packages for it to be reached at all.
3383+
int last = PyUnicode_GET_LENGTH(d->lz_from)
3384+
== dot + 1 + PyUnicode_GET_LENGTH(name);
3385+
if (last &&
3386+
_PyImport_UnmarkPendingSubmodule(tstate, from, name) < 0)
3387+
{
3388+
Py_DECREF(from);
3389+
return NULL;
3390+
}
33773391
ret = _PyLazyImport_New(frame, d->lz_builtins, from, name);
33783392
Py_DECREF(from);
33793393
return ret;

0 commit comments

Comments
 (0)