https://github.com/python/cpython/commit/7ed9062dd05e735505f75e9caac56ccd64913371
commit: 7ed9062dd05e735505f75e9caac56ccd64913371
branch: main
author: Brandt Bucher <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-06-28T23:24:32+01:00
summary:

GH-151619: Ensure non-module global/builtin namespaces are watched for lazy 
imports (#151762)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-06-19-16-40-01.gh-issue-151619.35yyJW.rst
M Lib/test/test_lazy_import/__init__.py
M Python/specialize.c

diff --git a/Lib/test/test_lazy_import/__init__.py 
b/Lib/test/test_lazy_import/__init__.py
index 4658882243d65ff..e145ae060d7150f 100644
--- a/Lib/test/test_lazy_import/__init__.py
+++ b/Lib/test/test_lazy_import/__init__.py
@@ -949,6 +949,81 @@ def f():
         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
index 000000000000000..28ba2038e876fa7
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-19-16-40-01.gh-issue-151619.35yyJW.rst
@@ -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>`.
diff --git a/Python/specialize.c b/Python/specialize.c
index 79dd70b7f457673..6aaae6f383d22f5 100644
--- a/Python/specialize.c
+++ b/Python/specialize.c
@@ -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;

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to