Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Lib/test/test_lazy_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_lazy_import/data/module_with_getattr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def __getattr__(name):
if name == "dynamic_attr":
return "from_getattr"
raise AttributeError(name)
5 changes: 5 additions & 0 deletions Lib/test/test_lazy_import/data/pkg/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
x = 42

def __getattr__(name):
if name == "dynamic_attr":
return "from_getattr"
raise AttributeError(name)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix lazy ``from`` imports of module attributes provided by module-level
``__getattr__``.
19 changes: 19 additions & 0 deletions Objects/moduleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment thread
pablogsal marked this conversation as resolved.
PyErr_Clear();
}
PyObject *new_value = _PyImport_LoadLazyImportTstate(
PyThreadState_GET(), attr);
if (new_value == NULL) {
Expand Down
Loading