Skip to content

Commit 1f1948c

Browse files
ampagentIsaac Miller
andcommitted
test(utils): make lazy-proxy cache tests deterministic and direct
Release the initializing module only after the second reader has returned or is blocked on the proxy lock, instead of sleeping. Add a test that a loaded proxy skips import_module on later reads and re-resolves after its sys.modules entry is replaced or removed. Amp-Thread-ID: https://ampcode.com/threads/T-01a12205-f6ff-70bc-bd7b-f1ab0cfc8ae6 Co-authored-by: Isaac Miller <isaac@cmpnd.ai>
1 parent 93e3c84 commit 1f1948c

1 file changed

Lines changed: 50 additions & 1 deletion

File tree

‎tests/utils/test_lazy_import.py‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,33 @@ def test_require_does_not_expose_module_accessed_during_its_own_initialization(t
110110
)
111111
hooks.proxy = require(module_name)
112112

113+
class ContentionLock:
114+
"""RLock that records when a thread has to wait for another thread's hold."""
115+
116+
def __init__(self):
117+
self._lock = threading.RLock()
118+
self.contended = threading.Event()
119+
120+
def __enter__(self):
121+
if not self._lock.acquire(blocking=False):
122+
self.contended.set()
123+
self._lock.acquire()
124+
125+
def __exit__(self, *exc_info):
126+
self._lock.release()
127+
128+
lock = ContentionLock()
129+
hooks.proxy._dspy_lazy_lock = lock
130+
113131
with ThreadPoolExecutor(max_workers=2) as executor:
114132
first = executor.submit(lambda: hooks.proxy.value)
115133
assert hooks.started.wait(5)
116134
# The module is still initializing, so another thread must wait instead of seeing the partial module.
117135
second = executor.submit(lambda: hooks.proxy.value)
118-
time.sleep(0.1)
136+
# Release only after the second read has returned or is blocked on the proxy lock.
137+
deadline = time.monotonic() + 5
138+
while not (second.done() or lock.contended.is_set()) and time.monotonic() < deadline:
139+
time.sleep(0.001)
119140
hooks.release.set()
120141

121142
assert first.result() == 42
@@ -144,6 +165,34 @@ def test_require_resolves_module_that_replaces_itself_during_initialization(tmp_
144165
assert hooks.proxy.value == "replacement"
145166

146167

168+
def test_require_reuses_loaded_module_until_sys_modules_changes(tmp_path, monkeypatch):
169+
module_name = "dspy_lazy_cached_module"
170+
monkeypatch.syspath_prepend(tmp_path)
171+
monkeypatch.delitem(sys.modules, module_name, raising=False)
172+
(tmp_path / f"{module_name}.py").write_text("value = 1\n")
173+
mod = require(module_name)
174+
assert mod.value == 1
175+
176+
imports = []
177+
import_module = importlib.import_module
178+
179+
def counting_import_module(name, *args, **kwargs):
180+
imports.append(name)
181+
return import_module(name, *args, **kwargs)
182+
183+
monkeypatch.setattr(importlib, "import_module", counting_import_module)
184+
185+
assert [mod.value for _ in range(3)] == [1, 1, 1]
186+
assert imports == []
187+
188+
monkeypatch.setitem(sys.modules, module_name, types.SimpleNamespace(value=2))
189+
assert mod.value == 2
190+
191+
del sys.modules[module_name]
192+
assert mod.value == 1
193+
assert imports == [module_name, module_name]
194+
195+
147196
def test_require_retries_after_failed_import(tmp_path, monkeypatch):
148197
module_name = "dspy_lazy_flaky_module"
149198
flag = tmp_path / "fail"

0 commit comments

Comments
 (0)