Skip to content

Commit 65695eb

Browse files
[3.15] GH-151619: Ensure non-module global/builtin namespaces are watched for lazy imports (GH-151762) (#152533)
GH-151619: Ensure non-module global/builtin namespaces are watched for lazy imports (GH-151762) (cherry picked from commit 7ed9062) Co-authored-by: Brandt Bucher <brandt@python.org>
1 parent aa06529 commit 65695eb

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
@@ -957,6 +957,81 @@ def f():
957957
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
958958
self.assertIn("OK", result.stdout)
959959

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

9611036
@support.requires_subprocess()
9621037
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)