]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-150052: Resolve un-loaded lazily loaded submodules via `module.__getattr__` instea...
authorBartosz Sławecki <bartosz@ilikepython.com>
Tue, 2 Jun 2026 08:58:51 +0000 (10:58 +0200)
committerGitHub <noreply@github.com>
Tue, 2 Jun 2026 08:58:51 +0000 (09:58 +0100)
Include/internal/pycore_import.h
Lib/test/test_lazy_import/__init__.py
Objects/moduleobject.c
Python/import.c

index 32ed3a62b2b4a7ce3bd097699e874a7c3d82fba5..a1078828afa572ef1329c759da272df1f73f5362 100644 (file)
@@ -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);
index 787caff4f8537bb436d0bb304d73e562f4c154bd..321733a4fdf17052f33e12a3355b90313f3efaa2 100644 (file)
@@ -503,6 +503,14 @@ class PackageTests(unittest.TestCase):
         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
@@ -515,6 +523,18 @@ class PackageTests(unittest.TestCase):
         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_raising(self):
         """Lazy from import should respect a package's __getattr__."""
@@ -716,19 +736,14 @@ class ErrorHandlingTests(unittest.TestCase):
         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(
@@ -776,7 +791,7 @@ class ErrorHandlingTests(unittest.TestCase):
             # 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)
@@ -786,7 +801,7 @@ class ErrorHandlingTests(unittest.TestCase):
             # 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(
@@ -799,7 +814,6 @@ class ErrorHandlingTests(unittest.TestCase):
 
     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
@@ -807,12 +821,8 @@ class ErrorHandlingTests(unittest.TestCase):
             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],
index f7b83c1d111cded819d3dc1d2291bed592357b12..f447403ef31b43a20ba3fa49cd802bdd2426125f 100644 (file)
@@ -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;
     }
index 2a8f7bddb9898672f349835c9dc82a4adc0239cc..82d15ad0683c190d32aba5e9410c1349751a2308 100644 (file)
@@ -4337,16 +4337,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 *
@@ -4369,18 +4359,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);
@@ -4401,9 +4389,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;
         }
@@ -4413,7 +4398,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) {
@@ -4426,44 +4410,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);
@@ -4472,7 +4423,7 @@ done:
 
 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) {
@@ -4486,11 +4437,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,
@@ -4585,8 +4584,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;
         }
     }
@@ -4594,14 +4592,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;
     }
 
@@ -5612,46 +5609,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
@@ -5665,44 +5622,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,