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]

Reply via email to