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]