Skip to content

Commit 8fdeb2d

Browse files
[3.15] gh-144957: Fix lazy imports + module __getattr__ (GH-149624) (#149678)
gh-144957: Fix lazy imports + module __getattr__ (GH-149624) (cherry picked from commit 56171da) Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent 592a356 commit 8fdeb2d

5 files changed

Lines changed: 61 additions & 0 deletions

File tree

Lib/test/test_lazy_import/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,26 @@ def test_basic_used(self):
8888
import test.test_lazy_import.data.basic_used
8989
self.assertIn("test.test_lazy_import.data.basic2", sys.modules)
9090

91+
@support.requires_subprocess()
92+
def test_from_import_with_module_getattr(self):
93+
"""Lazy from import should respect module-level __getattr__."""
94+
code = textwrap.dedent("""
95+
lazy from test.test_lazy_import.data.module_with_getattr import dynamic_attr
96+
assert dynamic_attr == "from_getattr"
97+
""")
98+
assert_python_ok("-c", code)
99+
100+
@support.requires_subprocess()
101+
def test_from_import_with_imported_module_getattr(self):
102+
"""Lazy from import should not shadow an imported module's __getattr__."""
103+
code = textwrap.dedent("""
104+
import test.test_lazy_import.data.module_with_getattr as mod
105+
lazy from test.test_lazy_import.data.module_with_getattr import dynamic_attr
106+
assert dynamic_attr == "from_getattr"
107+
assert mod.dynamic_attr == "from_getattr"
108+
""")
109+
assert_python_ok("-c", code)
110+
91111

92112
class GlobalLazyImportModeTests(unittest.TestCase):
93113
"""Tests for sys.set_lazy_imports() global mode control."""
@@ -385,6 +405,17 @@ def test_lazy_import_pkg_cross_import(self):
385405
self.assertEqual(type(g["x"]), int)
386406
self.assertEqual(type(g["b"]), types.LazyImportType)
387407

408+
@support.requires_subprocess()
409+
def test_package_from_import_with_module_getattr(self):
410+
"""Lazy from import should respect a package's __getattr__."""
411+
code = textwrap.dedent("""
412+
import test.test_lazy_import.data.pkg as pkg
413+
lazy from test.test_lazy_import.data.pkg import dynamic_attr
414+
assert dynamic_attr == "from_getattr"
415+
assert pkg.dynamic_attr == "from_getattr"
416+
""")
417+
assert_python_ok("-c", code)
418+
388419

389420
class DunderLazyImportTests(unittest.TestCase):
390421
"""Tests for __lazy_import__ builtin function."""
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def __getattr__(name):
2+
if name == "dynamic_attr":
3+
return "from_getattr"
4+
raise AttributeError(name)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
x = 42
2+
3+
def __getattr__(name):
4+
if name == "dynamic_attr":
5+
return "from_getattr"
6+
raise AttributeError(name)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix lazy ``from`` imports of module attributes provided by module-level
2+
``__getattr__``.

Objects/moduleobject.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,25 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
13071307
attr = _PyObject_GenericGetAttrWithDict((PyObject *)m, name, NULL, suppress);
13081308
if (attr) {
13091309
if (PyLazyImport_CheckExact(attr)) {
1310+
// gh-144957: Module __getattr__ should get a chance to provide
1311+
// the attribute before resolving a lazy import placeholder.
1312+
if (PyDict_GetItemRef(m->md_dict, &_Py_ID(__getattr__), &getattr) < 0) {
1313+
Py_DECREF(attr);
1314+
return NULL;
1315+
}
1316+
if (getattr) {
1317+
PyObject *result = PyObject_CallOneArg(getattr, name);
1318+
Py_DECREF(getattr);
1319+
if (result != NULL) {
1320+
Py_DECREF(attr);
1321+
return result;
1322+
}
1323+
if (!PyErr_ExceptionMatches(PyExc_AttributeError)) {
1324+
Py_DECREF(attr);
1325+
return NULL;
1326+
}
1327+
PyErr_Clear();
1328+
}
13101329
PyObject *new_value = _PyImport_LoadLazyImportTstate(
13111330
PyThreadState_GET(), attr);
13121331
if (new_value == NULL) {

0 commit comments

Comments
 (0)