diff --git a/Include/internal/pycore_import.h b/Include/internal/pycore_import.h index a1078828afa572e..d0c166a88426f67 100644 --- a/Include/internal/pycore_import.h +++ b/Include/internal/pycore_import.h @@ -39,6 +39,9 @@ extern PyObject * _PyImport_GetAbsName( // Symbol is exported for the JIT on Windows builds. PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate( PyThreadState *tstate, PyObject *lazy_import); +extern PyObject * _PyImport_ResolveLazyImportFromAttr( + PyThreadState *tstate, PyObject *package_name, + PyObject *name, PyObject *attr); extern PyObject * _PyImport_TryLoadLazySubmodule( PyObject *mod_name, PyObject *attr_name); extern PyObject * _PyImport_LazyImportModuleLevelObject( diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 1724beb8ce69517..7e62391fa9898cd 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -545,6 +545,36 @@ def test_package_from_import_with_module_getattr(self): """) assert_python_ok("-c", code) + @support.requires_subprocess() + def test_lazy_import_reimports_submodule_evicted_from_sys_modules(self): + """A submodule evicted from sys.modules but still cached on its parent + must be re-imported, like eager 'import a.b.c as t' is.""" + code = textwrap.dedent(""" + import sys + modname = "test.test_lazy_import.data.pkg.bar" + import test.test_lazy_import.data.pkg.bar + del sys.modules[modname] + lazy import test.test_lazy_import.data.pkg.bar as t + t.f() + assert modname in sys.modules, modname + """) + assert_python_ok("-c", code) + + @support.requires_subprocess() + def test_lazy_from_import_reimports_submodule_evicted_from_sys_modules(self): + """Same as above for 'from a.b import c': the stale parent attribute + must not shadow a re-import after sys.modules eviction.""" + code = textwrap.dedent(""" + import sys + modname = "test.test_lazy_import.data.pkg.bar" + import test.test_lazy_import.data.pkg.bar + del sys.modules[modname] + lazy from test.test_lazy_import.data.pkg import bar + bar.f() + assert modname in sys.modules, modname + """) + assert_python_ok("-c", code) + class DunderLazyImportTests(LazyImportTestCase): """Tests for __lazy_import__ builtin function.""" diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-12-11-45-00.gh-issue-151408.4s9KpL.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-12-11-45-00.gh-issue-151408.4s9KpL.rst new file mode 100644 index 000000000000000..05dd32460c7c55e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-12-11-45-00.gh-issue-151408.4s9KpL.rst @@ -0,0 +1,2 @@ +Fix lazy submodule imports so package submodules removed from +:data:`sys.modules` are properly re-imported. diff --git a/Python/ceval.c b/Python/ceval.c index a9b31affca9890a..3b35d72f205c84f 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3301,8 +3301,8 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje PyLazyImportObject *d = (PyLazyImportObject *)v; PyObject *mod = PyImport_GetModule(d->lz_from); if (mod != NULL) { - // Check if the module already has the attribute, if so, resolve it - // eagerly. + /* If the parent is already imported, resolve any module-valued + attribute through the same stale-submodule check. */ if (PyModule_Check(mod)) { PyObject *mod_dict = PyModule_GetDict(mod); if (mod_dict != NULL) { @@ -3311,8 +3311,11 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje return NULL; } if (ret != NULL) { + PyObject *resolved = _PyImport_ResolveLazyImportFromAttr( + tstate, d->lz_from, name, ret); + Py_DECREF(ret); Py_DECREF(mod); - return ret; + return resolved; } } } diff --git a/Python/import.c b/Python/import.c index 82d15ad0683c190..dae83e9f741c96b 100644 --- a/Python/import.c +++ b/Python/import.c @@ -3885,6 +3885,76 @@ _PyImport_ResolveName(PyThreadState *tstate, PyObject *name, return resolve_name(tstate, name, globals, level); } +PyObject * +_PyImport_ResolveLazyImportFromAttr(PyThreadState *tstate, + PyObject *package_name, + PyObject *name, PyObject *attr) +{ + /* Lazy from-imports repair evicted package submodules without + changing eager from-import behavior. */ + if (!PyModule_Check(attr) || package_name == NULL + || !PyUnicode_Check(package_name)) { + return Py_NewRef(attr); + } + + PyObject *package = PyImport_GetModule(package_name); + if (package == NULL) { + if (_PyErr_Occurred(tstate)) { + return NULL; + } + return Py_NewRef(attr); + } + + int is_package = PyObject_HasAttrWithError(package, &_Py_ID(__path__)); + Py_DECREF(package); + if (is_package <= 0) { + if (is_package < 0) { + return NULL; + } + return Py_NewRef(attr); + } + + PyObject *fullmodname = PyUnicode_FromFormat("%U.%U", package_name, name); + if (fullmodname == NULL) { + return NULL; + } + + PyObject *attr_name; + if (PyObject_GetOptionalAttr(attr, &_Py_ID(__name__), &attr_name) < 0) { + Py_DECREF(fullmodname); + return NULL; + } + + int matches = (attr_name != NULL && PyUnicode_Check(attr_name)) + ? PyObject_RichCompareBool(attr_name, fullmodname, Py_EQ) + : 0; + Py_XDECREF(attr_name); + if (matches <= 0) { + Py_DECREF(fullmodname); + return matches < 0 ? NULL : Py_NewRef(attr); + } + + PyObject *submod = import_get_module(tstate, fullmodname); + if (submod == NULL && !_PyErr_Occurred(tstate)) { + PyObject *imported = PyImport_ImportModuleLevelObject( + fullmodname, NULL, NULL, NULL, 0); + if (imported == NULL) { + Py_DECREF(fullmodname); + return NULL; + } + Py_DECREF(imported); + submod = import_get_module(tstate, fullmodname); + } + Py_DECREF(fullmodname); + if (submod != NULL) { + return submod; + } + if (_PyErr_Occurred(tstate)) { + return NULL; + } + return Py_NewRef(attr); +} + PyObject * _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) { @@ -4004,7 +4074,20 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) if (lz->lz_attr != NULL && PyUnicode_Check(lz->lz_attr)) { PyObject *from = obj; - obj = _PyEval_ImportFrom(tstate, from, lz->lz_attr); + obj = NULL; + PyObject *attr; + if (PyObject_GetOptionalAttr(from, lz->lz_attr, &attr) < 0) { + Py_DECREF(from); + goto error; + } + if (attr != NULL) { + obj = _PyImport_ResolveLazyImportFromAttr( + tstate, lz->lz_from, lz->lz_attr, attr); + Py_DECREF(attr); + } + else { + obj = _PyEval_ImportFrom(tstate, from, lz->lz_attr); + } Py_DECREF(from); if (obj == NULL) { goto error;