Skip to content

Commit 56f9837

Browse files
authored
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.
1 parent 2eb2af8 commit 56f9837

4 files changed

Lines changed: 20 additions & 23 deletions

File tree

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,19 @@ def test_from_import_with_imported_module_getattr(self):
168168
""")
169169
assert_python_ok("-c", code)
170170

171+
@support.requires_subprocess()
172+
def test_module_getattr_does_not_shadow_own_lazy_import(self):
173+
"""Module __getattr__ should not shadow the module's own lazy imports."""
174+
code = textwrap.dedent("""
175+
import types
176+
import test.test_lazy_import.data.module_with_lazy_import_and_getattr as mod
177+
assert mod.basic2.__name__ == "test.test_lazy_import.data.basic2"
178+
assert mod.f is mod.basic2.f
179+
assert not isinstance(vars(mod)["basic2"], types.LazyImportType)
180+
assert mod.missing == "from_getattr:missing"
181+
""")
182+
assert_python_ok("-c", code)
183+
171184

172185
class GlobalLazyImportModeTests(LazyImportTestCase):
173186
"""Tests for sys.set_lazy_imports() global mode control."""
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
lazy import test.test_lazy_import.data.basic2 as basic2
2+
lazy from test.test_lazy_import.data.basic2 import f
3+
4+
def __getattr__(name):
5+
return f"from_getattr:{name}"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix module-level :meth:`~module.__getattr__` being called before a module's
2+
own lazy imports were resolved, which allowed it to shadow them.

‎Objects/moduleobject.c‎

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,29 +1362,6 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
13621362
}
13631363
if (attr) {
13641364
if (PyLazyImport_CheckExact(attr)) {
1365-
// gh-144957: Module __getattr__ should get a chance to provide
1366-
// the attribute before resolving a lazy import placeholder.
1367-
// Resolving __getattr__ itself must not invoke the hook.
1368-
getattr = NULL;
1369-
if (!_PyUnicode_EqualToASCIIString(name, "__getattr__") &&
1370-
module_get_resolved_dict_item(
1371-
m->md_dict, &_Py_ID(__getattr__), &getattr) < 0) {
1372-
Py_DECREF(attr);
1373-
return NULL;
1374-
}
1375-
if (getattr) {
1376-
PyObject *result = PyObject_CallOneArg(getattr, name);
1377-
Py_DECREF(getattr);
1378-
if (result != NULL) {
1379-
Py_DECREF(attr);
1380-
return result;
1381-
}
1382-
if (!PyErr_ExceptionMatches(PyExc_AttributeError)) {
1383-
Py_DECREF(attr);
1384-
return NULL;
1385-
}
1386-
PyErr_Clear();
1387-
}
13881365
PyThreadState *tstate = PyThreadState_GET();
13891366
if (suppress && _PyLazyImport_IsResolving(tstate, attr)) {
13901367
// Only direct reentry means this attribute is unavailable.

0 commit comments

Comments
 (0)