diff --git a/Include/internal/pycore_import.h b/Include/internal/pycore_import.h index a1078828afa572e..2bf01c8cbf90f1b 100644 --- a/Include/internal/pycore_import.h +++ b/Include/internal/pycore_import.h @@ -32,6 +32,12 @@ extern int _PyImport_FixupBuiltin( PyObject *modules ); +extern int _PyImport_EnsureSubmoduleRegistered( + PyThreadState *tstate, + PyObject *parent, + PyObject *name, + PyObject *submodule); + extern PyObject * _PyImport_ResolveName( PyThreadState *tstate, PyObject *name, PyObject *globals, int level); extern PyObject * _PyImport_GetAbsName( diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 1724beb8ce69517..789241c66a4fbe1 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -469,6 +469,44 @@ def test_lazy_import_pkg_cross_import(self): self.assertEqual(type(g["x"]), int) self.assertEqual(type(g["b"]), types.LazyImportType) + def test_lazy_reimport_after_sys_modules_delete(self): + """gh-151408: re-resolving a lazy import restores sys.modules.""" + modname = "test.test_lazy_import.data.pkg.bar" + import test.test_lazy_import.data.pkg.bar + del sys.modules[modname] + + exec( + "lazy from test.test_lazy_import.data.pkg import bar\nbar.f()", + globals(), + ) + self.assertIn(modname, sys.modules) + + def test_lazy_import_as_reimport_after_sys_modules_delete(self): + """gh-151408: lazy import with alias restores sys.modules.""" + modname = "test.test_lazy_import.data.pkg.bar" + import test.test_lazy_import.data.pkg.bar + del sys.modules[modname] + + ns = {} + exec( + "lazy import test.test_lazy_import.data.pkg.bar as bar\nbar.f()", + ns, + ) + self.assertIn(modname, sys.modules) + + def test_lazy_import_dotted_reimport_after_sys_modules_delete(self): + """gh-151408: dotted lazy import access restores sys.modules.""" + modname = "test.test_lazy_import.data.pkg.bar" + import test.test_lazy_import.data.pkg.bar + del sys.modules[modname] + + exec( + "lazy import test.test_lazy_import.data.pkg.bar\n" + "test.test_lazy_import.data.pkg.bar.f()", + globals(), + ) + self.assertIn(modname, sys.modules) + @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.""" diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-12-21-15-00.gh-issue-151408.pR3kLm.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-12-21-15-00.gh-issue-151408.pR3kLm.rst new file mode 100644 index 000000000000000..f153c9c74955d7e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-12-21-15-00.gh-issue-151408.pR3kLm.rst @@ -0,0 +1,2 @@ +Fix lazy import reification to restore submodules removed from +:mod:`sys.modules` but still cached on the parent package. diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index f447403ef31b43a..369be5deba5c8f7 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1326,6 +1326,20 @@ try_load_lazy_submodule(PyModuleObject *m, PyObject *name) return result; } +static PyObject * +ensure_submodule_and_return(PyModuleObject *m, PyObject *name, PyObject *attr) +{ + if (attr != NULL && PyModule_Check(attr)) { + if (_PyImport_EnsureSubmoduleRegistered( + PyThreadState_GET(), (PyObject *)m, name, attr) < 0) + { + Py_DECREF(attr); + return NULL; + } + } + return attr; +} + PyObject* _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) { @@ -1372,9 +1386,9 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) Py_CLEAR(new_value); } Py_DECREF(attr); - return new_value; + return ensure_submodule_and_return(m, name, new_value); } - return attr; + return ensure_submodule_and_return(m, name, attr); } if (suppress == 1) { if (PyErr_Occurred()) { @@ -1392,7 +1406,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) assert(m->md_dict != NULL); attr = try_load_lazy_submodule(m, name); if (attr != NULL) { - return attr; + return ensure_submodule_and_return(m, name, attr); } if (PyErr_Occurred()) { return NULL; diff --git a/Python/ceval.c b/Python/ceval.c index a9b31affca9890a..82176d38e893270 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3130,6 +3130,12 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name) PyObject *fullmodname, *mod_name, *origin, *mod_name_or_unknown, *errmsg, *spec; if (PyObject_GetOptionalAttr(v, name, &x) != 0) { + if (x != NULL && + _PyImport_EnsureSubmoduleRegistered(tstate, v, name, x) < 0) + { + Py_DECREF(x); + return NULL; + } return x; } /* Issue #17636: in case this failed because of a circular relative @@ -3311,6 +3317,13 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje return NULL; } if (ret != NULL) { + if (_PyImport_EnsureSubmoduleRegistered( + tstate, mod, name, ret) < 0) + { + Py_DECREF(ret); + Py_DECREF(mod); + return NULL; + } Py_DECREF(mod); return ret; } diff --git a/Python/import.c b/Python/import.c index 42bfe15121f84f7..8876fbade8f5d65 100644 --- a/Python/import.c +++ b/Python/import.c @@ -255,6 +255,59 @@ _PyImport_SetModuleString(const char *name, PyObject *m) return PyMapping_SetItemString(modules, name, m); } +int +_PyImport_EnsureSubmoduleRegistered(PyThreadState *tstate, PyObject *parent, + PyObject *name, PyObject *submodule) +{ + if (!PyModule_Check(submodule) || !PyModule_Check(parent)) { + return 0; + } + + PyObject *parent_name; + if (PyObject_GetOptionalAttr(parent, &_Py_ID(__name__), &parent_name) < 0) { + return -1; + } + if (parent_name == NULL || !PyUnicode_Check(parent_name)) { + Py_XDECREF(parent_name); + return 0; + } + + PyObject *full_name = PyUnicode_FromFormat("%U.%U", parent_name, name); + Py_DECREF(parent_name); + if (full_name == NULL) { + return -1; + } + + PyObject *sub_name; + if (PyObject_GetOptionalAttr(submodule, &_Py_ID(__name__), &sub_name) < 0) { + Py_DECREF(full_name); + return -1; + } + if (sub_name == NULL || !PyUnicode_Check(sub_name) || + PyUnicode_Compare(sub_name, full_name) != 0) + { + Py_XDECREF(sub_name); + Py_DECREF(full_name); + return 0; + } + Py_DECREF(sub_name); + + PyObject *existing = PyImport_GetModule(full_name); + if (existing != NULL) { + Py_DECREF(existing); + Py_DECREF(full_name); + return 0; + } + if (_PyErr_Occurred(tstate)) { + Py_DECREF(full_name); + return -1; + } + + int res = _PyImport_SetModule(full_name, submodule); + Py_DECREF(full_name); + return res; +} + static PyObject * import_get_module(PyThreadState *tstate, PyObject *name) {