https://github.com/python/cpython/commit/03bb26f89e81b9232fd3b3396ea8a6b7615f7e6e
commit: 03bb26f89e81b9232fd3b3396ea8a6b7615f7e6e
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: hugovk <[email protected]>
date: 2026-10-02T18:48:03Z
summary:

[3.15] gh-158140: Resolve lazily imported sibling submodules independently 
(GH-158521) (#158613)

Co-authored-by: Pablo Galindo Salgado <[email protected]>
Co-authored-by: Petr Viktorin <[email protected]>
Co-authored-by: T. Wouters <[email protected]>
Co-authored-by: Hugo van Kemenade <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-10-01-20-00-00.gh-issue-158140.small.rst
M Include/internal/pycore_import.h
M Include/internal/pycore_interp_structs.h
M Include/internal/pycore_lazyimportobject.h
M Lib/test/test_lazy_import/__init__.py
M Objects/lazyimportobject.c
M Objects/moduleobject.c
M Python/import.c

diff --git a/Include/internal/pycore_import.h b/Include/internal/pycore_import.h
index 983ee0ad22789b..f64b65e0f890df 100644
--- a/Include/internal/pycore_import.h
+++ b/Include/internal/pycore_import.h
@@ -41,7 +41,9 @@ PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate(
 // new reference, NULL without an exception if absent, or NULL on error.
 // Set recheck_dict if a concurrent lookup may have already bound the child.
 extern PyObject * _PyImport_TryLoadLazySubmodule(
-    PyObject *module, PyObject *attr_name, int *recheck_dict);
+    PyObject *module, PyObject *attr_name, int suppress, int *recheck_dict);
+extern int _PyImport_ClearLazySubmodule(
+    PyThreadState *tstate, PyObject *name, int bind);
 extern PyObject * _PyImport_LazyImportModuleLevelObject(
     PyThreadState *tstate, PyObject *name, PyObject *builtins,
     PyObject *globals, PyObject *locals, PyObject *fromlist, int level);
diff --git a/Include/internal/pycore_interp_structs.h 
b/Include/internal/pycore_interp_structs.h
index 72270b9d12a02c..58a15eb87d2aad 100644
--- a/Include/internal/pycore_interp_structs.h
+++ b/Include/internal/pycore_interp_structs.h
@@ -352,9 +352,11 @@ struct _import_state {
     // lazily imported. This value is only for debugging/introspection
     // purposes and is not used by the runtime.
     PyObject *lazy_modules;
-    // A dict mapping package names to a set of submodule names that
-    // may need to be imported when an attribute is missing on the package.
+    // Package names map to pending children: declarations for plain imports,
+    // or None for from-import names that may be ordinary attributes.
     PyObject *lazy_pending_submodules;
+    // Avoid pending-child work for ordinary cached imports.
+    int has_lazy_submodules;
 #ifdef Py_GIL_DISABLED
     PyMutex lazy_mutex;
 #endif
diff --git a/Include/internal/pycore_lazyimportobject.h 
b/Include/internal/pycore_lazyimportobject.h
index c3f8417c089316..2da037b0b6fb84 100644
--- a/Include/internal/pycore_lazyimportobject.h
+++ b/Include/internal/pycore_lazyimportobject.h
@@ -18,6 +18,10 @@ PyAPI_FUNC(PyObject *) _PyLazyImport_New(
     struct _PyInterpreterFrame *frame, PyObject *builtins,
     PyObject *name, PyObject *fromlist);
 
+extern PyObject *_PyLazyImport_LoadChild(
+    PyThreadState *tstate, PyObject *declaration, PyObject *name);
+extern int _PyLazyImport_IsActive(PyObject *declaration);
+
 extern int _PyLazyImport_IsResolving(PyThreadState *tstate, PyObject *op);
 
 // Resolve a placeholder and replace its binding if it is unchanged or holds
diff --git a/Lib/test/test_lazy_import/__init__.py 
b/Lib/test/test_lazy_import/__init__.py
index dcee906c630c74..4c116ca0c0a5c1 100644
--- a/Lib/test/test_lazy_import/__init__.py
+++ b/Lib/test/test_lazy_import/__init__.py
@@ -457,6 +457,121 @@ def test_lazy_with_from(self):
         self.assertNotIn("test.test_lazy_import.data.basic2", sys.modules)
 
 
[email protected]_subprocess()
+class IndependentSubmoduleTests(LazyImportTestCase):
+    def check(self, code):
+        assert_python_ok('-c', textwrap.dedent(code))
+
+    def test_siblings(self):
+        for package, children in (
+            ('test.test_lazy_import.data.pkg', ('b', 'bar')),
+            ('test.test_lazy_import.data.metasyntactic.foo', ('ack', 'bar')),
+        ):
+            for first, second in (children, children[::-1]):
+                with self.subTest(package=package, first=first):
+                    self.check(f"""
+                        import sys
+                        lazy import {package}.{first}
+                        lazy import {package}.{second}
+                        assert {package}.{first} is 
sys.modules['{package}.{first}']
+                        assert '{package}.{second}' not in sys.modules
+                        assert {package}.{second} is 
sys.modules['{package}.{second}']
+                    """)
+
+    def test_star_import(self):
+        self.check("""
+            lazy import urllib.nonexistent
+            lazy import urllib
+            assert urllib.__name__ == 'urllib'
+            from urllib import *
+        """)
+
+    def test_deleted_child(self):
+        self.check("""
+            lazy from xml import dom
+            assert dom.__name__ == 'xml.dom'
+            import xml
+            del xml.dom
+            assert not hasattr(xml, 'dom')
+            lazy from xml import dom
+            assert hasattr(xml, 'dom')
+        """)
+
+    def test_cached_import_releases_builtins(self):
+        for statement in ('lazy from xml.dom import Node',
+                          'lazy import xml.dom.minidom'):
+            with self.subTest(statement=statement):
+                self.check(f"""
+                    import builtins, gc, weakref
+                    import xml.dom.minidom
+                    class Payload:
+                        pass
+                    payload = Payload()
+                    reference = weakref.ref(payload)
+                    namespace = {{'__builtins__': dict(vars(builtins), 
payload=payload)}}
+                    exec({statement!r}, namespace)
+                    exec('Node' if 'from' in {statement!r} else 
'xml.dom.minidom', namespace)
+                    del namespace, payload
+                    gc.collect()
+                    assert reference() is None
+                """)
+
+    def test_invalid_descendant_retries(self):
+        self.check("""
+            lazy import test.test_lazy_import.data.pkg.b.foo
+            for _ in range(2):
+                try:
+                    test.test_lazy_import.data.pkg.b.foo
+                except ModuleNotFoundError:
+                    pass
+                else:
+                    raise AssertionError('imported an attribute as a module')
+            import test.test_lazy_import.data.pkg.b
+            assert callable(test.test_lazy_import.data.pkg.b.foo)
+        """)
+
+    def test_namespace_import_hooks(self):
+        self.check("""
+            import builtins
+            first = {'__builtins__': vars(builtins).copy()}
+            second = {'__builtins__': vars(builtins).copy()}
+            exec('lazy import xml.dom', first)
+            exec('lazy import xml.dom', second)
+            def denied(*args):
+                raise AssertionError('used another namespace import hook')
+            second['__builtins__']['__import__'] = denied
+            exec('assert xml.dom.__name__ == "xml.dom"', first)
+        """)
+
+    def test_retry_after_recursive_access(self):
+        self.check("""
+            import sys, xml
+            from importlib.machinery import ModuleSpec
+            class Loader:
+                calls = 0
+                def find_spec(self, name, path=None, target=None):
+                    if name == 'xml.broken':
+                        return ModuleSpec(name, self)
+                def create_module(self, spec):
+                    return None
+                def exec_module(self, module):
+                    self.calls += 1
+                    xml.broken
+                    raise RuntimeError('failed initialization')
+            loader = Loader()
+            sys.meta_path.insert(0, loader)
+            lazy import xml.broken
+            for _ in range(2):
+                try:
+                    xml.broken
+                except RuntimeError as exc:
+                    assert str(exc) == 'failed initialization'
+                else:
+                    raise AssertionError('returned a failed partial module')
+            assert loader.calls == 2
+        """)
+
+
 class PackageTests(LazyImportTestCase):
     """Tests for lazy imports with packages."""
 
@@ -794,7 +909,7 @@ def test_chained_exception_import_shows_notes(self, name):
             lazy import {name}
 
             try:
-                _ = test
+                _ = {name}
             except ValueError as e:
                 assert any(
                     note.startswith("lazy import of '{name}' declared in ")
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-01-20-00-00.gh-issue-158140.small.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-01-20-00-00.gh-issue-158140.small.rst
new file mode 100644
index 00000000000000..b9a0fc5e7a80c7
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-01-20-00-00.gh-issue-158140.small.rst
@@ -0,0 +1,2 @@
+Resolve lazily imported sibling submodules independently, so accessing one
+does not import another merely because its declaration came last.
diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c
index 69b3e1663d0786..bb35ddcdae1755 100644
--- a/Objects/lazyimportobject.c
+++ b/Objects/lazyimportobject.c
@@ -9,6 +9,7 @@
 #include "pycore_lazyimportobject.h"
 #include "pycore_long.h"
 #include "pycore_moduleobject.h"
+#include "pycore_pyatomic_ft_wrappers.h"
 #include "pycore_pyerrors.h"
 #include "pycore_traceback.h"
 #include "pycore_tstate.h"
@@ -25,6 +26,8 @@ typedef struct {
     // Declaration location.
     PyCodeObject *lz_code;
     int lz_instr_offset;
+    // The root was accessed, so its declared path requires import semantics.
+    int lz_active;
 } PyLazyImportObject;
 
 #define PyLazyImportObject_CAST(op) ((PyLazyImportObject *)(op))
@@ -70,6 +73,7 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject 
*builtins,
 
     m->lz_code = NULL;
     m->lz_instr_offset = -1;
+    m->lz_active = 0;
 
     if (frame != NULL) {
         m->lz_code = (PyCodeObject *)Py_NewRef(_PyFrame_GetCode(frame));
@@ -429,10 +433,81 @@ lazy_import_resolve_impl(PyThreadState *tstate, PyObject 
*lazy_import,
         PyErr_SetString(PyExc_ImportError, "__import__ not found");
         goto done;
     }
+    PyObject *name = Py_NewRef(root->lz_from);
+    if (first == NULL &&
+        (fromlist == Py_None ||
+         (PyTuple_Check(fromlist) && PyTuple_GET_SIZE(fromlist) == 0)) &&
+        _PyImport_IsDefaultImportFunc(tstate->interp, import_func) &&
+        root->lz_builtins == tstate->interp->builtins) {
+        // A plain import binds the root package. Its pending children are
+        // imported separately when their attributes are accessed.
+        Py_ssize_t dot = PyUnicode_FindChar(
+            name, '.', 0, PyUnicode_GET_LENGTH(name), 1);
+        if (dot >= 0) {
+            // Preserve eager traversal through aliased or non-package parents.
+            int regular = 1;
+            int complete = 1;
+            Py_ssize_t end = PyUnicode_GET_LENGTH(name);
+            for (Py_ssize_t i = dot; i >= 0 && regular;
+                 i = PyUnicode_FindChar(name, '.', i + 1, end, 1)) {
+                PyObject *prefix = PyUnicode_Substring(name, 0, i);
+                PyObject *cached = prefix == NULL ? NULL :
+                    lazy_import_get_loaded_attr(tstate, prefix, 
&_Py_ID(__name__));
+                if (cached == NULL) {
+                    complete = 0;
+                    Py_XDECREF(prefix);
+                    break;
+                }
+                PyObject *path = lazy_import_get_loaded_attr(
+                    tstate, prefix, &_Py_ID(__path__));
+                regular = path != NULL && PyUnicode_Check(cached) &&
+                    PyUnicode_Compare(cached, prefix) == 0;
+                Py_XDECREF(path);
+                Py_DECREF(cached);
+                Py_DECREF(prefix);
+            }
+            PyObject *loaded = lazy_import_get_loaded_attr(
+                tstate, name, &_Py_ID(__name__));
+            if (regular && (!complete || loaded == NULL) && !PyErr_Occurred()) 
{
+                Py_SETREF(name, PyUnicode_Substring(name, 0, dot));
+            }
+            Py_XDECREF(loaded);
+            if (PyErr_Occurred()) {
+                Py_CLEAR(name);
+            }
+        }
+        else if (dot == -2) {
+            Py_CLEAR(name);
+        }
+        if (name == NULL) {
+            goto done;
+        }
+    }
     obj = _PyEval_ImportNameWithImport(
         tstate, import_func, globals, globals,
-        root->lz_from, fromlist, _PyLong_GetZero()
+        name, fromlist, _PyLong_GetZero()
     );
+    if (obj != NULL && name != root->lz_from) {
+        FT_ATOMIC_STORE_INT_RELAXED(root->lz_active, 1);
+        
FT_ATOMIC_STORE_INT_RELAXED(tstate->interp->imports.has_lazy_submodules, 1);
+        int package = PyModule_CheckExact(obj) ?
+            PyDict_Contains(_PyModule_GetDict(obj), &_Py_ID(__path__)) : 0;
+        if (package < 0) {
+            Py_CLEAR(obj);
+        }
+        else if (!package) {
+            Py_DECREF(obj);
+            obj = _PyEval_ImportNameWithImport(
+                tstate, import_func, globals, globals,
+                root->lz_from, fromlist, _PyLong_GetZero());
+        }
+    }
+    if (obj != NULL && name == root->lz_from &&
+        _PyImport_IsDefaultImportFunc(tstate->interp, import_func) &&
+        _PyImport_ClearLazySubmodule(tstate, name, 0) < 0) {
+        Py_CLEAR(obj);
+    }
+    Py_DECREF(name);
     obj = lazy_import_resolve_result(tstate, obj);
     // The normal importer may publish this module on its parent. Custom
     // hooks retain control of their own assignments to the parent.
@@ -453,7 +528,7 @@ lazy_import_resolve_impl(PyThreadState *tstate, PyObject 
*lazy_import,
     }
     assert(obj == NULL || !PyLazyImport_CheckExact(obj));
     if (obj != NULL) {
-        PyObject *name = lazy_import_name(lz);
+        PyObject *name = lazy_import_path(lz);
         if (name == NULL ||
             _PyImport_DiscardLazyModule(tstate->interp, name) < 0) {
             Py_CLEAR(obj);
@@ -484,6 +559,58 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, 
PyObject *lazy_import)
     return lazy_import_resolve_impl(tstate, lazy_import, NULL);
 }
 
+// Resolve a pending child as an aliased import, preserving its declaration.
+PyObject *
+_PyLazyImport_LoadChild(PyThreadState *tstate, PyObject *declaration,
+                        PyObject *name)
+{
+    assert(PyLazyImport_CheckExact(declaration));
+    PyLazyImportObject *source = PyLazyImportObject_CAST(declaration);
+    PyObject *lz = _PyLazyImport_New(NULL, source->lz_builtins, name, NULL);
+    Py_ssize_t end = PyUnicode_GET_LENGTH(name);
+    Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0, end, 1);
+    if (dot == -2) {
+        Py_CLEAR(lz);
+    }
+    while (lz != NULL && dot >= 0) {
+        Py_ssize_t start = dot + 1;
+        dot = PyUnicode_FindChar(name, '.', start, end, 1);
+        if (dot == -2) {
+            Py_CLEAR(lz);
+            break;
+        }
+        PyObject *attr = PyUnicode_Substring(name, start, dot < 0 ? end : dot);
+        PyObject *next = attr == NULL ? NULL :
+            _PyLazyImport_New(NULL, NULL, lz, attr);
+        Py_XDECREF(attr);
+        Py_SETREF(lz, next);
+    }
+    if (lz == NULL) {
+        return NULL;
+    }
+    PyLazyImportObject *child = PyLazyImportObject_CAST(lz);
+    child->lz_code = (PyCodeObject *)Py_XNewRef(source->lz_code);
+    child->lz_instr_offset = source->lz_instr_offset;
+    PyObject *result = _PyImport_LoadLazyImportTstate(tstate, lz);
+    Py_DECREF(lz);
+    if (result != NULL && PyModule_Check(result) &&
+        PyUnicode_Compare(source->lz_from, name) != 0 &&
+        !PyDict_Contains(_PyModule_GetDict(result), &_Py_ID(__path__))) {
+        // Reaching a non-package must still reject an invalid dotted import.
+        Py_DECREF(result);
+        return _PyLazyImport_LoadChild(tstate, declaration, source->lz_from);
+    }
+    return result;
+}
+
+int
+_PyLazyImport_IsActive(PyObject *declaration)
+{
+    assert(PyLazyImport_CheckExact(declaration));
+    return FT_ATOMIC_LOAD_INT_RELAXED(
+        PyLazyImportObject_CAST(declaration)->lz_active);
+}
+
 // Loading pkg.child can replace a placeholder in pkg.child with the module
 // before a from-import retrieves the value that belongs in that binding.
 // This is an optimization that can be safely skipped.
diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c
index 57df195b7f3201..d13b513ab468e4 100644
--- a/Objects/moduleobject.c
+++ b/Objects/moduleobject.c
@@ -1338,7 +1338,8 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject 
*name, int suppress)
         }
         assert(m->md_dict != NULL);
         int recheck_dict;
-        attr = _PyImport_TryLoadLazySubmodule((PyObject *)m, name, 
&recheck_dict);
+        attr = _PyImport_TryLoadLazySubmodule((PyObject *)m, name, suppress,
+                                             &recheck_dict);
         if (attr != NULL || PyErr_Occurred()) {
             return attr;
         }
diff --git a/Python/import.c b/Python/import.c
index f2aeb18fc9edd7..062f7009ae1255 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -305,6 +305,7 @@ get_importtime_name(PyObject *name)
     return encoded;
 }
 
+// Return whether the cached module was still initializing.
 static int
 import_ensure_initialized(PyInterpreterState *interp, PyObject *mod, PyObject 
*name)
 {
@@ -350,7 +351,7 @@ import_ensure_initialized(PyInterpreterState *interp, 
PyObject *mod, PyObject *n
 #undef import_level
     }
 
-    return 0;
+    return rc;
 }
 
 static void remove_importlib_frames(PyThreadState *tstate);
@@ -4016,7 +4017,8 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject 
*globals,
     }
 
     if (mod != NULL && mod != Py_None) {
-        if (import_ensure_initialized(tstate->interp, mod, abs_name) < 0) {
+        int initializing = import_ensure_initialized(tstate->interp, mod, 
abs_name);
+        if (initializing < 0) {
             goto error;
         }
         /* Verify the module is still in sys.modules. Another thread may have
@@ -4039,6 +4041,11 @@ PyImport_ImportModuleLevelObject(PyObject *name, 
PyObject *globals,
         }
         else {
             Py_DECREF(mod_check);
+            if (!initializing &&
+                
FT_ATOMIC_LOAD_INT_RELAXED(interp->imports.has_lazy_submodules) &&
+                _PyImport_ClearLazySubmodule(tstate, abs_name, 1) < 0) {
+                goto error;
+            }
         }
     }
     else {
@@ -4172,7 +4179,7 @@ lazy_modules_add(PyThreadState *tstate, PyObject *name,
     return loaded ? 0 : PySet_Add(LAZY_MODULES(tstate->interp), name);
 }
 
-// Ensure a set of pending submodule names exists for the parent.
+// Ensure a dict of pending submodule names exists for the parent.
 // Returns a new reference.
 static PyObject *
 ensure_lazy_pending_submodules(PyDictObject *lazy_modules, PyObject *parent)
@@ -4183,7 +4190,7 @@ ensure_lazy_pending_submodules(PyDictObject 
*lazy_modules, PyObject *parent)
                                                   &lazy_submodules);
     if (err == 0) {
         // value isn't present
-        lazy_submodules = PySet_New(NULL);
+        lazy_submodules = PyDict_New();
         if (lazy_submodules != NULL &&
             _PyDict_SetItem_LockHeld(lazy_modules, parent,
                                      lazy_submodules) < 0) {
@@ -4199,7 +4206,7 @@ ensure_lazy_pending_submodules(PyDictObject 
*lazy_modules, PyObject *parent)
 // 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)
+register_lazy_on_parent(PyThreadState *tstate, PyObject *name, PyObject 
*source)
 {
     PyDictObject *pending =
         (PyDictObject *)LAZY_PENDING_SUBMODULES(tstate->interp);
@@ -4222,7 +4229,18 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject 
*name)
 
         PyObject *submodules = ensure_lazy_pending_submodules(pending, parent);
         Py_DECREF(parent);
-        int err = submodules == NULL ? -1 : PySet_Add(submodules, child);
+        PyObject *fullname = PyUnicode_Substring(name, 0, end);
+        PyObject *cached = fullname == NULL ? NULL :
+            import_get_module(tstate, fullname);
+        Py_XDECREF(fullname);
+        PyObject *value = cached != NULL && cached != Py_None ? Py_None : 
source;
+        int err = -1;
+        if (submodules != NULL && !PyErr_Occurred()) {
+            err = source == Py_None ?
+                PyDict_SetDefaultRef(submodules, child, value, NULL) :
+                PyDict_SetItem(submodules, child, value);
+        }
+        Py_XDECREF(cached);
         Py_DECREF(child);
         Py_XDECREF(submodules);
         if (err < 0) {
@@ -4253,15 +4271,92 @@ register_from_lazy_on_parent(PyThreadState *tstate, 
PyObject *existing_module,
 
     rc = lazy_modules_add(tstate, fromname, NULL);
     if (rc == 0) {
-        rc = register_lazy_on_parent(tstate, fromname);
+        rc = register_lazy_on_parent(tstate, fromname, Py_None);
     }
     Py_DECREF(fromname);
     return rc;
 }
 
+// Release completed declarations, retaining names while their parent may fail.
+// Cached eager imports also restore a child removed after failed validation.
+int
+_PyImport_ClearLazySubmodule(PyThreadState *tstate, PyObject *name, int bind)
+{
+    if (LAZY_PENDING_SUBMODULES(tstate->interp) == NULL ||
+        PyDict_GET_SIZE(LAZY_PENDING_SUBMODULES(tstate->interp)) == 0) {
+        return 0;
+    }
+    Py_ssize_t end = PyUnicode_GET_LENGTH(name);
+    Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0, end, -1);
+    if (dot < 0) {
+        return dot == -2 ? -1 : 0;
+    }
+    PyObject *parent = PyUnicode_Substring(name, 0, dot);
+    PyObject *child = PyUnicode_Substring(name, dot + 1, end);
+    PyObject *pending = NULL;
+    int rc = -1;
+    if (parent != NULL && child != NULL) {
+        rc = PyDict_GetItemRef(LAZY_PENDING_SUBMODULES(tstate->interp),
+                              parent, &pending);
+        if (rc > 0) {
+            rc = PyDict_Contains(pending, child);
+        }
+        if (rc > 0) {
+            PyObject *loaded = import_get_module(tstate, name), *loaded_spec = 
NULL;
+            int loading = loaded == NULL ? (PyErr_Occurred() ? -1 : 0) :
+                PyObject_GetOptionalAttr(loaded, &_Py_ID(__spec__), 
&loaded_spec);
+            if (loading > 0) {
+                loading = _PyModuleSpec_IsInitializing(loaded_spec);
+            }
+            Py_XDECREF(loaded_spec);
+            Py_XDECREF(loaded);
+            if (loading != 0) {
+                if (PyErr_ExceptionMatches(PyExc_Exception)) {
+                    PyErr_Clear();
+                    loading = 1;
+                }
+                rc = loading < 0 ? -1 : 0;
+                goto done;
+            }
+            PyObject *module = import_get_module(tstate, parent);
+            PyObject *spec = NULL;
+            int initializing = module == NULL ? (PyErr_Occurred() ? -1 : 0) :
+                PyObject_GetOptionalAttr(module, &_Py_ID(__spec__), &spec);
+            if (initializing > 0) {
+                initializing = _PyModuleSpec_IsInitializing(spec);
+            }
+            Py_XDECREF(spec);
+            if (bind && initializing == 0 && module != NULL &&
+                PyModule_CheckExact(module) && PyDict_Contains(pending, child) 
> 0) {
+                PyObject *value = import_get_module(tstate, name);
+                if (value != NULL && value != Py_None) {
+                    initializing = PyDict_SetDefaultRef(
+                        _PyModule_GetDict(module), child, value, NULL) < 0 ? 
-1 : 0;
+                }
+                Py_XDECREF(value);
+            }
+            Py_XDECREF(module);
+            rc = initializing < 0 ? -1 :
+                (initializing ? PyDict_SetItem(pending, child, Py_None) :
+                                PyDict_Pop(pending, child, NULL));
+        }
+    }
+done:
+    Py_XDECREF(pending);
+    Py_XDECREF(parent);
+    Py_XDECREF(child);
+    return rc < 0 ? -1 : 0;
+}
+
+static int
+lazy_submodule_matches(PyObject *value, void *expected)
+{
+    return value == expected;
+}
+
 PyObject *
 _PyImport_TryLoadLazySubmodule(PyObject *module, PyObject *attr_name,
-                              int *recheck_dict)
+                              int suppress, int *recheck_dict)
 {
     *recheck_dict = 0;
     PyObject *mod_dict = _PyModule_GetDict(module);
@@ -4269,7 +4364,7 @@ _PyImport_TryLoadLazySubmodule(PyObject *module, PyObject 
*attr_name,
     if (PyDict_GetItemRef(mod_dict, &_Py_ID(__name__), &mod_name) <= 0) {
         return NULL;
     }
-    PyObject *mod = NULL, *pending_set = NULL;
+    PyObject *mod = NULL, *pending_set = NULL, *source = NULL;
     if (!PyUnicode_Check(mod_name)) {
         goto done;
     }
@@ -4281,7 +4376,7 @@ _PyImport_TryLoadLazySubmodule(PyObject *module, PyObject 
*attr_name,
     }
 
     *recheck_dict = 1;
-    if (PySet_Contains(pending_set, attr_name) <= 0) {
+    if (PyDict_GetItemRef(pending_set, attr_name, &source) <= 0) {
         goto done;
     }
     PyObject *full_name = PyUnicode_FromFormat("%U.%U", mod_name, attr_name);
@@ -4289,24 +4384,53 @@ _PyImport_TryLoadLazySubmodule(PyObject *module, 
PyObject *attr_name,
         goto done;
     }
     // Match eager from-imports: accept a partial module in an import cycle.
-    mod = PyImport_GetModule(full_name);
-    if ((mod == NULL && !PyErr_Occurred()) || mod == Py_None) {
-        Py_XDECREF(mod);
-        mod = import_find_and_load_with_name(
-            tstate, full_name, &_Py_ID(_find_and_load_lazy_submodule), 
Py_None);
+    if (source != Py_None && !suppress &&
+        (_PyLazyImport_IsActive(source) ||
+         !PyDict_Contains(mod_dict, &_Py_ID(__getattr__)))) {
+        mod = _PyLazyImport_LoadChild(tstate, source, full_name);
+    }
+    else {
+        mod = PyImport_GetModule(full_name);
+        if ((mod == NULL && !PyErr_Occurred()) || mod == Py_None) {
+            Py_XDECREF(mod);
+            mod = import_find_and_load_with_name(
+                tstate, full_name, &_Py_ID(_find_and_load_lazy_submodule), 
Py_None);
+        }
     }
-    Py_DECREF(full_name);
     if (mod == NULL) {
+        PyObject *exc = PyErr_GetRaisedException();
+        PyObject *loaded = import_get_module(tstate, full_name);
+        if (loaded != NULL && loaded != Py_None) {
+            (void)_PyDict_DelItemIf(mod_dict, attr_name,
+                                  lazy_submodule_matches, loaded);
+            (void)PyDict_SetDefaultRef(pending_set, attr_name, source, NULL);
+        }
+        Py_XDECREF(loaded);
+        PyErr_SetRaisedException(exc);
         remove_importlib_frames(tstate);
     }
     /* Keep missing children pending: a finder may provide them later. */
-    else if (mod == Py_None ||
-             PyDict_SetItem(mod_dict, attr_name, mod) < 0 ||
-             PySet_Discard(pending_set, attr_name) < 0) {
-        Py_CLEAR(mod);
+    else {
+        PyObject *spec = NULL;
+        int rc = mod == Py_None ? 0 :
+            PyObject_GetOptionalAttr(mod, &_Py_ID(__spec__), &spec);
+        int initializing = rc > 0 ? _PyModuleSpec_IsInitializing(spec) : rc;
+        Py_XDECREF(spec);
+        if (initializing < 0 && PyErr_ExceptionMatches(PyExc_Exception)) {
+            PyErr_Clear();
+            initializing = 0;
+        }
+        if (mod == Py_None || initializing < 0 ||
+            (initializing == 0 &&
+             (PyDict_SetItem(mod_dict, attr_name, mod) < 0 ||
+              PyDict_Pop(pending_set, attr_name, NULL) < 0))) {
+            Py_CLEAR(mod);
+        }
     }
+    Py_DECREF(full_name);
 
 done:
+    Py_XDECREF(source);
     Py_XDECREF(pending_set);
     Py_DECREF(mod_name);
     return mod;
@@ -4422,7 +4546,7 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState 
*tstate,
             }
         }
     }
-    else if (register_lazy_on_parent(tstate, abs_name) < 0) {
+    else if (register_lazy_on_parent(tstate, abs_name, res) < 0) {
         goto error;
     }
 
@@ -5451,6 +5575,9 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject 
*modobj,
     if (PySet_Discard(LAZY_MODULES(interp), name) < 0) {
         return NULL;
     }
+    if (_PyImport_ClearLazySubmodule(_PyThreadState_GET(), name, 0) < 0) {
+        return NULL;
+    }
     Py_RETURN_NONE;
 }
 

_______________________________________________
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