Skip to content

Commit b347b1b

Browse files
committed
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.
1 parent d986124 commit b347b1b

6 files changed

Lines changed: 129 additions & 3 deletions

File tree

Include/internal/pycore_import.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ extern int _PyImport_FixupBuiltin(
3232
PyObject *modules
3333
);
3434

35+
extern int _PyImport_EnsureSubmoduleRegistered(
36+
PyThreadState *tstate,
37+
PyObject *parent,
38+
PyObject *name,
39+
PyObject *submodule);
40+
3541
extern PyObject * _PyImport_ResolveName(
3642
PyThreadState *tstate, PyObject *name, PyObject *globals, int level);
3743
extern PyObject * _PyImport_GetAbsName(

Lib/test/test_lazy_import/__init__.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,44 @@ def test_lazy_import_pkg_cross_import(self):
469469
self.assertEqual(type(g["x"]), int)
470470
self.assertEqual(type(g["b"]), types.LazyImportType)
471471

472+
def test_lazy_reimport_after_sys_modules_delete(self):
473+
"""gh-151408: re-resolving a lazy import restores sys.modules."""
474+
modname = "test.test_lazy_import.data.pkg.bar"
475+
import test.test_lazy_import.data.pkg.bar
476+
del sys.modules[modname]
477+
478+
exec(
479+
"lazy from test.test_lazy_import.data.pkg import bar\nbar.f()",
480+
globals(),
481+
)
482+
self.assertIn(modname, sys.modules)
483+
484+
def test_lazy_import_as_reimport_after_sys_modules_delete(self):
485+
"""gh-151408: lazy import with alias restores sys.modules."""
486+
modname = "test.test_lazy_import.data.pkg.bar"
487+
import test.test_lazy_import.data.pkg.bar
488+
del sys.modules[modname]
489+
490+
ns = {}
491+
exec(
492+
"lazy import test.test_lazy_import.data.pkg.bar as bar\nbar.f()",
493+
ns,
494+
)
495+
self.assertIn(modname, sys.modules)
496+
497+
def test_lazy_import_dotted_reimport_after_sys_modules_delete(self):
498+
"""gh-151408: dotted lazy import access restores sys.modules."""
499+
modname = "test.test_lazy_import.data.pkg.bar"
500+
import test.test_lazy_import.data.pkg.bar
501+
del sys.modules[modname]
502+
503+
exec(
504+
"lazy import test.test_lazy_import.data.pkg.bar\n"
505+
"test.test_lazy_import.data.pkg.bar.f()",
506+
globals(),
507+
)
508+
self.assertIn(modname, sys.modules)
509+
472510
@support.requires_subprocess()
473511
def test_lazy_from_import_does_not_pollute_parent(self):
474512
"""Lazy from import should not add the name to the parent module's dict."""
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix lazy import reification to restore submodules removed from
2+
:mod:`sys.modules` but still cached on the parent package.

Objects/moduleobject.c

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,6 +1326,20 @@ try_load_lazy_submodule(PyModuleObject *m, PyObject *name)
13261326
return result;
13271327
}
13281328

1329+
static PyObject *
1330+
ensure_submodule_and_return(PyModuleObject *m, PyObject *name, PyObject *attr)
1331+
{
1332+
if (attr != NULL && PyModule_Check(attr)) {
1333+
if (_PyImport_EnsureSubmoduleRegistered(
1334+
PyThreadState_GET(), (PyObject *)m, name, attr) < 0)
1335+
{
1336+
Py_DECREF(attr);
1337+
return NULL;
1338+
}
1339+
}
1340+
return attr;
1341+
}
1342+
13291343
PyObject*
13301344
_Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
13311345
{
@@ -1372,9 +1386,9 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
13721386
Py_CLEAR(new_value);
13731387
}
13741388
Py_DECREF(attr);
1375-
return new_value;
1389+
return ensure_submodule_and_return(m, name, new_value);
13761390
}
1377-
return attr;
1391+
return ensure_submodule_and_return(m, name, attr);
13781392
}
13791393
if (suppress == 1) {
13801394
if (PyErr_Occurred()) {
@@ -1392,7 +1406,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
13921406
assert(m->md_dict != NULL);
13931407
attr = try_load_lazy_submodule(m, name);
13941408
if (attr != NULL) {
1395-
return attr;
1409+
return ensure_submodule_and_return(m, name, attr);
13961410
}
13971411
if (PyErr_Occurred()) {
13981412
return NULL;

Python/ceval.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3130,6 +3130,12 @@ _PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name)
31303130
PyObject *fullmodname, *mod_name, *origin, *mod_name_or_unknown, *errmsg, *spec;
31313131

31323132
if (PyObject_GetOptionalAttr(v, name, &x) != 0) {
3133+
if (x != NULL &&
3134+
_PyImport_EnsureSubmoduleRegistered(tstate, v, name, x) < 0)
3135+
{
3136+
Py_DECREF(x);
3137+
return NULL;
3138+
}
31333139
return x;
31343140
}
31353141
/* Issue #17636: in case this failed because of a circular relative
@@ -3311,6 +3317,13 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
33113317
return NULL;
33123318
}
33133319
if (ret != NULL) {
3320+
if (_PyImport_EnsureSubmoduleRegistered(
3321+
tstate, mod, name, ret) < 0)
3322+
{
3323+
Py_DECREF(ret);
3324+
Py_DECREF(mod);
3325+
return NULL;
3326+
}
33143327
Py_DECREF(mod);
33153328
return ret;
33163329
}

Python/import.c

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,59 @@ _PyImport_SetModuleString(const char *name, PyObject *m)
255255
return PyMapping_SetItemString(modules, name, m);
256256
}
257257

258+
int
259+
_PyImport_EnsureSubmoduleRegistered(PyThreadState *tstate, PyObject *parent,
260+
PyObject *name, PyObject *submodule)
261+
{
262+
if (!PyModule_Check(submodule) || !PyModule_Check(parent)) {
263+
return 0;
264+
}
265+
266+
PyObject *parent_name;
267+
if (PyObject_GetOptionalAttr(parent, &_Py_ID(__name__), &parent_name) < 0) {
268+
return -1;
269+
}
270+
if (parent_name == NULL || !PyUnicode_Check(parent_name)) {
271+
Py_XDECREF(parent_name);
272+
return 0;
273+
}
274+
275+
PyObject *full_name = PyUnicode_FromFormat("%U.%U", parent_name, name);
276+
Py_DECREF(parent_name);
277+
if (full_name == NULL) {
278+
return -1;
279+
}
280+
281+
PyObject *sub_name;
282+
if (PyObject_GetOptionalAttr(submodule, &_Py_ID(__name__), &sub_name) < 0) {
283+
Py_DECREF(full_name);
284+
return -1;
285+
}
286+
if (sub_name == NULL || !PyUnicode_Check(sub_name) ||
287+
PyUnicode_Compare(sub_name, full_name) != 0)
288+
{
289+
Py_XDECREF(sub_name);
290+
Py_DECREF(full_name);
291+
return 0;
292+
}
293+
Py_DECREF(sub_name);
294+
295+
PyObject *existing = PyImport_GetModule(full_name);
296+
if (existing != NULL) {
297+
Py_DECREF(existing);
298+
Py_DECREF(full_name);
299+
return 0;
300+
}
301+
if (_PyErr_Occurred(tstate)) {
302+
Py_DECREF(full_name);
303+
return -1;
304+
}
305+
306+
int res = _PyImport_SetModule(full_name, submodule);
307+
Py_DECREF(full_name);
308+
return res;
309+
}
310+
258311
static PyObject *
259312
import_get_module(PyThreadState *tstate, PyObject *name)
260313
{

0 commit comments

Comments
 (0)