https://github.com/python/cpython/commit/56f9837db25fd65fa6d10862e572270574ab2b0d
commit: 56f9837db25fd65fa6d10862e572270574ab2b0d
branch: main
author: Brittany Reynoso <[email protected]>
committer: encukou <[email protected]>
date: 2026-10-08T14:52:30+02:00
summary:

gh-158824: Fix `__getattr__` blocking reification of lazy imports (GH-158825)

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.

files:
A Lib/test/test_lazy_import/data/module_with_lazy_import_and_getattr.py
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-03-25-30.gh-issue-158824.h-hJj7.rst
M Lib/test/test_lazy_import/__init__.py
M Objects/moduleobject.c

diff --git a/Lib/test/test_lazy_import/__init__.py 
b/Lib/test/test_lazy_import/__init__.py
index e0c5552fe11c53e..8a90ffa662e6e23 100644
--- a/Lib/test/test_lazy_import/__init__.py
+++ b/Lib/test/test_lazy_import/__init__.py
@@ -168,6 +168,19 @@ 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 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)
+
 
 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..e9a1f1a88e909f4
--- /dev/null
+++ b/Lib/test/test_lazy_import/data/module_with_lazy_import_and_getattr.py
@@ -0,0 +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
new file mode 100644
index 000000000000000..b7bcc999ac68d75
--- /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,2 @@
+Fix module-level :meth:`~module.__getattr__` being called before a module's
+own lazy imports were resolved, which allowed it to shadow them.
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.

_______________________________________________
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