self.assertIn("test.test_lazy_import.data.pkg.bar", sys.modules)
self.assertIn("BAR_MODULE_LOADED", out.getvalue())
+ def test_lazy_submodule_stored_in_parent_dict(self):
+ """Accessing a lazy submodule should store it in the parent's __dict__."""
+ import test.test_lazy_import.data.lazy_import_pkg
+
+ pkg = sys.modules["test.test_lazy_import.data.pkg"]
+ self.assertIn("bar", pkg.__dict__)
+ self.assertIs(pkg.__dict__["bar"], sys.modules["test.test_lazy_import.data.pkg.bar"])
+
def test_lazy_import_pkg_cross_import(self):
"""Cross-imports within package should preserve lazy imports."""
import test.test_lazy_import.data.pkg.c
self.assertEqual(type(g["x"]), int)
self.assertEqual(type(g["b"]), types.LazyImportType)
+ @support.requires_subprocess()
+ def test_lazy_from_import_does_not_pollute_parent(self):
+ """Lazy from import should not add the name to the parent module's dict."""
+ code = textwrap.dedent("""
+ lazy from json import nonexistent_attr
+ import json
+ assert "nonexistent_attr" not in json.__dict__, (
+ "lazy from import should not publish attributes on the parent module"
+ )
+ """)
+ assert_python_ok("-c", code)
+
@support.requires_subprocess()
def test_package_from_import_with_module_getattr_raising(self):
"""Lazy from import should respect a package's __getattr__."""
sys.set_lazy_imports("normal")
def test_import_error_shows_chained_traceback(self):
- """ImportError during reification should chain to show both definition and access."""
- # Errors at reification must show where the lazy import was defined
- # AND where the access happened, per PEP 810 "Reification" section
+ """Accessing a nonexistent lazy submodule via parent attr raises AttributeError."""
code = textwrap.dedent("""
import sys
lazy import test.test_lazy_import.data.nonexistent_module
try:
x = test.test_lazy_import.data.nonexistent_module
- except ImportError as e:
- # Should have __cause__ showing the original error
- # The exception chain shows both where import was defined and where access happened
- assert e.__cause__ is not None, "Expected chained exception"
+ except AttributeError as e:
print("OK")
""")
result = subprocess.run(
# First access - should fail
try:
x = test.test_lazy_import.data.broken_module
- except ValueError:
+ except AttributeError:
pass
# The lazy object should still be a lazy proxy (not reified)
# Second access - should also fail (retry the import)
try:
x = test.test_lazy_import.data.broken_module
- except ValueError:
+ except AttributeError:
print("OK - retry worked")
""")
result = subprocess.run(
def test_error_during_module_execution_propagates(self):
"""Errors in module code during reification should propagate correctly."""
- # Module that raises during import should propagate with chaining
code = textwrap.dedent("""
import sys
lazy import test.test_lazy_import.data.broken_module
try:
_ = test.test_lazy_import.data.broken_module
print("FAIL - should have raised")
- except ValueError as e:
- # The ValueError from the module should be the cause
- if "always fails" in str(e) or (e.__cause__ and "always fails" in str(e.__cause__)):
- print("OK")
- else:
- print(f"FAIL - wrong error: {e}")
+ except AttributeError:
+ print("OK")
""")
result = subprocess.run(
[sys.executable, "-c", code],
return final_mod;
}
-static PyObject *
-get_mod_dict(PyObject *module)
-{
- if (PyModule_Check(module)) {
- return Py_NewRef(_PyModule_GetDict(module));
- }
-
- return PyObject_GetAttr(module, &_Py_ID(__dict__));
-}
-
// ensure we have the set for the parent module name in sys.lazy_modules.
// Returns a new reference.
static PyObject *
return lazy_submodules;
}
-// Ensures that we have a LazyImportObject on the parent module for
-// all children modules which have been lazily imported. If the parent
-// module overrides the child attribute then the value is not replaced.
+// Records all parent-child relationships in lazy_pending_submodules
+// for a lazily imported module name. When a parent module's attribute
+// is accessed, _Py_module_getattro_impl will check lazy_pending_submodules
+// and trigger the import.
static int
-register_lazy_on_parent(PyThreadState *tstate, PyObject *name,
- PyObject *builtins)
+register_lazy_on_parent(PyThreadState *tstate, PyObject *name)
{
int ret = -1;
PyObject *parent = NULL;
PyObject *child = NULL;
- PyObject *parent_module = NULL;
- PyObject *parent_dict = NULL;
PyInterpreterState *interp = tstate->interp;
PyObject *lazy_pending_submodules = LAZY_PENDING_SUBMODULES(interp);
goto done;
}
parent = PyUnicode_Substring(name, 0, dot);
- // If `parent` is NULL then this has hit the end of the import, no
- // more "parent.child" in the import name. The entire import will be
- // resolved lazily.
if (parent == NULL) {
goto done;
}
goto done;
}
- // Record the child as being lazily imported from the parent.
PyObject *lazy_submodules = ensure_lazy_pending_submodules(
(PyDictObject *)lazy_pending_submodules, parent);
if (lazy_submodules == NULL) {
}
Py_DECREF(lazy_submodules);
- // Add the lazy import for the child to the parent.
- Py_XSETREF(parent_module, PyImport_GetModule(parent));
- if (parent_module != NULL) {
- Py_XSETREF(parent_dict, get_mod_dict(parent_module));
- if (parent_dict == NULL) {
- goto done;
- }
- if (PyDict_CheckExact(parent_dict)) {
- int contains = PyDict_Contains(parent_dict, child);
- if (contains < 0) {
- goto done;
- }
- if (!contains) {
- PyObject *lazy_module_attr = _PyLazyImport_New(
- tstate->current_frame, builtins, parent, child
- );
- if (lazy_module_attr == NULL) {
- goto done;
- }
- if (PyDict_SetItem(parent_dict, child,
- lazy_module_attr) < 0) {
- Py_DECREF(lazy_module_attr);
- goto done;
- }
- Py_DECREF(lazy_module_attr);
- }
- }
- ret = 0;
- goto done;
- }
-
Py_SETREF(name, parent);
parent = NULL;
}
done:
- Py_XDECREF(parent_dict);
- Py_XDECREF(parent_module);
Py_XDECREF(child);
Py_XDECREF(parent);
Py_XDECREF(name);
static int
register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name,
- PyObject *from, PyObject *builtins)
+ PyObject *from)
{
PyObject *fromname = PyUnicode_FromFormat("%U.%U", abs_name, from);
if (fromname == NULL) {
return -1;
}
- int res = register_lazy_on_parent(tstate, fromname, builtins);
+ int res = register_lazy_on_parent(tstate, fromname);
Py_DECREF(fromname);
return res;
}
+PyObject *
+_PyImport_TryLoadLazySubmodule(PyObject *mod_name, PyObject *attr_name)
+{
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+ PyObject *lazy_pending = LAZY_PENDING_SUBMODULES(interp);
+ if (lazy_pending == NULL) {
+ return NULL;
+ }
+
+ PyObject *pending_set;
+ int rc = PyDict_GetItemRef(lazy_pending, mod_name, &pending_set);
+ if (rc <= 0) {
+ return NULL;
+ }
+
+ int contains = PySet_Contains(pending_set, attr_name);
+ if (contains <= 0) {
+ Py_DECREF(pending_set);
+ return NULL;
+ }
+
+ PyObject *full_name = PyUnicode_FromFormat("%U.%U", mod_name, attr_name);
+ if (full_name == NULL) {
+ Py_DECREF(pending_set);
+ return NULL;
+ }
+
+ PyObject *mod = PyImport_ImportModuleLevelObject(
+ full_name, NULL, NULL, NULL, 0);
+ if (mod == NULL) {
+ Py_DECREF(pending_set);
+ Py_DECREF(full_name);
+ return NULL;
+ }
+ Py_DECREF(mod);
+
+ if (PySet_Discard(pending_set, attr_name) < 0) {
+ Py_DECREF(pending_set);
+ Py_DECREF(full_name);
+ return NULL;
+ }
+ Py_DECREF(pending_set);
+
+ PyObject *submod = PyImport_GetModule(full_name);
+ Py_DECREF(full_name);
+ return submod;
+}
+
PyObject *
_PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
PyObject *name, PyObject *builtins,
}
if (fromlist && PyUnicode_Check(fromlist)) {
- if (register_from_lazy_on_parent(tstate, abs_name, fromlist,
- builtins) < 0) {
+ if (register_from_lazy_on_parent(tstate, abs_name, fromlist) < 0) {
goto error;
}
}
PyTuple_GET_SIZE(fromlist)) {
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(fromlist); i++) {
if (register_from_lazy_on_parent(tstate, abs_name,
- PyTuple_GET_ITEM(fromlist, i),
- builtins) < 0)
+ PyTuple_GET_ITEM(fromlist, i)) < 0)
{
goto error;
}
}
}
- else if (register_lazy_on_parent(tstate, abs_name, builtins) < 0) {
+ else if (register_lazy_on_parent(tstate, abs_name) < 0) {
goto error;
}
return PyBytes_FromStringAndSize(hash.data, sizeof(hash.data));
}
-static int
-publish_lazy_imports_on_module(PyThreadState *tstate,
- PyObject *lazy_submodules,
- PyObject *name,
- PyObject *module_dict)
-{
- PyObject *builtins = _PyEval_GetBuiltins(tstate);
- PyObject *attr_name;
- Py_ssize_t pos = 0;
- Py_hash_t hash;
-
- // Enumerate the set of lazy submodules which have been imported from the
- // parent module.
- while (_PySet_NextEntryRef(lazy_submodules, &pos, &attr_name, &hash)) {
- if (_PyDict_Contains_KnownHash(module_dict, attr_name, hash)) {
- Py_DECREF(attr_name);
- continue;
- }
- // Create a new lazy module attr for the subpackage which was
- // previously lazily imported.
- PyObject *lazy_module_attr = _PyLazyImport_New(tstate->current_frame, builtins,
- name, attr_name);
- if (lazy_module_attr == NULL) {
- Py_DECREF(attr_name);
- return -1;
- }
-
- // Publish on the module that was just imported.
- if (PyDict_SetItem(module_dict, attr_name,
- lazy_module_attr) < 0) {
- Py_DECREF(lazy_module_attr);
- Py_DECREF(attr_name);
- return -1;
- }
- Py_DECREF(lazy_module_attr);
- Py_DECREF(attr_name);
- }
- return 0;
-}
-
/*[clinic input]
_imp._set_lazy_attributes
modobj: object
PyObject *name)
/*[clinic end generated code: output=3369bb3242b1f043 input=38ea6f30956dd7d6]*/
{
- PyThreadState *tstate = _PyThreadState_GET();
- PyObject *module_dict = NULL;
- PyObject *ret = NULL;
- PyObject *lazy_pending_modules = LAZY_PENDING_SUBMODULES(tstate->interp);
- assert(lazy_pending_modules != NULL);
-
- PyObject *lazy_submodules;
- if (PySet_Discard(LAZY_MODULES(tstate->interp), name) < 0) {
- return NULL;
- } else if (PyDict_GetItemRef(lazy_pending_modules, name, &lazy_submodules) < 0) {
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+ if (PySet_Discard(LAZY_MODULES(interp), name) < 0) {
return NULL;
}
- else if (lazy_submodules == NULL) {
- Py_RETURN_NONE;
- }
-
- module_dict = get_mod_dict(modobj);
- if (module_dict == NULL || !PyDict_CheckExact(module_dict)) {
- Py_DECREF(lazy_submodules);
- goto done;
- }
-
- assert(PyAnySet_CheckExact(lazy_submodules));
- Py_BEGIN_CRITICAL_SECTION(lazy_submodules);
- publish_lazy_imports_on_module(tstate, lazy_submodules, name, module_dict);
- Py_END_CRITICAL_SECTION();
- Py_DECREF(lazy_submodules);
-
- if (PyDict_DelItem(lazy_pending_modules, name) < 0) {
- goto error;
- }
-
-done:
- ret = Py_NewRef(Py_None);
-
-error:
- Py_XDECREF(module_dict);
- return ret;
+ Py_RETURN_NONE;
}
PyDoc_STRVAR(doc_imp,