Skip to content

Commit 94c8bac

Browse files
authored
[3.15] gh-148587: Make sys.lazy_modules match PEP and keep internal lazy submodules tra… (#150014)
Make sys.lazy_modules match PEP and keep internal lazy submodules tracking internal
1 parent 66ade28 commit 94c8bac

5 files changed

Lines changed: 62 additions & 31 deletions

File tree

Include/internal/pycore_interp_structs.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,15 @@ struct _import_state {
349349
int lazy_imports_mode;
350350
PyObject *lazy_imports_filter;
351351
PyObject *lazy_importing_modules;
352+
// The set stored in sys.lazy_modules if values that have been
353+
// lazily imported. This value is only for debugging/introspection
354+
// purposes and is not used by the runtime.
352355
PyObject *lazy_modules;
356+
// A dict mapping package names to a set of submodule names that
357+
// have been imported lazily from packages which have been imported
358+
// lazily. When the package is reified we need to add a
359+
// LazyImportObject which refers to the submodule on the module.
360+
PyObject *lazy_pending_submodules;
353361
#ifdef Py_GIL_DISABLED
354362
PyMutex lazy_mutex;
355363
#endif

Lib/test/test_lazy_import/__init__.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ def test_basic_unused(self):
3838
"""Lazy imported module should not be loaded if never accessed."""
3939
import test.test_lazy_import.data.basic_unused
4040
self.assertNotIn("test.test_lazy_import.data.basic2", sys.modules)
41-
self.assertIn("test.test_lazy_import.data", sys.lazy_modules)
42-
self.assertEqual(sys.lazy_modules["test.test_lazy_import.data"], {"basic2"})
41+
self.assertIn("test.test_lazy_import.data.basic2", sys.lazy_modules)
4342

4443
def test_sys_lazy_modules(self):
4544
try:
@@ -49,7 +48,7 @@ def test_sys_lazy_modules(self):
4948

5049
self.assertFalse("test.test_lazy_import.data.basic2" in sys.modules)
5150
self.assertIn("test.test_lazy_import.data", sys.lazy_modules)
52-
self.assertEqual(sys.lazy_modules["test.test_lazy_import.data"], {"basic2"})
51+
self.assertIn("test.test_lazy_import.data.basic2", sys.lazy_modules)
5352
test.test_lazy_import.data.basic_from_unused.basic2
5453
self.assertNotIn("test.test_import.data", sys.lazy_modules)
5554

@@ -574,8 +573,8 @@ def my_filter(name):
574573
self.assertIs(sys.get_lazy_imports_filter(), my_filter)
575574

576575
def test_lazy_modules_attribute_is_dict(self):
577-
"""sys.lazy_modules should be a dict per PEP 810."""
578-
self.assertIsInstance(sys.lazy_modules, dict)
576+
"""sys.lazy_modules should be a set per PEP 810."""
577+
self.assertIsInstance(sys.lazy_modules, set)
579578

580579
@support.requires_subprocess()
581580
def test_lazy_modules_tracks_lazy_imports(self):
@@ -584,8 +583,7 @@ def test_lazy_modules_tracks_lazy_imports(self):
584583
import sys
585584
initial_count = len(sys.lazy_modules)
586585
import test.test_lazy_import.data.basic_unused
587-
assert "test.test_lazy_import.data" in sys.lazy_modules
588-
assert sys.lazy_modules["test.test_lazy_import.data"] == {"basic2"}
586+
assert "test.test_lazy_import.data.basic2" in sys.lazy_modules
589587
assert len(sys.lazy_modules) > initial_count
590588
print("OK")
591589
""")
@@ -1034,15 +1032,14 @@ def test_module_added_to_lazy_modules_on_lazy_import(self):
10341032
lazy import test.test_lazy_import.data.basic2
10351033
10361034
# Should be in lazy_modules after lazy import
1037-
assert "test.test_lazy_import.data" in sys.lazy_modules
1038-
assert sys.lazy_modules["test.test_lazy_import.data"] == {"basic2"}
1035+
assert "test.test_lazy_import.data.basic2" in sys.lazy_modules
10391036
assert len(sys.lazy_modules) > initial_count
10401037
10411038
# Trigger reification
10421039
_ = test.test_lazy_import.data.basic2.x
10431040
10441041
# Module should still be tracked (for diagnostics per PEP 810)
1045-
assert "test.test_lazy_import.data" not in sys.lazy_modules
1042+
assert "test.test_lazy_import.data.basic2" not in sys.lazy_modules
10461043
print("OK")
10471044
""")
10481045
result = subprocess.run(
@@ -1055,8 +1052,8 @@ def test_module_added_to_lazy_modules_on_lazy_import(self):
10551052

10561053
def test_lazy_modules_is_per_interpreter(self):
10571054
"""Each interpreter should have independent sys.lazy_modules."""
1058-
# Basic test that sys.lazy_modules exists and is a dict
1059-
self.assertIsInstance(sys.lazy_modules, dict)
1055+
# Basic test that sys.lazy_modules exists and is a set
1056+
self.assertIsInstance(sys.lazy_modules, set)
10601057

10611058
def test_lazy_module_without_children_is_tracked(self):
10621059
code = textwrap.dedent("""
@@ -1065,10 +1062,6 @@ def test_lazy_module_without_children_is_tracked(self):
10651062
assert "json" in sys.lazy_modules, (
10661063
f"expected 'json' in sys.lazy_modules, got {set(sys.lazy_modules)}"
10671064
)
1068-
assert sys.lazy_modules["json"] == set(), (
1069-
f"expected empty set for sys.lazy_modules['json'], "
1070-
f"got {sys.lazy_modules['json']!r}"
1071-
)
10721065
print("OK")
10731066
""")
10741067
assert_python_ok("-c", code)
@@ -1937,7 +1930,7 @@ def create_lazy_imports(idx):
19371930
t.join()
19381931
19391932
assert not errors, f"Errors: {errors}"
1940-
assert isinstance(sys.lazy_modules, dict), "sys.lazy_modules is not a dict"
1933+
assert isinstance(sys.lazy_modules, set), "sys.lazy_modules is not a dict"
19411934
print("OK")
19421935
""")
19431936

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import unittest
2+
3+
unittest.main('test.test_lazy_import')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``sys.lazy_modules`` is now a set instead of a dict as initially spelled out in PEP 810.

Python/import.c

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ static struct _inittab *inittab_copy = NULL;
9494
(interp)->imports.modules_by_index
9595
#define LAZY_MODULES(interp) \
9696
(interp)->imports.lazy_modules
97+
#define LAZY_PENDING_SUBMODULES(interp) \
98+
(interp)->imports.lazy_pending_submodules
9799
#define IMPORTLIB(interp) \
98100
(interp)->imports.importlib
99101
#define OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) \
@@ -271,15 +273,19 @@ import_get_module(PyThreadState *tstate, PyObject *name)
271273
PyObject *
272274
_PyImport_InitLazyModules(PyInterpreterState *interp)
273275
{
274-
assert(LAZY_MODULES(interp) == NULL);
275-
LAZY_MODULES(interp) = PyDict_New();
276+
assert(LAZY_MODULES(interp) == NULL &&
277+
LAZY_PENDING_SUBMODULES(interp) == NULL);
278+
279+
LAZY_PENDING_SUBMODULES(interp) = PyDict_New();
280+
LAZY_MODULES(interp) = PySet_New(0);
276281
return LAZY_MODULES(interp);
277282
}
278283

279284
void
280285
_PyImport_ClearLazyModules(PyInterpreterState *interp)
281286
{
282287
Py_CLEAR(LAZY_MODULES(interp));
288+
Py_CLEAR(LAZY_PENDING_SUBMODULES(interp));
283289
}
284290

285291
static int
@@ -4339,7 +4345,7 @@ get_mod_dict(PyObject *module)
43394345
// ensure we have the set for the parent module name in sys.lazy_modules.
43404346
// Returns a new reference.
43414347
static PyObject *
4342-
ensure_lazy_submodules(PyDictObject *lazy_modules, PyObject *parent)
4348+
ensure_lazy_pending_submodules(PyDictObject *lazy_modules, PyObject *parent)
43434349
{
43444350
PyObject *lazy_submodules;
43454351
Py_BEGIN_CRITICAL_SECTION(lazy_modules);
@@ -4358,6 +4364,9 @@ ensure_lazy_submodules(PyDictObject *lazy_modules, PyObject *parent)
43584364
return lazy_submodules;
43594365
}
43604366

4367+
// Ensures that we have a LazyImportObject on the parent module for
4368+
// all children modules which have been lazily imported. If the parent
4369+
// module overrides the child attribute then the value is not replaced.
43614370
static int
43624371
register_lazy_on_parent(PyThreadState *tstate, PyObject *name,
43634372
PyObject *builtins)
@@ -4369,16 +4378,16 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name,
43694378
PyObject *parent_dict = NULL;
43704379

43714380
PyInterpreterState *interp = tstate->interp;
4372-
PyObject *lazy_modules = LAZY_MODULES(interp);
4373-
assert(lazy_modules != NULL);
4381+
PyObject *lazy_pending_submodules = LAZY_PENDING_SUBMODULES(interp);
4382+
assert(lazy_pending_submodules != NULL);
43744383

43754384
Py_INCREF(name);
43764385
while (true) {
43774386
Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0,
43784387
PyUnicode_GET_LENGTH(name), -1);
43794388
if (dot < 0) {
4380-
PyObject *lazy_submodules = ensure_lazy_submodules(
4381-
(PyDictObject *)lazy_modules, name);
4389+
PyObject *lazy_submodules = ensure_lazy_pending_submodules(
4390+
(PyDictObject *)lazy_pending_submodules, name);
43824391
if (lazy_submodules == NULL) {
43834392
goto done;
43844393
}
@@ -4400,8 +4409,8 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name,
44004409
}
44014410

44024411
// Record the child as being lazily imported from the parent.
4403-
PyObject *lazy_submodules = ensure_lazy_submodules(
4404-
(PyDictObject *)lazy_modules, parent);
4412+
PyObject *lazy_submodules = ensure_lazy_pending_submodules(
4413+
(PyDictObject *)lazy_pending_submodules, parent);
44054414
if (lazy_submodules == NULL) {
44064415
goto done;
44074416
}
@@ -4464,6 +4473,14 @@ register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name,
44644473
if (fromname == NULL) {
44654474
return -1;
44664475
}
4476+
4477+
// Add the module name to sys.lazy_modules set (PEP 810).
4478+
PyObject *lazy_modules = LAZY_MODULES(tstate->interp);
4479+
if (PySet_Add(lazy_modules, fromname) < 0) {
4480+
Py_DECREF(fromname);
4481+
return -1;
4482+
}
4483+
44674484
int res = register_lazy_on_parent(tstate, fromname, builtins);
44684485
Py_DECREF(fromname);
44694486
return res;
@@ -4555,6 +4572,13 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
45554572
Py_DECREF(abs_name);
45564573
return NULL;
45574574
}
4575+
4576+
// Add the module name to sys.lazy_modules set (PEP 810).
4577+
PyObject *lazy_modules = LAZY_MODULES(tstate->interp);
4578+
if (PySet_Add(lazy_modules, abs_name) < 0) {
4579+
goto error;
4580+
}
4581+
45584582
if (fromlist && PyUnicode_Check(fromlist)) {
45594583
if (register_from_lazy_on_parent(tstate, abs_name, fromlist,
45604584
builtins) < 0) {
@@ -4791,6 +4815,7 @@ _PyImport_ClearCore(PyInterpreterState *interp)
47914815
Py_CLEAR(IMPORTLIB(interp));
47924816
Py_CLEAR(IMPORT_FUNC(interp));
47934817
Py_CLEAR(LAZY_IMPORT_FUNC(interp));
4818+
Py_CLEAR(interp->imports.lazy_pending_submodules);
47944819
Py_CLEAR(interp->imports.lazy_modules);
47954820
Py_CLEAR(interp->imports.lazy_importing_modules);
47964821
Py_CLEAR(interp->imports.lazy_imports_filter);
@@ -5636,11 +5661,13 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
56365661
PyThreadState *tstate = _PyThreadState_GET();
56375662
PyObject *module_dict = NULL;
56385663
PyObject *ret = NULL;
5639-
PyObject *lazy_modules = LAZY_MODULES(tstate->interp);
5640-
assert(lazy_modules != NULL);
5664+
PyObject *lazy_pending_modules = LAZY_PENDING_SUBMODULES(tstate->interp);
5665+
assert(lazy_pending_modules != NULL);
56415666

56425667
PyObject *lazy_submodules;
5643-
if (PyDict_GetItemRef(lazy_modules, name, &lazy_submodules) < 0) {
5668+
if (PySet_Discard(LAZY_MODULES(tstate->interp), name) < 0) {
5669+
return NULL;
5670+
} else if (PyDict_GetItemRef(lazy_pending_modules, name, &lazy_submodules) < 0) {
56445671
return NULL;
56455672
}
56465673
else if (lazy_submodules == NULL) {
@@ -5659,8 +5686,7 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
56595686
Py_END_CRITICAL_SECTION();
56605687
Py_DECREF(lazy_submodules);
56615688

5662-
// once a module is imported it is removed from sys.lazy_modules
5663-
if (PyDict_DelItem(lazy_modules, name) < 0) {
5689+
if (PyDict_DelItem(lazy_pending_modules, name) < 0) {
56645690
goto error;
56655691
}
56665692

0 commit comments

Comments
 (0)