From 9a55b7869a7213edf16ec459c33821801a4122d1 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Sun, 4 Oct 2026 19:50:28 -0700 Subject: [PATCH 1/4] Don't let module __getattr__ shadow a module's own lazy imports When a module attribute was a lazy import placeholder, module attribute lookup called the module's __getattr__ before resolving it. This was needed while lazy from-imports published placeholders on the source module. That no longer happens, so the check only affected the module's own lazy imports: a catch-all __getattr__ hid them, unlike eager imports. Resolve the placeholder directly. --- Lib/test/test_lazy_import/__init__.py | 10 ++++++++ .../module_with_lazy_import_and_getattr.py | 4 ++++ Objects/moduleobject.c | 23 ------------------- 3 files changed, 14 insertions(+), 23 deletions(-) create mode 100644 Lib/test/test_lazy_import/data/module_with_lazy_import_and_getattr.py diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index e0c5552fe11c53e..0491bf147a3c02f 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -168,6 +168,16 @@ def test_from_import_with_imported_module_getattr(self): """) assert_python_ok("-c", code) + @support.requires_subprocess() + def test_module_getattr_does_not_shadow_own_lazy_import(self): + """Module __getattr__ should not shadow the module's own lazy imports.""" + code = textwrap.dedent(""" + import test.test_lazy_import.data.module_with_lazy_import_and_getattr as mod + assert mod.basic2.__name__ == "test.test_lazy_import.data.basic2" + assert mod.missing == "from_getattr:missing" + """) + assert_python_ok("-c", code) + class GlobalLazyImportModeTests(LazyImportTestCase): """Tests for sys.set_lazy_imports() global mode control.""" diff --git a/Lib/test/test_lazy_import/data/module_with_lazy_import_and_getattr.py b/Lib/test/test_lazy_import/data/module_with_lazy_import_and_getattr.py new file mode 100644 index 000000000000000..59cdb4efee8d658 --- /dev/null +++ b/Lib/test/test_lazy_import/data/module_with_lazy_import_and_getattr.py @@ -0,0 +1,4 @@ +lazy import test.test_lazy_import.data.basic2 as basic2 + +def __getattr__(name): + return f"from_getattr:{name}" diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index d13b513ab468e49..164f24e69c69bae 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1362,29 +1362,6 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } if (attr) { if (PyLazyImport_CheckExact(attr)) { - // gh-144957: Module __getattr__ should get a chance to provide - // the attribute before resolving a lazy import placeholder. - // Resolving __getattr__ itself must not invoke the hook. - getattr = NULL; - if (!_PyUnicode_EqualToASCIIString(name, "__getattr__") && - module_get_resolved_dict_item( - m->md_dict, &_Py_ID(__getattr__), &getattr) < 0) { - Py_DECREF(attr); - return NULL; - } - if (getattr) { - PyObject *result = PyObject_CallOneArg(getattr, name); - Py_DECREF(getattr); - if (result != NULL) { - Py_DECREF(attr); - return result; - } - if (!PyErr_ExceptionMatches(PyExc_AttributeError)) { - Py_DECREF(attr); - return NULL; - } - PyErr_Clear(); - } PyThreadState *tstate = PyThreadState_GET(); if (suppress && _PyLazyImport_IsResolving(tstate, attr)) { // Only direct reentry means this attribute is unavailable. From c83d2206223aae13068bb41427373641cf15eb4d Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 03:25:32 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst new file mode 100644 index 000000000000000..aff0cd7c26e9ed2 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst @@ -0,0 +1 @@ +Bug fix: module level `__getattr__` was obfuscating lazy imports from being reified. From ebcb52c915db5ee75d38ee45d0524dec5137bcdd Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Sun, 4 Oct 2026 20:31:56 -0700 Subject: [PATCH 3/4] Extend the module __getattr__ test and fix the NEWS entry markup --- Lib/test/test_lazy_import/__init__.py | 3 +++ .../data/module_with_lazy_import_and_getattr.py | 1 + .../2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 0491bf147a3c02f..8a90ffa662e6e23 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -172,8 +172,11 @@ def test_from_import_with_imported_module_getattr(self): def test_module_getattr_does_not_shadow_own_lazy_import(self): """Module __getattr__ should not shadow the module's own lazy imports.""" code = textwrap.dedent(""" + import types import test.test_lazy_import.data.module_with_lazy_import_and_getattr as mod assert mod.basic2.__name__ == "test.test_lazy_import.data.basic2" + assert mod.f is mod.basic2.f + assert not isinstance(vars(mod)["basic2"], types.LazyImportType) assert mod.missing == "from_getattr:missing" """) assert_python_ok("-c", code) diff --git a/Lib/test/test_lazy_import/data/module_with_lazy_import_and_getattr.py b/Lib/test/test_lazy_import/data/module_with_lazy_import_and_getattr.py index 59cdb4efee8d658..e9a1f1a88e909f4 100644 --- a/Lib/test/test_lazy_import/data/module_with_lazy_import_and_getattr.py +++ b/Lib/test/test_lazy_import/data/module_with_lazy_import_and_getattr.py @@ -1,4 +1,5 @@ lazy import test.test_lazy_import.data.basic2 as basic2 +lazy from test.test_lazy_import.data.basic2 import f def __getattr__(name): return f"from_getattr:{name}" diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst index aff0cd7c26e9ed2..3cf4f67c858b564 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst @@ -1 +1,2 @@ -Bug fix: module level `__getattr__` was obfuscating lazy imports from being reified. +Module-level :meth:`~module.__getattr__` was called before a module's own +lazy imports were resolved, so it could shadow them. From e71c95070463cf76a6a574d09bb38831472aaa10 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Mon, 5 Oct 2026 13:11:18 -0700 Subject: [PATCH 4/4] Reword the NEWS entry to describe the fix --- .../2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst index 3cf4f67c858b564..b7bcc999ac68d75 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst @@ -1,2 +1,2 @@ -Module-level :meth:`~module.__getattr__` was called before a module's own -lazy imports were resolved, so it could shadow them. +Fix module-level :meth:`~module.__getattr__` being called before a module's +own lazy imports were resolved, which allowed it to shadow them.