Skip to content

Commit 1d4e965

Browse files
johnslavikDinoV
andauthored
gh-148587: Make sys.lazy_modules match PEP and keep internal lazy submodules tracking internal (#150086)
Make sys.lazy_modules match PEP and keep internal lazy submodules tracking internal Co-authored-by: Dino Viehland <dinoviehland@meta.com>
1 parent baf11a4 commit 1d4e965

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

@@ -677,8 +676,8 @@ def my_filter(name):
677676
self.assertIs(sys.get_lazy_imports_filter(), my_filter)
678677

679678
def test_lazy_modules_attribute_is_dict(self):
680-
"""sys.lazy_modules should be a dict per PEP 810."""
681-
self.assertIsInstance(sys.lazy_modules, dict)
679+
"""sys.lazy_modules should be a set per PEP 810."""
680+
self.assertIsInstance(sys.lazy_modules, set)
682681

683682
@support.requires_subprocess()
684683
def test_lazy_modules_tracks_lazy_imports(self):
@@ -687,8 +686,7 @@ def test_lazy_modules_tracks_lazy_imports(self):
687686
import sys
688687
initial_count = len(sys.lazy_modules)
689688
import test.test_lazy_import.data.basic_unused
690-
assert "test.test_lazy_import.data" in sys.lazy_modules
691-
assert sys.lazy_modules["test.test_lazy_import.data"] == {"basic2"}
689+
assert "test.test_lazy_import.data.basic2" in sys.lazy_modules
692690
assert len(sys.lazy_modules) > initial_count
693691
print("OK")
694692
""")
@@ -1137,15 +1135,14 @@ def test_module_added_to_lazy_modules_on_lazy_import(self):
11371135
lazy import test.test_lazy_import.data.basic2
11381136
11391137
# Should be in lazy_modules after lazy import
1140-
assert "test.test_lazy_import.data" in sys.lazy_modules
1141-
assert sys.lazy_modules["test.test_lazy_import.data"] == {"basic2"}
1138+
assert "test.test_lazy_import.data.basic2" in sys.lazy_modules
11421139
assert len(sys.lazy_modules) > initial_count
11431140
11441141
# Trigger reification
11451142
_ = test.test_lazy_import.data.basic2.x
11461143
11471144
# Module should still be tracked (for diagnostics per PEP 810)
1148-
assert "test.test_lazy_import.data" not in sys.lazy_modules
1145+
assert "test.test_lazy_import.data.basic2" not in sys.lazy_modules
11491146
print("OK")
11501147
""")
11511148
result = subprocess.run(
@@ -1158,8 +1155,8 @@ def test_module_added_to_lazy_modules_on_lazy_import(self):
11581155

11591156
def test_lazy_modules_is_per_interpreter(self):
11601157
"""Each interpreter should have independent sys.lazy_modules."""
1161-
# Basic test that sys.lazy_modules exists and is a dict
1162-
self.assertIsInstance(sys.lazy_modules, dict)
1158+
# Basic test that sys.lazy_modules exists and is a set
1159+
self.assertIsInstance(sys.lazy_modules, set)
11631160

11641161
def test_lazy_module_without_children_is_tracked(self):
11651162
code = textwrap.dedent("""
@@ -1168,10 +1165,6 @@ def test_lazy_module_without_children_is_tracked(self):
11681165
assert "json" in sys.lazy_modules, (
11691166
f"expected 'json' in sys.lazy_modules, got {set(sys.lazy_modules)}"
11701167
)
1171-
assert sys.lazy_modules["json"] == set(), (
1172-
f"expected empty set for sys.lazy_modules['json'], "
1173-
f"got {sys.lazy_modules['json']!r}"
1174-
)
11751168
print("OK")
11761169
""")
11771170
assert_python_ok("-c", code)
@@ -1938,7 +1931,7 @@ def create_lazy_imports(idx):
19381931
t.join()
19391932
19401933
assert not errors, f"Errors: {errors}"
1941-
assert isinstance(sys.lazy_modules, dict), "sys.lazy_modules is not a dict"
1934+
assert isinstance(sys.lazy_modules, set), "sys.lazy_modules is not a dict"
19421935
print("OK")
19431936
""")
19441937

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
@@ -4338,7 +4344,7 @@ get_mod_dict(PyObject *module)
43384344
// ensure we have the set for the parent module name in sys.lazy_modules.
43394345
// Returns a new reference.
43404346
static PyObject *
4341-
ensure_lazy_submodules(PyDictObject *lazy_modules, PyObject *parent)
4347+
ensure_lazy_pending_submodules(PyDictObject *lazy_modules, PyObject *parent)
43424348
{
43434349
PyObject *lazy_submodules;
43444350
Py_BEGIN_CRITICAL_SECTION(lazy_modules);
@@ -4357,6 +4363,9 @@ ensure_lazy_submodules(PyDictObject *lazy_modules, PyObject *parent)
43574363
return lazy_submodules;
43584364
}
43594365

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

43704379
PyInterpreterState *interp = tstate->interp;
4371-
PyObject *lazy_modules = LAZY_MODULES(interp);
4372-
assert(lazy_modules != NULL);
4380+
PyObject *lazy_pending_submodules = LAZY_PENDING_SUBMODULES(interp);
4381+
assert(lazy_pending_submodules != NULL);
43734382

43744383
Py_INCREF(name);
43754384
while (true) {
43764385
Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0,
43774386
PyUnicode_GET_LENGTH(name), -1);
43784387
if (dot < 0) {
4379-
PyObject *lazy_submodules = ensure_lazy_submodules(
4380-
(PyDictObject *)lazy_modules, name);
4388+
PyObject *lazy_submodules = ensure_lazy_pending_submodules(
4389+
(PyDictObject *)lazy_pending_submodules, name);
43814390
if (lazy_submodules == NULL) {
43824391
goto done;
43834392
}
@@ -4399,8 +4408,8 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name,
43994408
}
44004409

44014410
// Record the child as being lazily imported from the parent.
4402-
PyObject *lazy_submodules = ensure_lazy_submodules(
4403-
(PyDictObject *)lazy_modules, parent);
4411+
PyObject *lazy_submodules = ensure_lazy_pending_submodules(
4412+
(PyDictObject *)lazy_pending_submodules, parent);
44044413
if (lazy_submodules == NULL) {
44054414
goto done;
44064415
}
@@ -4463,6 +4472,14 @@ register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name,
44634472
if (fromname == NULL) {
44644473
return -1;
44654474
}
4475+
4476+
// Add the module name to sys.lazy_modules set (PEP 810).
4477+
PyObject *lazy_modules = LAZY_MODULES(tstate->interp);
4478+
if (PySet_Add(lazy_modules, fromname) < 0) {
4479+
Py_DECREF(fromname);
4480+
return -1;
4481+
}
4482+
44664483
int res = register_lazy_on_parent(tstate, fromname, builtins);
44674484
Py_DECREF(fromname);
44684485
return res;
@@ -4554,6 +4571,13 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
45544571
Py_DECREF(abs_name);
45554572
return NULL;
45564573
}
4574+
4575+
// Add the module name to sys.lazy_modules set (PEP 810).
4576+
PyObject *lazy_modules = LAZY_MODULES(tstate->interp);
4577+
if (PySet_Add(lazy_modules, abs_name) < 0) {
4578+
goto error;
4579+
}
4580+
45574581
if (fromlist && PyUnicode_Check(fromlist)) {
45584582
if (register_from_lazy_on_parent(tstate, abs_name, fromlist,
45594583
builtins) < 0) {
@@ -4790,6 +4814,7 @@ _PyImport_ClearCore(PyInterpreterState *interp)
47904814
Py_CLEAR(IMPORTLIB(interp));
47914815
Py_CLEAR(IMPORT_FUNC(interp));
47924816
Py_CLEAR(LAZY_IMPORT_FUNC(interp));
4817+
Py_CLEAR(interp->imports.lazy_pending_submodules);
47934818
Py_CLEAR(interp->imports.lazy_modules);
47944819
Py_CLEAR(interp->imports.lazy_importing_modules);
47954820
Py_CLEAR(interp->imports.lazy_imports_filter);
@@ -5637,11 +5662,13 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
56375662
PyThreadState *tstate = _PyThreadState_GET();
56385663
PyObject *module_dict = NULL;
56395664
PyObject *ret = NULL;
5640-
PyObject *lazy_modules = LAZY_MODULES(tstate->interp);
5641-
assert(lazy_modules != NULL);
5665+
PyObject *lazy_pending_modules = LAZY_PENDING_SUBMODULES(tstate->interp);
5666+
assert(lazy_pending_modules != NULL);
56425667

56435668
PyObject *lazy_submodules;
5644-
if (PyDict_GetItemRef(lazy_modules, name, &lazy_submodules) < 0) {
5669+
if (PySet_Discard(LAZY_MODULES(tstate->interp), name) < 0) {
5670+
return NULL;
5671+
} else if (PyDict_GetItemRef(lazy_pending_modules, name, &lazy_submodules) < 0) {
56455672
return NULL;
56465673
}
56475674
else if (lazy_submodules == NULL) {
@@ -5660,8 +5687,7 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
56605687
Py_END_CRITICAL_SECTION();
56615688
Py_DECREF(lazy_submodules);
56625689

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

0 commit comments

Comments
 (0)