diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index df19af05246dcd..64b254e4bd16ca 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -87,6 +87,23 @@ def test_basic_used(self): import test.test_lazy_import.data.basic_used self.assertIn("test.test_lazy_import.data.basic2", sys.modules) + def test_lazy_import_with_getattr(self): + """Lazy imports work with module __getattr__ (gh-144957).""" + code = textwrap.dedent(""" + import sys + sys.set_lazy_imports("normal") + lazy from test.test_lazy_import.data.module_with_getattr import dynamic_attr + assert dynamic_attr == "from_getattr" + print("OK") + """) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("OK", result.stdout) + class GlobalLazyImportModeTests(unittest.TestCase): """Tests for sys.set_lazy_imports() global mode control.""" diff --git a/Lib/test/test_lazy_import/data/module_with_getattr.py b/Lib/test/test_lazy_import/data/module_with_getattr.py new file mode 100644 index 00000000000000..9688c85f7bb3a1 --- /dev/null +++ b/Lib/test/test_lazy_import/data/module_with_getattr.py @@ -0,0 +1,7 @@ +"""Test module with __getattr__ for gh-144957.""" + +def __getattr__(name): + """Provide dynamic attributes.""" + if name == "dynamic_attr": + return "from_getattr" + raise AttributeError(f"module has no attribute {name!r}") diff --git a/Python/ceval.c b/Python/ceval.c index 2cd7c7bfd28d09..a3f8671a0aad58 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3084,6 +3084,24 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) PyObject *fullmodname, *mod_name, *origin, *mod_name_or_unknown, *errmsg, *spec; if (PyObject_GetOptionalAttr(v, name, &x) != 0) { + // gh-144957: If we got a lazy import object, the module might have + // __getattr__ that should be tried first + if (x != NULL && PyLazyImport_CheckExact(x)) { + PyObject *getattr_func; + if (PyObject_GetOptionalAttr(v, &_Py_ID(__getattr__), &getattr_func) < 0) { + Py_DECREF(x); + return NULL; + } + if (getattr_func != NULL) { + PyObject *result = PyObject_CallOneArg(getattr_func, name); + Py_DECREF(getattr_func); + if (result != NULL) { + Py_DECREF(x); + return result; + } + PyErr_Clear(); + } + } return x; } /* Issue #17636: in case this failed because of a circular relative