]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-151619: Ensure non-module global/builtin namespaces are watched for lazy imports...
authorBrandt Bucher <brandt@python.org>
Sun, 28 Jun 2026 22:24:32 +0000 (15:24 -0700)
committerGitHub <noreply@github.com>
Sun, 28 Jun 2026 22:24:32 +0000 (23:24 +0100)
Lib/test/test_lazy_import/__init__.py
Misc/NEWS.d/next/Core_and_Builtins/2026-06-19-16-40-01.gh-issue-151619.35yyJW.rst [new file with mode: 0644]
Python/specialize.c

index 4658882243d65ff0b9373ba5292c689bee463730..e145ae060d7150fc5f0f12808a9fbad21bc93e3d 100644 (file)
@@ -949,6 +949,81 @@ class GlobalsAndDictTests(LazyImportTestCase):
         self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
         self.assertIn("OK", result.stdout)
 
+    def test_add_lazy_to_exec_globals_after_specialization(self):
+        code = textwrap.dedent("""
+            source = '''
+            import sys
+            import types
+
+            lazy from test.test_lazy_import.data import basic2
+
+            assert 'test.test_lazy_import.data.basic2' not in sys.modules
+
+            class C: pass
+            sneaky = C()
+            sneaky.x = 1
+
+            def f():
+                t = 0
+                for _ in range(5):
+                    t += sneaky.x
+                return t
+
+            f()
+            globals()["sneaky"] = globals()["basic2"]
+            assert f() == 210
+            print("OK")
+            '''
+            ns = {"__name__": "lazy_exec_globals"}
+            exec(source, ns)
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_add_lazy_to_exec_builtins_after_specialization(self):
+        code = textwrap.dedent("""
+            import builtins
+            source = '''
+            import sys
+            import types
+
+            lazy from test.test_lazy_import.data import basic2
+
+            assert 'test.test_lazy_import.data.basic2' not in sys.modules
+
+            class C: pass
+            sneaky = C()
+            sneaky.x = 1
+            __builtins__["sneaky"] = sneaky
+            del sneaky
+
+            def f():
+                t = 0
+                for _ in range(5):
+                    t += sneaky.x
+                return t
+
+            f()
+            __builtins__["sneaky"] = globals()["basic2"]
+            assert f() == 210
+            print("OK")
+            '''
+            ns = {"__name__": "lazy_exec_builtins", "__builtins__": builtins.__dict__.copy()}
+            exec(source, ns)
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
 
 @support.requires_subprocess()
 class MultipleNameFromImportTests(LazyImportTestCase):
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-19-16-40-01.gh-issue-151619.35yyJW.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-19-16-40-01.gh-issue-151619.35yyJW.rst
new file mode 100644 (file)
index 0000000..28ba203
--- /dev/null
@@ -0,0 +1,3 @@
+Fix an issue where using non-module global or builtin namespaces (such as
+dictionaries passed to :func:`exec`) could cause cached global loads to
+produce unresolved :ref:`lazy imports <lazy-imports>`.
index 79dd70b7f457673746f59167929a30397600baff..6aaae6f383d22f56fe7a5eaa3d56edee17917ddd 100644 (file)
@@ -1388,6 +1388,7 @@ specialize_load_global_lock_held(
             SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_OUT_OF_RANGE);
             goto fail;
         }
+        PyDict_Watch(MODULE_WATCHER_ID, globals);
 #ifdef Py_GIL_DISABLED
         maybe_enable_deferred_ref_count(value);
 #endif
@@ -1405,11 +1406,15 @@ specialize_load_global_lock_held(
         SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_LOAD_GLOBAL_NON_STRING_OR_SPLIT);
         goto fail;
     }
-    index = _PyDictKeys_StringLookup(builtin_keys, name);
+    index = _PyDict_LookupIndexAndValue((PyDictObject *)builtins, name, &value);
     if (index == DKIX_ERROR) {
         SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_EXPECTED_ERROR);
         goto fail;
     }
+    if (value != NULL && PyLazyImport_CheckExact(value)) {
+        SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_ATTR_MODULE_LAZY_VALUE);
+        goto fail;
+    }
     if (index != (uint16_t)index) {
         SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_OUT_OF_RANGE);
         goto fail;
@@ -1424,6 +1429,7 @@ specialize_load_global_lock_held(
         SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_OUT_OF_RANGE);
         goto fail;
     }
+    PyDict_Watch(MODULE_WATCHER_ID, globals);
     uint32_t builtins_version = _PyDict_GetKeysVersionForCurrentState(
             interp, (PyDictObject*) builtins);
     if (builtins_version == 0) {
@@ -1434,6 +1440,7 @@ specialize_load_global_lock_held(
         SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_OUT_OF_RANGE);
         goto fail;
     }
+    PyDict_Watch(MODULE_WATCHER_ID, builtins);
     cache->index = (uint16_t)index;
     cache->module_keys_version = (uint16_t)globals_version;
     cache->builtin_keys_version = (uint16_t)builtins_version;