From b347b1b897451e9b4643922fe993c0577980ea62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cbhuvi27=E2=80=9D?= Date: Fri, 12 Jun 2026 21:16:17 +0530 Subject: [PATCH] gh-151408: Re-register submodules in sys.modules on lazy re-import When a submodule is removed from sys.modules but still cached on its parent package, lazy import reification returned the stale module without restoring sys.modules. Register the module again when import or attribute lookup resolves a submodule whose __name__ matches. --- Include/internal/pycore_import.h | 6 +++ Lib/test/test_lazy_import/__init__.py | 38 +++++++++++++ ...-06-12-21-15-00.gh-issue-151408.pR3kLm.rst | 2 + Objects/moduleobject.c | 20 +++++-- Python/ceval.c | 13 +++++ Python/import.c | 53 +++++++++++++++++++ 6 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-12-21-15-00.gh-issue-151408.pR3kLm.rst 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) {