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.