https://github.com/python/cpython/commit/6847f4bc60f984a8d8e619de4b824d1b1da9524a
commit: 6847f4bc60f984a8d8e619de4b824d1b1da9524a
branch: 3.15
author: Dino Viehland <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-05-19T21:23:30Z
summary:
[3.15] gh-150052: Resolve un-loaded lazily loaded submodules via
module.__getattr__ instead of publishing lazy values (#150055)
files:
M Include/internal/pycore_import.h
M Lib/test/test_lazy_import/__init__.py
M Objects/moduleobject.c
M Python/import.c
diff --git a/Include/internal/pycore_import.h b/Include/internal/pycore_import.h
index 32ed3a62b2b4a7..a1078828afa572 100644
--- a/Include/internal/pycore_import.h
+++ b/Include/internal/pycore_import.h
@@ -39,6 +39,8 @@ extern PyObject * _PyImport_GetAbsName(
// Symbol is exported for the JIT on Windows builds.
PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate(
PyThreadState *tstate, PyObject *lazy_import);
+extern PyObject * _PyImport_TryLoadLazySubmodule(
+ PyObject *mod_name, PyObject *attr_name);
extern PyObject * _PyImport_LazyImportModuleLevelObject(
PyThreadState *tstate, PyObject *name, PyObject *builtins,
PyObject *globals, PyObject *locals, PyObject *fromlist, int level);
diff --git a/Lib/test/test_lazy_import/__init__.py
b/Lib/test/test_lazy_import/__init__.py
index 366cb203f8f256..9f2cc92bcfcc78 100644
--- a/Lib/test/test_lazy_import/__init__.py
+++ b/Lib/test/test_lazy_import/__init__.py
@@ -450,6 +450,14 @@ def test_lazy_import_pkg(self):
self.assertIn("test.test_lazy_import.data.pkg.bar", sys.modules)
self.assertIn("BAR_MODULE_LOADED", out.getvalue())
+ def test_lazy_submodule_stored_in_parent_dict(self):
+ """Accessing a lazy submodule should store it in the parent's
__dict__."""
+ import test.test_lazy_import.data.lazy_import_pkg
+
+ pkg = sys.modules["test.test_lazy_import.data.pkg"]
+ self.assertIn("bar", pkg.__dict__)
+ self.assertIs(pkg.__dict__["bar"],
sys.modules["test.test_lazy_import.data.pkg.bar"])
+
def test_lazy_import_pkg_cross_import(self):
"""Cross-imports within package should preserve lazy imports."""
import test.test_lazy_import.data.pkg.c
@@ -462,6 +470,18 @@ def test_lazy_import_pkg_cross_import(self):
self.assertEqual(type(g["x"]), int)
self.assertEqual(type(g["b"]), types.LazyImportType)
+ @support.requires_subprocess()
+ def test_lazy_from_import_does_not_pollute_parent(self):
+ """Lazy from import should not add the name to the parent module's
dict."""
+ code = textwrap.dedent("""
+ lazy from json import nonexistent_attr
+ import json
+ assert "nonexistent_attr" not in json.__dict__, (
+ "lazy from import should not publish attributes on the parent
module"
+ )
+ """)
+ assert_python_ok("-c", code)
+
@support.requires_subprocess()
def test_package_from_import_with_module_getattr(self):
"""Lazy from import should respect a package's __getattr__."""
@@ -613,19 +633,14 @@ def tearDown(self):
sys.set_lazy_imports("normal")
def test_import_error_shows_chained_traceback(self):
- """ImportError during reification should chain to show both definition
and access."""
- # Errors at reification must show where the lazy import was defined
- # AND where the access happened, per PEP 810 "Reification" section
+ """Accessing a nonexistent lazy submodule via parent attr raises
AttributeError."""
code = textwrap.dedent("""
import sys
lazy import test.test_lazy_import.data.nonexistent_module
try:
x = test.test_lazy_import.data.nonexistent_module
- except ImportError as e:
- # Should have __cause__ showing the original error
- # The exception chain shows both where import was defined and
where access happened
- assert e.__cause__ is not None, "Expected chained exception"
+ except AttributeError as e:
print("OK")
""")
result = subprocess.run(
@@ -673,7 +688,7 @@ def test_reification_retries_on_failure(self):
# First access - should fail
try:
x = test.test_lazy_import.data.broken_module
- except ValueError:
+ except AttributeError:
pass
# The lazy object should still be a lazy proxy (not reified)
@@ -683,7 +698,7 @@ def test_reification_retries_on_failure(self):
# Second access - should also fail (retry the import)
try:
x = test.test_lazy_import.data.broken_module
- except ValueError:
+ except AttributeError:
print("OK - retry worked")
""")
result = subprocess.run(
@@ -696,7 +711,6 @@ def test_reification_retries_on_failure(self):
def test_error_during_module_execution_propagates(self):
"""Errors in module code during reification should propagate
correctly."""
- # Module that raises during import should propagate with chaining
code = textwrap.dedent("""
import sys
lazy import test.test_lazy_import.data.broken_module
@@ -704,12 +718,8 @@ def test_error_during_module_execution_propagates(self):
try:
_ = test.test_lazy_import.data.broken_module
print("FAIL - should have raised")
- except ValueError as e:
- # The ValueError from the module should be the cause
- if "always fails" in str(e) or (e.__cause__ and "always fails"
in str(e.__cause__)):
- print("OK")
- else:
- print(f"FAIL - wrong error: {e}")
+ except AttributeError:
+ print("OK")
""")
result = subprocess.run(
[sys.executable, "-c", code],
diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c
index f7b83c1d111cde..f447403ef31b43 100644
--- a/Objects/moduleobject.c
+++ b/Objects/moduleobject.c
@@ -1299,6 +1299,33 @@ _PyModule_IsPossiblyShadowing(PyObject *origin)
return result;
}
+// Check if `name` is a lazily pending submodule of module `m`.
+// Returns a new reference on success, or NULL with no error set.
+static PyObject *
+try_load_lazy_submodule(PyModuleObject *m, PyObject *name)
+{
+ PyObject *mod_name;
+ int rc = PyDict_GetItemRef(m->md_dict, &_Py_ID(__name__), &mod_name);
+ if (rc <= 0) {
+ return NULL;
+ }
+ if (!PyUnicode_Check(mod_name)) {
+ Py_DECREF(mod_name);
+ return NULL;
+ }
+ PyObject *result = _PyImport_TryLoadLazySubmodule(mod_name, name);
+ Py_DECREF(mod_name);
+ if (result == NULL) {
+ PyErr_Clear();
+ return NULL;
+ }
+ if (PyDict_SetItem(m->md_dict, name, result) < 0) {
+ Py_DECREF(result);
+ return NULL;
+ }
+ return result;
+}
+
PyObject*
_Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
{
@@ -1363,6 +1390,13 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject
*name, int suppress)
PyErr_Clear();
}
assert(m->md_dict != NULL);
+ attr = try_load_lazy_submodule(m, name);
+ if (attr != NULL) {
+ return attr;
+ }
+ if (PyErr_Occurred()) {
+ return NULL;
+ }
if (PyDict_GetItemRef(m->md_dict, &_Py_ID(__getattr__), &getattr) < 0) {
return NULL;
}
diff --git a/Python/import.c b/Python/import.c
index c5cc7b52922d5b..ef6f5274a23665 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -4332,16 +4332,6 @@ PyImport_ImportModuleLevelObject(PyObject *name,
PyObject *globals,
return final_mod;
}
-static PyObject *
-get_mod_dict(PyObject *module)
-{
- if (PyModule_Check(module)) {
- return Py_NewRef(_PyModule_GetDict(module));
- }
-
- return PyObject_GetAttr(module, &_Py_ID(__dict__));
-}
-
// ensure we have the set for the parent module name in sys.lazy_modules.
// Returns a new reference.
static PyObject *
@@ -4364,18 +4354,16 @@ ensure_lazy_pending_submodules(PyDictObject
*lazy_modules, PyObject *parent)
return lazy_submodules;
}
-// Ensures that we have a LazyImportObject on the parent module for
-// all children modules which have been lazily imported. If the parent
-// module overrides the child attribute then the value is not replaced.
+// Records all parent-child relationships in lazy_pending_submodules
+// for a lazily imported module name. When a parent module's attribute
+// is accessed, _Py_module_getattro_impl will check lazy_pending_submodules
+// and trigger the import.
static int
-register_lazy_on_parent(PyThreadState *tstate, PyObject *name,
- PyObject *builtins)
+register_lazy_on_parent(PyThreadState *tstate, PyObject *name)
{
int ret = -1;
PyObject *parent = NULL;
PyObject *child = NULL;
- PyObject *parent_module = NULL;
- PyObject *parent_dict = NULL;
PyInterpreterState *interp = tstate->interp;
PyObject *lazy_pending_submodules = LAZY_PENDING_SUBMODULES(interp);
@@ -4396,9 +4384,6 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject
*name,
goto done;
}
parent = PyUnicode_Substring(name, 0, dot);
- // If `parent` is NULL then this has hit the end of the import, no
- // more "parent.child" in the import name. The entire import will be
- // resolved lazily.
if (parent == NULL) {
goto done;
}
@@ -4408,7 +4393,6 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject
*name,
goto done;
}
- // Record the child as being lazily imported from the parent.
PyObject *lazy_submodules = ensure_lazy_pending_submodules(
(PyDictObject *)lazy_pending_submodules, parent);
if (lazy_submodules == NULL) {
@@ -4421,44 +4405,11 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject
*name,
}
Py_DECREF(lazy_submodules);
- // Add the lazy import for the child to the parent.
- Py_XSETREF(parent_module, PyImport_GetModule(parent));
- if (parent_module != NULL) {
- Py_XSETREF(parent_dict, get_mod_dict(parent_module));
- if (parent_dict == NULL) {
- goto done;
- }
- if (PyDict_CheckExact(parent_dict)) {
- int contains = PyDict_Contains(parent_dict, child);
- if (contains < 0) {
- goto done;
- }
- if (!contains) {
- PyObject *lazy_module_attr = _PyLazyImport_New(
- tstate->current_frame, builtins, parent, child
- );
- if (lazy_module_attr == NULL) {
- goto done;
- }
- if (PyDict_SetItem(parent_dict, child,
- lazy_module_attr) < 0) {
- Py_DECREF(lazy_module_attr);
- goto done;
- }
- Py_DECREF(lazy_module_attr);
- }
- }
- ret = 0;
- goto done;
- }
-
Py_SETREF(name, parent);
parent = NULL;
}
done:
- Py_XDECREF(parent_dict);
- Py_XDECREF(parent_module);
Py_XDECREF(child);
Py_XDECREF(parent);
Py_XDECREF(name);
@@ -4467,7 +4418,7 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject
*name,
static int
register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name,
- PyObject *from, PyObject *builtins)
+ PyObject *from)
{
PyObject *fromname = PyUnicode_FromFormat("%U.%U", abs_name, from);
if (fromname == NULL) {
@@ -4481,11 +4432,59 @@ register_from_lazy_on_parent(PyThreadState *tstate,
PyObject *abs_name,
return -1;
}
- int res = register_lazy_on_parent(tstate, fromname, builtins);
+ int res = register_lazy_on_parent(tstate, fromname);
Py_DECREF(fromname);
return res;
}
+PyObject *
+_PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name)
+{
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+ PyObject *lazy_pending = LAZY_PENDING_SUBMODULES(interp);
+ if (lazy_pending == NULL) {
+ return NULL;
+ }
+
+ PyObject *pending_set;
+ int rc = PyDict_GetItemRef(lazy_pending, mod_name, &pending_set);
+ if (rc <= 0) {
+ return NULL;
+ }
+
+ int contains = PySet_Contains(pending_set, attr_name);
+ if (contains <= 0) {
+ Py_DECREF(pending_set);
+ return NULL;
+ }
+
+ PyObject *full_name = PyUnicode_FromFormat("%U.%U", mod_name, attr_name);
+ if (full_name == NULL) {
+ Py_DECREF(pending_set);
+ return NULL;
+ }
+
+ PyObject *mod = PyImport_ImportModuleLevelObject(
+ full_name, NULL, NULL, NULL, 0);
+ if (mod == NULL) {
+ Py_DECREF(pending_set);
+ Py_DECREF(full_name);
+ return NULL;
+ }
+ Py_DECREF(mod);
+
+ if (PySet_Discard(pending_set, attr_name) < 0) {
+ Py_DECREF(pending_set);
+ Py_DECREF(full_name);
+ return NULL;
+ }
+ Py_DECREF(pending_set);
+
+ PyObject *submod = PyImport_GetModule(full_name);
+ Py_DECREF(full_name);
+ return submod;
+}
+
PyObject *
_PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
PyObject *name, PyObject *builtins,
@@ -4580,8 +4579,7 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState
*tstate,
}
if (fromlist && PyUnicode_Check(fromlist)) {
- if (register_from_lazy_on_parent(tstate, abs_name, fromlist,
- builtins) < 0) {
+ if (register_from_lazy_on_parent(tstate, abs_name, fromlist) < 0) {
goto error;
}
}
@@ -4589,14 +4587,13 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState
*tstate,
PyTuple_GET_SIZE(fromlist)) {
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(fromlist); i++) {
if (register_from_lazy_on_parent(tstate, abs_name,
- PyTuple_GET_ITEM(fromlist, i),
- builtins) < 0)
+ PyTuple_GET_ITEM(fromlist, i)) <
0)
{
goto error;
}
}
}
- else if (register_lazy_on_parent(tstate, abs_name, builtins) < 0) {
+ else if (register_lazy_on_parent(tstate, abs_name) < 0) {
goto error;
}
@@ -5605,46 +5602,6 @@ _imp_source_hash_impl(PyObject *module, long key,
Py_buffer *source)
return PyBytes_FromStringAndSize(hash.data, sizeof(hash.data));
}
-static int
-publish_lazy_imports_on_module(PyThreadState *tstate,
- PyObject *lazy_submodules,
- PyObject *name,
- PyObject *module_dict)
-{
- PyObject *builtins = _PyEval_GetBuiltins(tstate);
- PyObject *attr_name;
- Py_ssize_t pos = 0;
- Py_hash_t hash;
-
- // Enumerate the set of lazy submodules which have been imported from the
- // parent module.
- while (_PySet_NextEntryRef(lazy_submodules, &pos, &attr_name, &hash)) {
- if (_PyDict_Contains_KnownHash(module_dict, attr_name, hash)) {
- Py_DECREF(attr_name);
- continue;
- }
- // Create a new lazy module attr for the subpackage which was
- // previously lazily imported.
- PyObject *lazy_module_attr = _PyLazyImport_New(tstate->current_frame,
builtins,
- name, attr_name);
- if (lazy_module_attr == NULL) {
- Py_DECREF(attr_name);
- return -1;
- }
-
- // Publish on the module that was just imported.
- if (PyDict_SetItem(module_dict, attr_name,
- lazy_module_attr) < 0) {
- Py_DECREF(lazy_module_attr);
- Py_DECREF(attr_name);
- return -1;
- }
- Py_DECREF(lazy_module_attr);
- Py_DECREF(attr_name);
- }
- return 0;
-}
-
/*[clinic input]
_imp._set_lazy_attributes
modobj: object
@@ -5658,44 +5615,11 @@ _imp__set_lazy_attributes_impl(PyObject *module,
PyObject *modobj,
PyObject *name)
/*[clinic end generated code: output=3369bb3242b1f043 input=38ea6f30956dd7d6]*/
{
- PyThreadState *tstate = _PyThreadState_GET();
- PyObject *module_dict = NULL;
- PyObject *ret = NULL;
- PyObject *lazy_pending_modules = LAZY_PENDING_SUBMODULES(tstate->interp);
- assert(lazy_pending_modules != NULL);
-
- PyObject *lazy_submodules;
- if (PySet_Discard(LAZY_MODULES(tstate->interp), name) < 0) {
- return NULL;
- } else if (PyDict_GetItemRef(lazy_pending_modules, name, &lazy_submodules)
< 0) {
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+ if (PySet_Discard(LAZY_MODULES(interp), name) < 0) {
return NULL;
}
- else if (lazy_submodules == NULL) {
- Py_RETURN_NONE;
- }
-
- module_dict = get_mod_dict(modobj);
- if (module_dict == NULL || !PyDict_CheckExact(module_dict)) {
- Py_DECREF(lazy_submodules);
- goto done;
- }
-
- assert(PyAnySet_CheckExact(lazy_submodules));
- Py_BEGIN_CRITICAL_SECTION(lazy_submodules);
- publish_lazy_imports_on_module(tstate, lazy_submodules, name, module_dict);
- Py_END_CRITICAL_SECTION();
- Py_DECREF(lazy_submodules);
-
- if (PyDict_DelItem(lazy_pending_modules, name) < 0) {
- goto error;
- }
-
-done:
- ret = Py_NewRef(Py_None);
-
-error:
- Py_XDECREF(module_dict);
- return ret;
+ Py_RETURN_NONE;
}
PyDoc_STRVAR(doc_imp,
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]