From aab59a0d04836f7d7d0c3175438d3f7aa323f46e Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Wed, 16 Sep 2026 14:47:32 -0700 Subject: [PATCH 1/5] Remove resolved names from sys.lazy_modules consistently Names only left sys.lazy_modules through _imp._set_lazy_attributes(), which the import machinery calls from _find_and_load_unlocked(). Two cases never reached it, so their names were recorded and then kept forever: - A lazy import of a module already in sys.modules. _find_and_load() returns early, so nothing ever discards the name. Do not record it in the first place. - The "pkg.attr" entry for `lazy from pkg import attr`. The import machinery only discards module names, and attr is often not a module. Discard it when the lazy object is reified, where the name is already known and has been resolved either way. Submodules that are not yet loaded are still tracked: loading a package does not load its submodules, so those imports can still fire. Names whose reification failed also stay tracked, since the import can still happen. --- Lib/test/test_lazy_import/__init__.py | 100 +++++++++++++++++- ...-09-16-15-02-11.gh-issue-155695.Kf3xQa.rst | 3 + Python/import.c | 47 ++++++-- 3 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-16-15-02-11.gh-issue-155695.Kf3xQa.rst diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 5026c9670d81d23..20a9e2ad78f3b1a 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -53,7 +53,8 @@ def test_sys_lazy_modules(self): self.fail('lazy import failed') self.assertFalse("test.test_lazy_import.data.basic2" in sys.modules) - self.assertIn("test.test_lazy_import.data", sys.lazy_modules) + # The package is already loaded, so it is not a pending import. + self.assertNotIn("test.test_lazy_import.data", sys.lazy_modules) self.assertIn("test.test_lazy_import.data.basic2", sys.lazy_modules) test.test_lazy_import.data.basic_from_unused.basic2 self.assertNotIn("test.test_import.data", sys.lazy_modules) @@ -1275,6 +1276,103 @@ def test_lazy_module_without_children_is_tracked(self): """) assert_python_ok("-c", code) + def test_already_loaded_module_is_not_tracked(self): + """A lazy import of an already loaded module should not be tracked.""" + code = textwrap.dedent(""" + import sys + + # Loaded by a regular import. + import json + lazy import json as lazy_json + assert "json" not in sys.lazy_modules, ( + f"expected 'json' not in sys.lazy_modules, got {sys.lazy_modules}" + ) + + # Loaded by reifying an earlier lazy import. + lazy import base64 + _ = base64.b64encode + lazy import base64 as lazy_base64 + assert "base64" not in sys.lazy_modules, ( + f"expected 'base64' not in sys.lazy_modules, got {sys.lazy_modules}" + ) + """) + assert_python_ok("-c", code) + + def test_already_loaded_submodule_is_not_tracked(self): + """`lazy from` a loaded submodule should not be tracked either.""" + code = textwrap.dedent(""" + import sys + import test.test_lazy_import.data.pkg.b + lazy from test.test_lazy_import.data.pkg import b + assert "test.test_lazy_import.data.pkg.b" not in sys.lazy_modules, ( + f"expected 'pkg.b' untracked, got {sys.lazy_modules}" + ) + """) + assert_python_ok("-c", code) + + def test_attribute_entry_removed_on_reification(self): + """`lazy from x import attr` should untrack "x.attr" once resolved.""" + code = textwrap.dedent(""" + import sys + lazy from test.test_lazy_import.data.basic2 import x + assert "test.test_lazy_import.data.basic2.x" in sys.lazy_modules, ( + f"expected 'basic2.x' tracked, got {sys.lazy_modules}" + ) + _ = x + assert "test.test_lazy_import.data.basic2.x" not in sys.lazy_modules, ( + f"expected 'basic2.x' untracked, got {sys.lazy_modules}" + ) + """) + assert_python_ok("-c", code) + + def test_failed_reification_stays_tracked(self): + """A lazy import that fails to resolve must stay tracked.""" + code = textwrap.dedent(""" + import sys + lazy import test.test_lazy_import.data.broken_module + try: + _ = test.test_lazy_import.data.broken_module + except ValueError: + pass + else: + raise AssertionError("ValueError was not raised") + assert "test.test_lazy_import.data.broken_module" in sys.lazy_modules, ( + f"failed reification must stay tracked, got {sys.lazy_modules}" + ) + """) + assert_python_ok("-c", code) + + def test_blocked_module_is_still_tracked(self): + """A ``None`` entry in sys.modules must not count as loaded.""" + code = textwrap.dedent(""" + import sys + sys.modules['test.test_lazy_import.data.basic2'] = None + lazy import test.test_lazy_import.data.basic2 + assert "test.test_lazy_import.data.basic2" in sys.lazy_modules, ( + f"blocked module must stay tracked, got {sys.lazy_modules}" + ) + """) + assert_python_ok("-c", code) + + def test_pending_submodule_is_still_tracked(self): + """`lazy from` a submodule that is not loaded must stay tracked.""" + code = textwrap.dedent(""" + import sys + lazy from test.test_lazy_import.data.pkg import b + assert "test.test_lazy_import.data.pkg.b" in sys.lazy_modules, ( + f"expected 'pkg.b' tracked, got {sys.lazy_modules}" + ) + import test.test_lazy_import.data.pkg + assert "test.test_lazy_import.data.pkg.b" not in sys.modules, ( + "loading the package must not load the submodule" + ) + assert "test.test_lazy_import.data.pkg.b" in sys.lazy_modules, ( + f"loading the package must not untrack the submodule, " + f"got {sys.lazy_modules}" + ) + """) + assert_python_ok("-c", code) + @support.requires_subprocess() class CommandLineAndEnvVarTests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-16-15-02-11.gh-issue-155695.Kf3xQa.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-16-15-02-11.gh-issue-155695.Kf3xQa.rst new file mode 100644 index 000000000000000..f0e8c7c849be220 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-16-15-02-11.gh-issue-155695.Kf3xQa.rst @@ -0,0 +1,3 @@ +Resolved names are now removed from :data:`sys.lazy_modules` more +consistently: a lazy import of an already loaded module is no longer recorded, +and reifying ``lazy from pkg import attr`` now discards the ``pkg.attr`` entry. diff --git a/Python/import.c b/Python/import.c index 2fa63dd01833a64..e2f4a568bfda4cf 100644 --- a/Python/import.c +++ b/Python/import.c @@ -3917,6 +3917,20 @@ lazy_import_replay_from(PyThreadState *tstate, PyObject *mod, return obj; } +// The import machinery only discards module names, so this is what removes +// the "pkg.attr" entry left by `lazy from pkg import attr`, submodule or not. +static int +discard_reified_lazy_import(PyInterpreterState *interp, PyObject *lazy_import) +{ + PyObject *name = _PyLazyImport_GetName(lazy_import); + if (name == NULL) { + return -1; + } + int res = PySet_Discard(LAZY_MODULES(interp), name); + Py_DECREF(name); + return res < 0 ? -1 : 0; +} + PyObject * _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) { @@ -4100,6 +4114,10 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) } ok: + if (obj != NULL && discard_reified_lazy_import(interp, lazy_import) < 0) { + Py_CLEAR(obj); + } + if (PySet_Discard(importing, lazy_import) < 0) { Py_CLEAR(obj); } @@ -4358,6 +4376,27 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals, return final_mod; } +// Check if a module is already loaded before adding it to sys.lazy_modules +static int +lazy_modules_add(PyThreadState *tstate, PyObject *name) +{ + PyObject *modules = get_modules_dict(tstate, false); + if (modules == NULL) { + return -1; + } + PyObject *existing; + if (PyDict_GetItemRef(modules, name, &existing) < 0) { + return -1; + } + // A None entry blocks the import rather than satisfying it. + int loaded = (existing != NULL && existing != Py_None); + Py_XDECREF(existing); + if (loaded) { + return 0; + } + return PySet_Add(LAZY_MODULES(tstate->interp), name); +} + // ensure we have the set for the parent module name in sys.lazy_modules. // Returns a new reference. static PyObject * @@ -4451,9 +4490,7 @@ register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name, return -1; } - // Add the module name to sys.lazy_modules set (PEP 810). - PyObject *lazy_modules = LAZY_MODULES(tstate->interp); - if (PySet_Add(lazy_modules, fromname) < 0) { + if (lazy_modules_add(tstate, fromname) < 0) { Py_DECREF(fromname); return -1; } @@ -4627,9 +4664,7 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate, return NULL; } - // Add the module name to sys.lazy_modules set (PEP 810). - PyObject *lazy_modules = LAZY_MODULES(tstate->interp); - if (PySet_Add(lazy_modules, abs_name) < 0) { + if (lazy_modules_add(tstate, abs_name) < 0) { goto error; } From 8dedf69a632c6c0b57a60f1057691b698cca6c71 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Fri, 18 Sep 2026 10:33:44 -0700 Subject: [PATCH 2/5] Keep tracking modules that are still initializing A module is in sys.modules while its body runs, so lazy_modules_add() counted it as loaded and skipped recording the name. If the body then raises, the module is removed from sys.modules again and the lazy import is left pending under no name at all. Check __spec__._initializing so such a module does not count as loaded. A name added while a module initializes is still discarded once the import completes, since _set_lazy_attributes() runs after the body. --- Lib/test/test_lazy_import/__init__.py | 18 ++++++++++++++++++ Lib/test/test_lazy_import/data/init_fails.py | 4 ++++ .../data/lazy_on_init_fails.py | 1 + Python/import.c | 14 ++++++++++++++ 4 files changed, 37 insertions(+) create mode 100644 Lib/test/test_lazy_import/data/init_fails.py create mode 100644 Lib/test/test_lazy_import/data/lazy_on_init_fails.py diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 20a9e2ad78f3b1a..c5527ebab2a9a45 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -1354,6 +1354,24 @@ def test_blocked_module_is_still_tracked(self): """) assert_python_ok("-c", code) + def test_initializing_module_is_still_tracked(self): + """A module that is still executing must not count as loaded.""" + code = textwrap.dedent(""" + import sys + name = "test.test_lazy_import.data.init_fails" + try: + import test.test_lazy_import.data.init_fails + except ValueError: + pass + else: + raise AssertionError("ValueError was not raised") + assert name not in sys.modules, "failed import left a module behind" + assert name in sys.lazy_modules, ( + f"expected {name!r} tracked, got {sys.lazy_modules}" + ) + """) + assert_python_ok("-c", code) + def test_pending_submodule_is_still_tracked(self): """`lazy from` a submodule that is not loaded must stay tracked.""" code = textwrap.dedent(""" diff --git a/Lib/test/test_lazy_import/data/init_fails.py b/Lib/test/test_lazy_import/data/init_fails.py new file mode 100644 index 000000000000000..16e7ad363b0eba6 --- /dev/null +++ b/Lib/test/test_lazy_import/data/init_fails.py @@ -0,0 +1,4 @@ +# Imported by test_initializing_module_is_still_tracked. The module it imports +# lazily imports this one back while this one is still initializing. +import test.test_lazy_import.data.lazy_on_init_fails +raise ValueError("initialization failed") diff --git a/Lib/test/test_lazy_import/data/lazy_on_init_fails.py b/Lib/test/test_lazy_import/data/lazy_on_init_fails.py new file mode 100644 index 000000000000000..7203fcbb4aaa452 --- /dev/null +++ b/Lib/test/test_lazy_import/data/lazy_on_init_fails.py @@ -0,0 +1 @@ +lazy import test.test_lazy_import.data.init_fails diff --git a/Python/import.c b/Python/import.c index e2f4a568bfda4cf..303b4aa080c93af 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4390,6 +4390,20 @@ lazy_modules_add(PyThreadState *tstate, PyObject *name) } // A None entry blocks the import rather than satisfying it. int loaded = (existing != NULL && existing != Py_None); + if (loaded) { + // Check if the module is still initializing. + PyObject *spec; + int rc = PyObject_GetOptionalAttr(existing, &_Py_ID(__spec__), &spec); + if (rc > 0) { + rc = _PyModuleSpec_IsInitializing(spec); + Py_DECREF(spec); + } + if (rc < 0) { + Py_DECREF(existing); + return -1; + } + loaded = !rc; + } Py_XDECREF(existing); if (loaded) { return 0; From 8ef4ae0d767b1739d457bd70b58e5c5baddc8720 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Mon, 21 Sep 2026 11:47:46 -0700 Subject: [PATCH 3/5] Empty commit to re-run CI From 04560684ba821078b8e267eea11f4a256ff1e258 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Sat, 26 Sep 2026 06:21:41 -0700 Subject: [PATCH 4/5] Read __spec__ from the module dict when checking for initialization --- Lib/test/test_lazy_import/__init__.py | 19 +++++++++++++++++++ Python/import.c | 8 +++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index c5527ebab2a9a45..b065d8207ab701c 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -1372,6 +1372,25 @@ def test_initializing_module_is_still_tracked(self): """) assert_python_ok("-c", code) + def test_module_spec_descriptor_is_not_run(self): + """Checking whether a module is loaded must not run its descriptors.""" + code = textwrap.dedent(""" + import sys + import types + + class RaisingSpec(types.ModuleType): + @property + def __spec__(self): + raise RuntimeError("__spec__ descriptor was run") + + sys.modules["raising_spec"] = RaisingSpec("raising_spec") + lazy import raising_spec + assert "raising_spec" not in sys.lazy_modules, ( + f"expected 'raising_spec' untracked, got {sys.lazy_modules}" + ) + """) + assert_python_ok("-c", code) + def test_pending_submodule_is_still_tracked(self): """`lazy from` a submodule that is not loaded must stay tracked.""" code = textwrap.dedent(""" diff --git a/Python/import.c b/Python/import.c index 303b4aa080c93af..5737691c948dc42 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4390,10 +4390,12 @@ lazy_modules_add(PyThreadState *tstate, PyObject *name) } // A None entry blocks the import rather than satisfying it. int loaded = (existing != NULL && existing != Py_None); - if (loaded) { - // Check if the module is still initializing. + if (loaded && PyModule_Check(existing)) { + // Check if the module is still initializing. Read __spec__ from the + // module dict so that a descriptor on a module subclass is not run. PyObject *spec; - int rc = PyObject_GetOptionalAttr(existing, &_Py_ID(__spec__), &spec); + int rc = PyDict_GetItemRef(_PyModule_GetDict(existing), + &_Py_ID(__spec__), &spec); if (rc > 0) { rc = _PyModuleSpec_IsInitializing(spec); Py_DECREF(spec); From ad36b560b9ab1f5e073b5ecb3d6c2d1e132dce42 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sat, 26 Sep 2026 23:48:11 +0100 Subject: [PATCH 5/5] gh-155695: Preserve import behavior in lazy module tracking Read stored initialization flags without invoking spec callbacks or materializing dictionaries. Keep unsupported spec state conservatively tracked. Skip cached exports without changing their import context or clearing independently pending modules. --- Lib/test/test_lazy_import/__init__.py | 132 ++++++++++++++++++++++++++ Python/import.c | 93 +++++++++--------- 2 files changed, 177 insertions(+), 48 deletions(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index b065d8207ab701c..abec4a1d193c8c6 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -1325,6 +1325,61 @@ def test_attribute_entry_removed_on_reification(self): """) assert_python_ok("-c", code) + def test_already_loaded_attribute_is_not_tracked(self): + code = textwrap.dedent(""" + import sys + import math + + lazy from math import pi + assert "math.pi" not in sys.lazy_modules, sys.lazy_modules + assert pi == math.pi + """) + assert_python_ok("-c", code) + + def test_cached_lazy_attribute_keeps_its_import_context(self): + code = textwrap.dedent(""" + import builtins + import sys + import test.test_lazy_import.data.basic_from_unused + + holder = "test.test_lazy_import.data.basic_from_unused" + target = "test.test_lazy_import.data.basic2" + default_import = builtins.__import__ + + def import_hook(name, *args): + if name == holder: + raise RuntimeError("cached placeholder imported its holder") + return default_import(name, *args) + + namespace = { + "__builtins__": dict(builtins.__dict__, __import__=import_hook), + "__name__": "cached_import_test", + } + exec(f"lazy from {holder} import basic2", namespace) + assert holder + ".basic2" not in sys.lazy_modules, sys.lazy_modules + assert target in sys.lazy_modules, sys.lazy_modules + exec("assert basic2.x == 42", namespace) + assert target not in sys.lazy_modules, sys.lazy_modules + """) + assert_python_ok("-c", code) + + def test_cached_attribute_keeps_pending_module_tracked(self): + code = textwrap.dedent(""" + import sys + import test.test_lazy_import.data.pkg as pkg + pkg.b = 42 + + lazy import test.test_lazy_import.data.pkg.b as pending + lazy from test.test_lazy_import.data.pkg import b + name = "test.test_lazy_import.data.pkg.b" + assert b == 42, b + assert name not in sys.modules, sys.modules + assert name in sys.lazy_modules, sys.lazy_modules + assert pending.foo() == "foo" + assert name not in sys.lazy_modules, sys.lazy_modules + """) + assert_python_ok("-c", code) + def test_failed_reification_stays_tracked(self): """A lazy import that fails to resolve must stay tracked.""" code = textwrap.dedent(""" @@ -1391,6 +1446,83 @@ def __spec__(self): """) assert_python_ok("-c", code) + def test_spec_initializing_descriptor_is_not_run(self): + code = textwrap.dedent(""" + import sys + import types + + class Spec: + @property + def _initializing(self): + raise RuntimeError("_initializing descriptor was run") + + @property + def __dict__(self): + raise RuntimeError("__dict__ descriptor was run") + + class SlottedSpec: + __slots__ = () + + @property + def _initializing(self): + raise RuntimeError("_initializing descriptor was run") + + for spec in (Spec(), SlottedSpec()): + module = types.ModuleType("custom_spec") + module.__spec__ = spec + sys.modules["custom_spec"] = module + lazy import custom_spec + """) + assert_python_ok("-c", code) + + def test_cached_attribute_does_not_check_spec_twice(self): + code = textwrap.dedent(""" + import sys + import types + + class Spec: + def __init__(self): + self.calls = 0 + + @property + def _initializing(self): + self.calls += 1 + if self.calls == 2: + raise RuntimeError("_initializing was read twice") + return False + + module = types.ModuleType("cached_spec") + module.__spec__ = Spec() + module.attr = 1 + sys.modules["cached_spec"] = module + lazy from cached_spec import attr + assert attr == 1, attr + """) + assert_python_ok("-c", code) + + def test_spec_initializing_truth_conversion_is_not_run(self): + code = textwrap.dedent(""" + import sys + import types + from importlib.machinery import ModuleSpec + + class Flag: + def __bool__(self): + raise RuntimeError("_initializing truth conversion was run") + + released = memoryview(b"") + released.release() + for flag in (Flag(), released): + sys.lazy_modules.discard("custom_spec") + module = types.ModuleType("custom_spec") + module.__spec__ = ModuleSpec("custom_spec", None) + module.__spec__._initializing = flag + sys.modules["custom_spec"] = module + lazy import custom_spec + assert "custom_spec" in sys.lazy_modules, sys.lazy_modules + """) + assert_python_ok("-c", code) + def test_pending_submodule_is_still_tracked(self): """`lazy from` a submodule that is not loaded must stay tracked.""" code = textwrap.dedent(""" diff --git a/Python/import.c b/Python/import.c index 5737691c948dc42..b9b3bc3fcdd20a7 100644 --- a/Python/import.c +++ b/Python/import.c @@ -3917,20 +3917,6 @@ lazy_import_replay_from(PyThreadState *tstate, PyObject *mod, return obj; } -// The import machinery only discards module names, so this is what removes -// the "pkg.attr" entry left by `lazy from pkg import attr`, submodule or not. -static int -discard_reified_lazy_import(PyInterpreterState *interp, PyObject *lazy_import) -{ - PyObject *name = _PyLazyImport_GetName(lazy_import); - if (name == NULL) { - return -1; - } - int res = PySet_Discard(LAZY_MODULES(interp), name); - Py_DECREF(name); - return res < 0 ? -1 : 0; -} - PyObject * _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) { @@ -4114,8 +4100,12 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) } ok: - if (obj != NULL && discard_reified_lazy_import(interp, lazy_import) < 0) { - Py_CLEAR(obj); + if (obj != NULL) { + PyObject *name = _PyLazyImport_GetName(lazy_import); + if (name == NULL || PySet_Discard(LAZY_MODULES(interp), name) < 0) { + Py_CLEAR(obj); + } + Py_XDECREF(name); } if (PySet_Discard(importing, lazy_import) < 0) { @@ -4376,41 +4366,35 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals, return final_mod; } -// Check if a module is already loaded before adding it to sys.lazy_modules static int lazy_modules_add(PyThreadState *tstate, PyObject *name) { - PyObject *modules = get_modules_dict(tstate, false); - if (modules == NULL) { + PyObject *mod = import_get_module(tstate, name); + if (mod == NULL && PyErr_Occurred()) { return -1; } - PyObject *existing; - if (PyDict_GetItemRef(modules, name, &existing) < 0) { - return -1; - } - // A None entry blocks the import rather than satisfying it. - int loaded = (existing != NULL && existing != Py_None); - if (loaded && PyModule_Check(existing)) { - // Check if the module is still initializing. Read __spec__ from the - // module dict so that a descriptor on a module subclass is not run. + int loaded = mod != NULL && mod != Py_None; + if (loaded && PyModule_Check(mod)) { PyObject *spec; - int rc = PyDict_GetItemRef(_PyModule_GetDict(existing), - &_Py_ID(__spec__), &spec); - if (rc > 0) { - rc = _PyModuleSpec_IsInitializing(spec); - Py_DECREF(spec); - } - if (rc < 0) { - Py_DECREF(existing); + if (PyDict_GetItemRef(_PyModule_GetDict(mod), &_Py_ID(__spec__), &spec) < 0) { + Py_DECREF(mod); return -1; } - loaded = !rc; - } - Py_XDECREF(existing); - if (loaded) { - return 0; + loaded = spec == NULL || spec == Py_None; + if (!loaded) { + // Read the usual flag without callbacks or dictionary allocation. + // Unsupported spec representations stay conservatively tracked. + PyObject *initializing = NULL; + if ((Py_TYPE(spec)->tp_flags & Py_TPFLAGS_INLINE_VALUES) && + _PyObject_TryGetInstanceAttribute(spec, &_Py_ID(_initializing), &initializing)) { + loaded = initializing == NULL || initializing == Py_False; + } + Py_XDECREF(initializing); + } + Py_XDECREF(spec); } - return PySet_Add(LAZY_MODULES(tstate->interp), name); + Py_XDECREF(mod); + return loaded ? 0 : PySet_Add(LAZY_MODULES(tstate->interp), name); } // ensure we have the set for the parent module name in sys.lazy_modules. @@ -4501,19 +4485,32 @@ static int register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name, PyObject *from) { - PyObject *fromname = PyUnicode_FromFormat("%U.%U", abs_name, from); - if (fromname == NULL) { + // IMPORT_FROM returns stored attributes directly. Their imports are + // already resolved or tracked by their own placeholders, so skip the alias. + PyObject *mod = import_get_module(tstate, abs_name); + if (mod == NULL && PyErr_Occurred()) { return -1; } + int rc = 0; + if (mod != NULL && PyModule_Check(mod)) { + rc = PyDict_Contains(_PyModule_GetDict(mod), from); + } + Py_XDECREF(mod); + if (rc != 0) { + return rc < 0 ? -1 : 0; + } - if (lazy_modules_add(tstate, fromname) < 0) { - Py_DECREF(fromname); + PyObject *fromname = PyUnicode_FromFormat("%U.%U", abs_name, from); + if (fromname == NULL) { return -1; } - int res = register_lazy_on_parent(tstate, fromname); + rc = lazy_modules_add(tstate, fromname); + if (rc == 0) { + rc = register_lazy_on_parent(tstate, fromname); + } Py_DECREF(fromname); - return res; + return rc; } _PyLazySubmoduleImportResult