From 3e63300a46496a42b720a3301c24fb7ad2142868 Mon Sep 17 00:00:00 2001 From: Gouri Jain Date: Fri, 27 Feb 2026 23:47:31 +0530 Subject: [PATCH 1/2] gh-144957: Add test for lazy imports with __getattr__ Adds regression test to verify lazy imports work correctly with modules that use __getattr__ for dynamic attributes (e.g. typing.Match). The issue appears to be already fixed in current main branch. --- .../data/lazy_imports/module_with_getattr.py | 7 +++++++ Lib/test/test_import/test_lazy_imports.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 Lib/test/test_import/data/lazy_imports/module_with_getattr.py diff --git a/Lib/test/test_import/data/lazy_imports/module_with_getattr.py b/Lib/test/test_import/data/lazy_imports/module_with_getattr.py new file mode 100644 index 00000000000000..9688c85f7bb3a1 --- /dev/null +++ b/Lib/test/test_import/data/lazy_imports/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/Lib/test/test_import/test_lazy_imports.py b/Lib/test/test_import/test_lazy_imports.py index d4df772d2034d9..036d2e897183fa 100644 --- a/Lib/test/test_import/test_lazy_imports.py +++ b/Lib/test/test_import/test_lazy_imports.py @@ -85,6 +85,23 @@ def test_basic_used(self): import test.test_import.data.lazy_imports.basic_used self.assertIn("test.test_import.data.lazy_imports.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_import.data.lazy_imports.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.""" From e7def6c0d8799277110a131232b70443bcbaad96 Mon Sep 17 00:00:00 2001 From: Gouri Jain Date: Sat, 28 Feb 2026 02:34:08 +0530 Subject: [PATCH 2/2] gh-144957: Fix lazy imports to respect module __getattr__ When resolving lazy imports, check if a lazy import object was found and the module has __getattr__. If so, try calling __getattr__ first before using the lazy import object. --- Python/ceval.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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