https://github.com/python/cpython/commit/56171da3417bc14fded2f42033d72f63e1bf7cd9
commit: 56171da3417bc14fded2f42033d72f63e1bf7cd9
branch: main
author: Jelle Zijlstra <[email protected]>
committer: encukou <[email protected]>
date: 2026-05-11T15:08:12+02:00
summary:
gh-144957: Fix lazy imports + module __getattr__ (GH-149624)
files:
A Lib/test/test_lazy_import/data/module_with_getattr.py
A
Misc/NEWS.d/next/Core_and_Builtins/2026-05-09-15-22-32.gh-issue-144957.u1F2aQ.rst
M Lib/test/test_lazy_import/__init__.py
M Lib/test/test_lazy_import/data/pkg/__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 1d1d2e00bd733f..ea534a8ee5b981 100644
--- a/Lib/test/test_lazy_import/__init__.py
+++ b/Lib/test/test_lazy_import/__init__.py
@@ -88,6 +88,26 @@ def test_basic_used(self):
import test.test_lazy_import.data.basic_used
self.assertIn("test.test_lazy_import.data.basic2", sys.modules)
+ @support.requires_subprocess()
+ def test_from_import_with_module_getattr(self):
+ """Lazy from import should respect module-level __getattr__."""
+ code = textwrap.dedent("""
+ lazy from test.test_lazy_import.data.module_with_getattr import
dynamic_attr
+ assert dynamic_attr == "from_getattr"
+ """)
+ assert_python_ok("-c", code)
+
+ @support.requires_subprocess()
+ def test_from_import_with_imported_module_getattr(self):
+ """Lazy from import should not shadow an imported module's
__getattr__."""
+ code = textwrap.dedent("""
+ import test.test_lazy_import.data.module_with_getattr as mod
+ lazy from test.test_lazy_import.data.module_with_getattr import
dynamic_attr
+ assert dynamic_attr == "from_getattr"
+ assert mod.dynamic_attr == "from_getattr"
+ """)
+ assert_python_ok("-c", code)
+
class GlobalLazyImportModeTests(unittest.TestCase):
"""Tests for sys.set_lazy_imports() global mode control."""
@@ -385,6 +405,17 @@ def test_lazy_import_pkg_cross_import(self):
self.assertEqual(type(g["x"]), int)
self.assertEqual(type(g["b"]), types.LazyImportType)
+ @support.requires_subprocess()
+ def test_package_from_import_with_module_getattr(self):
+ """Lazy from import should respect a package's __getattr__."""
+ code = textwrap.dedent("""
+ import test.test_lazy_import.data.pkg as pkg
+ lazy from test.test_lazy_import.data.pkg import dynamic_attr
+ assert dynamic_attr == "from_getattr"
+ assert pkg.dynamic_attr == "from_getattr"
+ """)
+ assert_python_ok("-c", code)
+
class DunderLazyImportTests(unittest.TestCase):
"""Tests for __lazy_import__ builtin function."""
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..2ac01a90d76e62
--- /dev/null
+++ b/Lib/test/test_lazy_import/data/module_with_getattr.py
@@ -0,0 +1,4 @@
+def __getattr__(name):
+ if name == "dynamic_attr":
+ return "from_getattr"
+ raise AttributeError(name)
diff --git a/Lib/test/test_lazy_import/data/pkg/__init__.py
b/Lib/test/test_lazy_import/data/pkg/__init__.py
index 2d76abaa89f893..e526aab94131b8 100644
--- a/Lib/test/test_lazy_import/data/pkg/__init__.py
+++ b/Lib/test/test_lazy_import/data/pkg/__init__.py
@@ -1 +1,6 @@
x = 42
+
+def __getattr__(name):
+ if name == "dynamic_attr":
+ return "from_getattr"
+ raise AttributeError(name)
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-09-15-22-32.gh-issue-144957.u1F2aQ.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-09-15-22-32.gh-issue-144957.u1F2aQ.rst
new file mode 100644
index 00000000000000..3063f1a3c0e6d3
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-09-15-22-32.gh-issue-144957.u1F2aQ.rst
@@ -0,0 +1,2 @@
+Fix lazy ``from`` imports of module attributes provided by module-level
+``__getattr__``.
diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c
index b7d2e5ffde4fe7..f7b83c1d111cde 100644
--- a/Objects/moduleobject.c
+++ b/Objects/moduleobject.c
@@ -1307,6 +1307,25 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject
*name, int suppress)
attr = _PyObject_GenericGetAttrWithDict((PyObject *)m, name, NULL,
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.
+ if (PyDict_GetItemRef(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();
+ }
PyObject *new_value = _PyImport_LoadLazyImportTstate(
PyThreadState_GET(), attr);
if (new_value == NULL) {
_______________________________________________
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]