Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Include/internal/pycore_lazyimportobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,25 @@ typedef struct {
PyObject *lz_builtins;
PyObject *lz_from;
PyObject *lz_attr;
// Protected by the lazy object lock.
PyObject *lz_globals;
PyObject *lz_key;
PyObject *lz_resolved;
// Frame information for the original import location.
PyCodeObject *lz_code; // Code object where the lazy import was created.
int lz_instr_offset; // Instruction offset where the lazy import was created.
} PyLazyImportObject;


PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import);
extern PyObject * _PyLazyImport_GetResolved(PyObject *lazy_import);
extern int _PyLazyImport_FinishResolve(
PyObject *lazy_import, PyObject *resolved);
PyAPI_FUNC(int) _PyLazyImport_ReplaceDictItemIfCurrent(
PyObject *lazy_import, PyObject *dict, PyObject *name,
PyObject *resolved);
PyAPI_FUNC(int) _PyLazyImport_SetGlobalBindingAndDictItem(
PyObject *lazy_import, PyObject *globals, PyObject *name);
PyAPI_FUNC(PyObject *) _PyLazyImport_New(
struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from, PyObject *attr);

Expand Down
304 changes: 278 additions & 26 deletions Lib/test/test_lazy_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,42 +905,294 @@ def test_direct_access_triggers_reification(self):
result = test.test_lazy_import.data.globals_access.get_direct()
self.assertIn("test.test_lazy_import.data.basic2", sys.modules)

def test_resolve_method_forces_reification(self):
"""Calling resolve() on lazy proxy should force reification.
def test_resolve_method_preserves_binding_lifecycle(self):
code = textwrap.dedent("""
import builtins
import threading
import types

Note: Must access lazy proxy from within a function to avoid automatic
reification by LOAD_NAME at module level.
"""
real_import = builtins.__import__
calls = []

lazy import target_module as target
lazy import rebound_module as rebound
lazy import deleted_module as deleted
lazy import broken_module as broken
lazy from from_module import VALUE as value
lazy import threaded_module as threaded

def custom_import(name, globals=None, locals=None, fromlist=None, level=0):
calls.append(name)
if name == "broken_module":
raise ImportError("boom")
module = types.ModuleType(name)
module.VALUE = name
return module

builtins.__import__ = custom_import
try:
def check():
g = globals()

target_proxy = g["target"]
g["alias"] = target_proxy
resolved = target_proxy.resolve()
assert g["target"] is resolved
assert g["alias"] is target_proxy
assert target_proxy.resolve() is resolved
assert target is resolved
assert calls.count("target_module") == 1

sentinel = object()
rebound_proxy = g["rebound"]
g["rebound"] = sentinel
rebound_resolved = rebound_proxy.resolve()
assert g["rebound"] is sentinel
assert rebound_proxy.resolve() is rebound_resolved
assert calls.count("rebound_module") == 1

deleted_proxy = g["deleted"]
del g["deleted"]
deleted_proxy.resolve()
assert "deleted" not in g
assert calls.count("deleted_module") == 1

broken_proxy = g["broken"]
for _ in range(2):
try:
broken_proxy.resolve()
except ImportError as exc:
assert str(exc) == "boom"
else:
raise AssertionError("resolve() unexpectedly succeeded")
assert g["broken"] is broken_proxy
assert calls.count("broken_module") == 2

value_proxy = g["value"]
assert value_proxy.resolve() == "from_module"
assert g["value"] == "from_module"
assert calls.count("from_module") == 1

threaded_proxy = g["threaded"]
barrier = threading.Barrier(3)
results = []
errors = []

def resolve_threaded():
barrier.wait()
try:
results.append(threaded_proxy.resolve())
except BaseException as exc:
errors.append(exc)

threads = [
threading.Thread(target=resolve_threaded)
for _ in range(2)
]
for thread in threads:
thread.start()
barrier.wait()
for thread in threads:
thread.join()

assert not errors, errors
assert len(results) == 2
assert results[0] is results[1]
assert g["threaded"] is results[0]
assert calls.count("threaded_module") == 1

check()
finally:
builtins.__import__ = real_import
""")
assert_python_ok("-c", code)

def test_dict_subclass_globals_store_and_load(self):
code = textwrap.dedent("""
import sys
import builtins
import types

lazy from test.test_lazy_import.data.basic2 import x
class Globals(dict):
pass

assert 'test.test_lazy_import.data.basic2' not in sys.modules
lazy import store_module as store_source
lazy import rebound_module as rebound_source
lazy import deleted_module as deleted_source
lazy import builtin_module as builtin_source
lazy import load_name_rebound_module as load_name_rebound_source
lazy import load_name_deleted_module as load_name_deleted_source

sentinel = object()
calls = []

def store_template(value):
global target
target = value

def read_rebound():
return rebound_target

def read_deleted():
return deleted_target

def read_builtin():
return builtin_target

def custom_import(name, globals=None, locals=None, fromlist=(), level=0):
calls.append(name)
if name == "rebound_module":
globals["rebound_target"] = sentinel
elif name == "deleted_module":
del globals["deleted_target"]
elif name == "load_name_rebound_module":
globals["target"] = sentinel
elif name == "load_name_deleted_module":
del globals["target"]
module = types.ModuleType(name)
module.VALUE = name
return module

def check():
module_globals = globals()
store_proxy = module_globals["store_source"]
rebound_proxy = module_globals["rebound_source"]
deleted_proxy = module_globals["deleted_source"]
builtin_proxy = module_globals["builtin_source"]
load_name_rebound_proxy = module_globals["load_name_rebound_source"]
load_name_deleted_proxy = module_globals["load_name_deleted_source"]

store_globals = Globals()
store = types.FunctionType(store_template.__code__, store_globals)
store(store_proxy)
assert store_globals["target"] is store_proxy

custom_builtins = dict(vars(builtins))
custom_builtins["__import__"] = custom_import
custom_builtins["builtin_target"] = builtin_proxy
function_globals = Globals(
__builtins__=custom_builtins,
rebound_target=rebound_proxy,
deleted_target=deleted_proxy,
)

def test_resolve():
g = globals()
lazy_obj = g['x']
assert type(lazy_obj) is types.LazyImportType, f"Expected lazy proxy, got {type(lazy_obj)}"
rebound_reader = types.FunctionType(
read_rebound.__code__, function_globals)
assert rebound_reader().VALUE == "rebound_module"
assert function_globals["rebound_target"] is sentinel

deleted_reader = types.FunctionType(
read_deleted.__code__, function_globals)
assert deleted_reader().VALUE == "deleted_module"
assert "deleted_target" not in function_globals

builtin_reader = types.FunctionType(
read_builtin.__code__, function_globals)
builtin_result = builtin_reader()
assert builtin_result.VALUE == "builtin_module"
assert builtin_reader() is builtin_result
assert "builtin_target" not in function_globals

load_name_rebound_globals = Globals(
__builtins__=custom_builtins,
target=load_name_rebound_proxy,
)
exec("result = target", load_name_rebound_globals,
load_name_rebound_globals)
assert load_name_rebound_globals["result"].VALUE == (
"load_name_rebound_module")
assert load_name_rebound_globals["target"] is sentinel

load_name_deleted_globals = Globals(
__builtins__=custom_builtins,
target=load_name_deleted_proxy,
)
exec("result = target", load_name_deleted_globals,
load_name_deleted_globals)
assert load_name_deleted_globals["result"].VALUE == (
"load_name_deleted_module")
assert "target" not in load_name_deleted_globals

assert calls == [
"rebound_module",
"deleted_module",
"builtin_module",
"load_name_rebound_module",
"load_name_deleted_module",
]

real_import = builtins.__import__
builtins.__import__ = custom_import
try:
check()
finally:
builtins.__import__ = real_import
""")
assert_python_ok("-c", code)

resolved = lazy_obj.resolve()
def test_lazy_import_rejects_dict_subclass_globals(self):
class Globals(dict):
pass

# Now module should be loaded
assert 'test.test_lazy_import.data.basic2' in sys.modules
assert resolved == 42 # x is 42 in basic2.py
return True
namespace = Globals()
with self.assertRaisesRegex(TypeError, "requires exact dict globals"):
exec("lazy import json as value", namespace, namespace)
self.assertNotIn("value", namespace)

assert test_resolve()
print("OK")
sys.set_lazy_imports_filter(lambda *args: False)
exec("lazy import json as value", namespace, namespace)
self.assertIs(namespace["value"], sys.modules["json"])

def test_reification_preserves_changed_bindings(self):
code = textwrap.dedent("""
import builtins
import types

real_import = builtins.__import__
calls = []
sentinel = object()

lazy import load_global_module as load_global_value
lazy import load_name_module as load_name_value

holder = types.ModuleType("holder")
holder.__dict__["__builtins__"] = builtins
exec("lazy import module_attr_module as value", holder.__dict__)

def custom_import(name, globals=None, locals=None, fromlist=None, level=0):
calls.append(name)
if name == "load_global_module":
globals["load_global_value"] = sentinel
elif name == "load_name_module":
del globals["load_name_value"]
elif name == "module_attr_module":
holder.__dict__["value"] = sentinel
module = types.ModuleType(name)
module.VALUE = name
return module

builtins.__import__ = custom_import
try:
def load_global():
return load_global_value

assert load_global().VALUE == "load_global_module"
assert globals()["load_global_value"] is sentinel

load_name_result = load_name_value
assert load_name_result.VALUE == "load_name_module"
assert "load_name_value" not in globals()

assert holder.value.VALUE == "module_attr_module"
assert holder.__dict__["value"] is sentinel
assert calls == [
"load_global_module",
"load_name_module",
"module_attr_module",
]
finally:
builtins.__import__ = real_import
""")
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)
assert_python_ok("-c", code)

def test_add_lazy_to_globals(self):
code = textwrap.dedent("""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :meth:`!types.LazyImportType.resolve` so resolving a module-level lazy
import replaces the original lazy proxy in the module globals. Lazy import
creation now requires an exact dictionary for the globals namespace.
32 changes: 24 additions & 8 deletions Modules/_testinternalcapi/test_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading