https://github.com/python/cpython/commit/aad4500cd14bcd022b291c293f8cefadcd266302 commit: aad4500cd14bcd022b291c293f8cefadcd266302 branch: 3.15 author: Miss Islington (bot) <[email protected]> committer: encukou <[email protected]> date: 2026-10-12T13:38:34+02:00 summary:
[3.15] gh-158824: Fix `__getattr__` blocking reification of lazy imports (GH-158825) (GH-159022) 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. (cherry picked from commit 56f9837db25fd65fa6d10862e572270574ab2b0d) Co-authored-by: Brittany Reynoso <[email protected]> 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 aea0d84ee685fed..1a310314aa87828 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]
