]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-148587: Make sys.lazy_modules match PEP and keep internal lazy submodules tracking...
authorBartosz Sławecki <bartosz@ilikepython.com>
Fri, 29 May 2026 00:55:47 +0000 (02:55 +0200)
committerGitHub <noreply@github.com>
Fri, 29 May 2026 00:55:47 +0000 (17:55 -0700)
Make sys.lazy_modules match PEP and keep internal lazy submodules tracking internal

Co-authored-by: Dino Viehland <dinoviehland@meta.com>
Include/internal/pycore_interp_structs.h
Lib/test/test_lazy_import/__init__.py
Lib/test/test_lazy_import/__main__.py [new file with mode: 0644]
Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-18-36-28.gh-issue-148587.-RD3z5.rst [new file with mode: 0644]
Python/import.c

index 4c8ad11447aa5918318324d1e53c28bcbf55cf78..956fa290f0ad0e73538bbb8bdf58e5d78d341da6 100644 (file)
@@ -349,7 +349,15 @@ struct _import_state {
     int lazy_imports_mode;
     PyObject *lazy_imports_filter;
     PyObject *lazy_importing_modules;
+    // The set stored in sys.lazy_modules if values that have been
+    // lazily imported. This value is only for debugging/introspection
+    // purposes and is not used by the runtime.
     PyObject *lazy_modules;
+    // A dict mapping package names to a set of submodule names that
+    // have been imported lazily from packages which have been imported
+    // lazily. When the package is reified we need to add a
+    // LazyImportObject which refers to the submodule on the module.
+    PyObject *lazy_pending_submodules;
 #ifdef Py_GIL_DISABLED
     PyMutex lazy_mutex;
 #endif
index 1298d532b91b97c56d393dcf6fd86aafadc1ae5c..787caff4f8537bb436d0bb304d73e562f4c154bd 100644 (file)
@@ -38,8 +38,7 @@ class LazyImportTests(unittest.TestCase):
         """Lazy imported module should not be loaded if never accessed."""
         import test.test_lazy_import.data.basic_unused
         self.assertNotIn("test.test_lazy_import.data.basic2", sys.modules)
-        self.assertIn("test.test_lazy_import.data", sys.lazy_modules)
-        self.assertEqual(sys.lazy_modules["test.test_lazy_import.data"], {"basic2"})
+        self.assertIn("test.test_lazy_import.data.basic2", sys.lazy_modules)
 
     def test_sys_lazy_modules(self):
         try:
@@ -49,7 +48,7 @@ class LazyImportTests(unittest.TestCase):
 
         self.assertFalse("test.test_lazy_import.data.basic2" in sys.modules)
         self.assertIn("test.test_lazy_import.data", sys.lazy_modules)
-        self.assertEqual(sys.lazy_modules["test.test_lazy_import.data"], {"basic2"})
+        self.assertIn("test.test_lazy_import.data.basic2", sys.lazy_modules)
         test.test_lazy_import.data.basic_from_unused.basic2
         self.assertNotIn("test.test_import.data", sys.lazy_modules)
 
@@ -677,8 +676,8 @@ class SysLazyImportsAPITests(unittest.TestCase):
         self.assertIs(sys.get_lazy_imports_filter(), my_filter)
 
     def test_lazy_modules_attribute_is_dict(self):
-        """sys.lazy_modules should be a dict per PEP 810."""
-        self.assertIsInstance(sys.lazy_modules, dict)
+        """sys.lazy_modules should be a set per PEP 810."""
+        self.assertIsInstance(sys.lazy_modules, set)
 
     @support.requires_subprocess()
     def test_lazy_modules_tracks_lazy_imports(self):
@@ -687,8 +686,7 @@ class SysLazyImportsAPITests(unittest.TestCase):
             import sys
             initial_count = len(sys.lazy_modules)
             import test.test_lazy_import.data.basic_unused
-            assert "test.test_lazy_import.data" in sys.lazy_modules
-            assert sys.lazy_modules["test.test_lazy_import.data"] == {"basic2"}
+            assert "test.test_lazy_import.data.basic2" in sys.lazy_modules
             assert len(sys.lazy_modules) > initial_count
             print("OK")
         """)
@@ -1137,15 +1135,14 @@ class SysLazyModulesTrackingTests(unittest.TestCase):
             lazy import test.test_lazy_import.data.basic2
 
             # Should be in lazy_modules after lazy import
-            assert "test.test_lazy_import.data" in sys.lazy_modules
-            assert sys.lazy_modules["test.test_lazy_import.data"] == {"basic2"}
+            assert "test.test_lazy_import.data.basic2" in sys.lazy_modules
             assert len(sys.lazy_modules) > initial_count
 
             # Trigger reification
             _ = test.test_lazy_import.data.basic2.x
 
             # Module should still be tracked (for diagnostics per PEP 810)
-            assert "test.test_lazy_import.data" not in sys.lazy_modules
+            assert "test.test_lazy_import.data.basic2" not in sys.lazy_modules
             print("OK")
         """)
         result = subprocess.run(
@@ -1158,8 +1155,8 @@ class SysLazyModulesTrackingTests(unittest.TestCase):
 
     def test_lazy_modules_is_per_interpreter(self):
         """Each interpreter should have independent sys.lazy_modules."""
-        # Basic test that sys.lazy_modules exists and is a dict
-        self.assertIsInstance(sys.lazy_modules, dict)
+        # Basic test that sys.lazy_modules exists and is a set
+        self.assertIsInstance(sys.lazy_modules, set)
 
     def test_lazy_module_without_children_is_tracked(self):
         code = textwrap.dedent("""
@@ -1168,10 +1165,6 @@ class SysLazyModulesTrackingTests(unittest.TestCase):
             assert "json" in sys.lazy_modules, (
                 f"expected 'json' in sys.lazy_modules, got {set(sys.lazy_modules)}"
             )
-            assert sys.lazy_modules["json"] == set(), (
-                f"expected empty set for sys.lazy_modules['json'], "
-                f"got {sys.lazy_modules['json']!r}"
-            )
             print("OK")
         """)
         assert_python_ok("-c", code)
@@ -1938,7 +1931,7 @@ class ThreadSafetyTests(unittest.TestCase):
                 t.join()
 
             assert not errors, f"Errors: {errors}"
-            assert isinstance(sys.lazy_modules, dict), "sys.lazy_modules is not a dict"
+            assert isinstance(sys.lazy_modules, set), "sys.lazy_modules is not a dict"
             print("OK")
         """)
 
diff --git a/Lib/test/test_lazy_import/__main__.py b/Lib/test/test_lazy_import/__main__.py
new file mode 100644 (file)
index 0000000..d6c94ef
--- /dev/null
@@ -0,0 +1,3 @@
+import unittest
+
+unittest.main('test.test_lazy_import')
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-18-36-28.gh-issue-148587.-RD3z5.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-18-36-28.gh-issue-148587.-RD3z5.rst
new file mode 100644 (file)
index 0000000..61bfdcd
--- /dev/null
@@ -0,0 +1 @@
+``sys.lazy_modules`` is now a set instead of a dict as initially spelled out in PEP 810.
index 3e470d2c22cfbd789253767f6f202955a9856907..8d57b92925b63035fd633200d4fa3b9a62a56493 100644 (file)
@@ -94,6 +94,8 @@ static struct _inittab *inittab_copy = NULL;
     (interp)->imports.modules_by_index
 #define LAZY_MODULES(interp) \
     (interp)->imports.lazy_modules
+#define LAZY_PENDING_SUBMODULES(interp) \
+    (interp)->imports.lazy_pending_submodules
 #define IMPORTLIB(interp) \
     (interp)->imports.importlib
 #define OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) \
@@ -271,8 +273,11 @@ import_get_module(PyThreadState *tstate, PyObject *name)
 PyObject *
 _PyImport_InitLazyModules(PyInterpreterState *interp)
 {
-    assert(LAZY_MODULES(interp) == NULL);
-    LAZY_MODULES(interp) = PyDict_New();
+    assert(LAZY_MODULES(interp) == NULL &&
+           LAZY_PENDING_SUBMODULES(interp) == NULL);
+
+    LAZY_PENDING_SUBMODULES(interp) = PyDict_New();
+    LAZY_MODULES(interp) = PySet_New(0);
     return LAZY_MODULES(interp);
 }
 
@@ -280,6 +285,7 @@ void
 _PyImport_ClearLazyModules(PyInterpreterState *interp)
 {
     Py_CLEAR(LAZY_MODULES(interp));
+    Py_CLEAR(LAZY_PENDING_SUBMODULES(interp));
 }
 
 static int
@@ -4338,7 +4344,7 @@ get_mod_dict(PyObject *module)
 // ensure we have the set for the parent module name in sys.lazy_modules.
 // Returns a new reference.
 static PyObject *
-ensure_lazy_submodules(PyDictObject *lazy_modules, PyObject *parent)
+ensure_lazy_pending_submodules(PyDictObject *lazy_modules, PyObject *parent)
 {
     PyObject *lazy_submodules;
     Py_BEGIN_CRITICAL_SECTION(lazy_modules);
@@ -4357,6 +4363,9 @@ ensure_lazy_submodules(PyDictObject *lazy_modules, PyObject *parent)
     return lazy_submodules;
 }
 
+// Ensures that we have a LazyImportObject on the parent module for
+// all children modules which have been lazily imported. If the parent
+// module overrides the child attribute then the value is not replaced.
 static int
 register_lazy_on_parent(PyThreadState *tstate, PyObject *name,
                         PyObject *builtins)
@@ -4368,16 +4377,16 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name,
     PyObject *parent_dict = NULL;
 
     PyInterpreterState *interp = tstate->interp;
-    PyObject *lazy_modules = LAZY_MODULES(interp);
-    assert(lazy_modules != NULL);
+    PyObject *lazy_pending_submodules = LAZY_PENDING_SUBMODULES(interp);
+    assert(lazy_pending_submodules != NULL);
 
     Py_INCREF(name);
     while (true) {
         Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0,
                                             PyUnicode_GET_LENGTH(name), -1);
         if (dot < 0) {
-            PyObject *lazy_submodules = ensure_lazy_submodules(
-                (PyDictObject *)lazy_modules, name);
+            PyObject *lazy_submodules = ensure_lazy_pending_submodules(
+                (PyDictObject *)lazy_pending_submodules, name);
             if (lazy_submodules == NULL) {
                 goto done;
             }
@@ -4399,8 +4408,8 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name,
         }
 
         // Record the child as being lazily imported from the parent.
-        PyObject *lazy_submodules = ensure_lazy_submodules(
-            (PyDictObject *)lazy_modules, parent);
+        PyObject *lazy_submodules = ensure_lazy_pending_submodules(
+            (PyDictObject *)lazy_pending_submodules, parent);
         if (lazy_submodules == NULL) {
             goto done;
         }
@@ -4463,6 +4472,14 @@ register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name,
     if (fromname == NULL) {
         return -1;
     }
+
+    // Add the module name to sys.lazy_modules set (PEP 810).
+    PyObject *lazy_modules = LAZY_MODULES(tstate->interp);
+    if (PySet_Add(lazy_modules, fromname) < 0) {
+        Py_DECREF(fromname);
+        return -1;
+    }
+
     int res = register_lazy_on_parent(tstate, fromname, builtins);
     Py_DECREF(fromname);
     return res;
@@ -4554,6 +4571,13 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
         Py_DECREF(abs_name);
         return NULL;
     }
+
+    // Add the module name to sys.lazy_modules set (PEP 810).
+    PyObject *lazy_modules = LAZY_MODULES(tstate->interp);
+    if (PySet_Add(lazy_modules, abs_name) < 0) {
+        goto error;
+    }
+
     if (fromlist && PyUnicode_Check(fromlist)) {
         if (register_from_lazy_on_parent(tstate, abs_name, fromlist,
                                          builtins) < 0) {
@@ -4790,6 +4814,7 @@ _PyImport_ClearCore(PyInterpreterState *interp)
     Py_CLEAR(IMPORTLIB(interp));
     Py_CLEAR(IMPORT_FUNC(interp));
     Py_CLEAR(LAZY_IMPORT_FUNC(interp));
+    Py_CLEAR(interp->imports.lazy_pending_submodules);
     Py_CLEAR(interp->imports.lazy_modules);
     Py_CLEAR(interp->imports.lazy_importing_modules);
     Py_CLEAR(interp->imports.lazy_imports_filter);
@@ -5637,11 +5662,13 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
     PyThreadState *tstate = _PyThreadState_GET();
     PyObject *module_dict = NULL;
     PyObject *ret = NULL;
-    PyObject *lazy_modules = LAZY_MODULES(tstate->interp);
-    assert(lazy_modules != NULL);
+    PyObject *lazy_pending_modules = LAZY_PENDING_SUBMODULES(tstate->interp);
+    assert(lazy_pending_modules != NULL);
 
     PyObject *lazy_submodules;
-    if (PyDict_GetItemRef(lazy_modules, name, &lazy_submodules) < 0) {
+    if (PySet_Discard(LAZY_MODULES(tstate->interp), name) < 0) {
+        return NULL;
+    } else if (PyDict_GetItemRef(lazy_pending_modules, name, &lazy_submodules) < 0) {
         return NULL;
     }
     else if (lazy_submodules == NULL) {
@@ -5660,8 +5687,7 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
     Py_END_CRITICAL_SECTION();
     Py_DECREF(lazy_submodules);
 
-    // once a module is imported it is removed from sys.lazy_modules
-    if (PyDict_DelItem(lazy_modules, name) < 0) {
+    if (PyDict_DelItem(lazy_pending_modules, name) < 0) {
         goto error;
     }