Skip to content

Commit 7ed9062

Browse files
authored
GH-151619: Ensure non-module global/builtin namespaces are watched for lazy imports (#151762)
1 parent 41cc78a commit 7ed9062

3 files changed

Lines changed: 86 additions & 1 deletion

File tree

Lib/test/test_lazy_import/__init__.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,81 @@ def f():
949949
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
950950
self.assertIn("OK", result.stdout)
951951

952+
def test_add_lazy_to_exec_globals_after_specialization(self):
953+
code = textwrap.dedent("""
954+
source = '''
955+
import sys
956+
import types
957+
958+
lazy from test.test_lazy_import.data import basic2
959+
960+
assert 'test.test_lazy_import.data.basic2' not in sys.modules
961+
962+
class C: pass
963+
sneaky = C()
964+
sneaky.x = 1
965+
966+
def f():
967+
t = 0
968+
for _ in range(5):
969+
t += sneaky.x
970+
return t
971+
972+
f()
973+
globals()["sneaky"] = globals()["basic2"]
974+
assert f() == 210
975+
print("OK")
976+
'''
977+
ns = {"__name__": "lazy_exec_globals"}
978+
exec(source, ns)
979+
""")
980+
result = subprocess.run(
981+
[sys.executable, "-c", code],
982+
capture_output=True,
983+
text=True
984+
)
985+
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
986+
self.assertIn("OK", result.stdout)
987+
988+
def test_add_lazy_to_exec_builtins_after_specialization(self):
989+
code = textwrap.dedent("""
990+
import builtins
991+
source = '''
992+
import sys
993+
import types
994+
995+
lazy from test.test_lazy_import.data import basic2
996+
997+
assert 'test.test_lazy_import.data.basic2' not in sys.modules
998+
999+
class C: pass
1000+
sneaky = C()
1001+
sneaky.x = 1
1002+
__builtins__["sneaky"] = sneaky
1003+
del sneaky
1004+
1005+
def f():
1006+
t = 0
1007+
for _ in range(5):
1008+
t += sneaky.x
1009+
return t
1010+
1011+
f()
1012+
__builtins__["sneaky"] = globals()["basic2"]
1013+
assert f() == 210
1014+
print("OK")
1015+
'''
1016+
ns = {"__name__": "lazy_exec_builtins", "__builtins__": builtins.__dict__.copy()}
1017+
exec(source, ns)
1018+
""")
1019+
result = subprocess.run(
1020+
[sys.executable, "-c", code],
1021+
capture_output=True,
1022+
text=True
1023+
)
1024+
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
1025+
self.assertIn("OK", result.stdout)
1026+
9521027

9531028
@support.requires_subprocess()
9541029
class MultipleNameFromImportTests(LazyImportTestCase):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix an issue where using non-module global or builtin namespaces (such as
2+
dictionaries passed to :func:`exec`) could cause cached global loads to
3+
produce unresolved :ref:`lazy imports <lazy-imports>`.

Python/specialize.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1388,6 +1388,7 @@ specialize_load_global_lock_held(
13881388
SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_OUT_OF_RANGE);
13891389
goto fail;
13901390
}
1391+
PyDict_Watch(MODULE_WATCHER_ID, globals);
13911392
#ifdef Py_GIL_DISABLED
13921393
maybe_enable_deferred_ref_count(value);
13931394
#endif
@@ -1405,11 +1406,15 @@ specialize_load_global_lock_held(
14051406
SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_LOAD_GLOBAL_NON_STRING_OR_SPLIT);
14061407
goto fail;
14071408
}
1408-
index = _PyDictKeys_StringLookup(builtin_keys, name);
1409+
index = _PyDict_LookupIndexAndValue((PyDictObject *)builtins, name, &value);
14091410
if (index == DKIX_ERROR) {
14101411
SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_EXPECTED_ERROR);
14111412
goto fail;
14121413
}
1414+
if (value != NULL && PyLazyImport_CheckExact(value)) {
1415+
SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_ATTR_MODULE_LAZY_VALUE);
1416+
goto fail;
1417+
}
14131418
if (index != (uint16_t)index) {
14141419
SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_OUT_OF_RANGE);
14151420
goto fail;
@@ -1424,6 +1429,7 @@ specialize_load_global_lock_held(
14241429
SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_OUT_OF_RANGE);
14251430
goto fail;
14261431
}
1432+
PyDict_Watch(MODULE_WATCHER_ID, globals);
14271433
uint32_t builtins_version = _PyDict_GetKeysVersionForCurrentState(
14281434
interp, (PyDictObject*) builtins);
14291435
if (builtins_version == 0) {
@@ -1434,6 +1440,7 @@ specialize_load_global_lock_held(
14341440
SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_OUT_OF_RANGE);
14351441
goto fail;
14361442
}
1443+
PyDict_Watch(MODULE_WATCHER_ID, builtins);
14371444
cache->index = (uint16_t)index;
14381445
cache->module_keys_version = (uint16_t)globals_version;
14391446
cache->builtin_keys_version = (uint16_t)builtins_version;

0 commit comments

Comments
 (0)