]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.15] gh-144957: Fix lazy imports + module __getattr__ (GH-149624) (#149678)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Mon, 11 May 2026 13:36:35 +0000 (15:36 +0200)
committerGitHub <noreply@github.com>
Mon, 11 May 2026 13:36:35 +0000 (13:36 +0000)
gh-144957: Fix lazy imports + module __getattr__ (GH-149624)
(cherry picked from commit 56171da3417bc14fded2f42033d72f63e1bf7cd9)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Lib/test/test_lazy_import/__init__.py
Lib/test/test_lazy_import/data/module_with_getattr.py [new file with mode: 0644]
Lib/test/test_lazy_import/data/pkg/__init__.py
Misc/NEWS.d/next/Core_and_Builtins/2026-05-09-15-22-32.gh-issue-144957.u1F2aQ.rst [new file with mode: 0644]
Objects/moduleobject.c

index 1d1d2e00bd733f47014cf07c06925b8323f84491..ea534a8ee5b981129a3a466bc3073a0794814588 100644 (file)
@@ -88,6 +88,26 @@ class LazyImportTests(unittest.TestCase):
         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 @@ class PackageTests(unittest.TestCase):
         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 (file)
index 0000000..2ac01a9
--- /dev/null
@@ -0,0 +1,4 @@
+def __getattr__(name):
+    if name == "dynamic_attr":
+        return "from_getattr"
+    raise AttributeError(name)
index 2d76abaa89f8937e714ae74aabbac5aa1b755274..e526aab94131b861a01f2f92597c29c928520a9e 100644 (file)
@@ -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 (file)
index 0000000..3063f1a
--- /dev/null
@@ -0,0 +1,2 @@
+Fix lazy ``from`` imports of module attributes provided by module-level
+``__getattr__``.
index b7d2e5ffde4fe7d4e5e67bff1fbca381434db248..f7b83c1d111cded819d3dc1d2291bed592357b12 100644 (file)
@@ -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) {