Skip to content

Commit e1eb340

Browse files
Fix sys.modules leak in offline cross-sync test (#926)
reload_zoo popped unsloth_zoo from sys.modules and re-imported it without restoring the original module. The reimport installs a fresh package object that lacks lazily-bound submodule attributes such as vision_utils, so once this fixture had run, any later test resolving a submodule through a string path (monkeypatch.setattr("unsloth_zoo.vision_utils....", ...)) failed with AttributeError. This surfaced as 5 errors in test_vision_collator_audio.py in full-suite runs while every test passed in isolation. Snapshot the unsloth_zoo* modules and restore them on teardown so the reload no longer leaks a half-initialized package into the rest of the session. Also patch the imported module object directly in the audio collator test instead of the string path, so it stays robust to any future sys.modules pollution.
1 parent 385f9ef commit e1eb340

2 files changed

Lines changed: 32 additions & 3 deletions

File tree

tests/test_offline_env_cross_sync.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,38 @@
2424
@pytest.fixture
2525
def reload_zoo(monkeypatch):
2626
"""Strip the three offline env vars, then reload ``unsloth_zoo`` so its
27-
import-time cross-sync runs."""
27+
import-time cross-sync runs.
28+
29+
Reloading swaps a fresh, half-initialized ``unsloth_zoo`` object into
30+
``sys.modules`` -- one that lacks submodule attributes (e.g.
31+
``vision_utils``) the package only binds when they are first imported.
32+
Snapshot the original ``unsloth_zoo*`` modules and restore them on
33+
teardown so the reload does not leak that shell into the rest of the
34+
session and break later tests that resolve submodules via string paths
35+
(e.g. ``monkeypatch.setattr("unsloth_zoo.vision_utils.<attr>", ...)``).
36+
"""
2837
for v in ("HF_HUB_OFFLINE", "TRANSFORMERS_OFFLINE", "HF_DATASETS_OFFLINE"):
2938
monkeypatch.delenv(v, raising = False)
3039

40+
saved = {
41+
name: module
42+
for name, module in sys.modules.items()
43+
if name == "unsloth_zoo" or name.startswith("unsloth_zoo.")
44+
}
45+
3146
def _reload():
3247
sys.modules.pop("unsloth_zoo", None)
3348
return importlib.import_module("unsloth_zoo")
3449

35-
return _reload
50+
yield _reload
51+
52+
for name in [
53+
name for name in sys.modules
54+
if name == "unsloth_zoo" or name.startswith("unsloth_zoo.")
55+
]:
56+
if name not in saved:
57+
del sys.modules[name]
58+
sys.modules.update(saved)
3659

3760

3861
class TestOfflineCrossSync:

tests/test_vision_collator_audio.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,14 @@ def __getitem__(self, key):
507507
@pytest.fixture
508508
def _recognize_unpatched_decoder(monkeypatch):
509509
# Treat _UnpatchedFakeAudioDecoder as the decoder type so these run in CI.
510+
# Patch the imported module object directly rather than the
511+
# "unsloth_zoo.vision_utils._audio_decoder_types" string path: the string
512+
# form resolves unsloth_zoo through sys.modules at runtime, so a preceding
513+
# test that swaps unsloth_zoo in sys.modules would break resolution here.
514+
import unsloth_zoo.vision_utils as vision_utils
510515
monkeypatch.setattr(
511-
"unsloth_zoo.vision_utils._audio_decoder_types",
516+
vision_utils,
517+
"_audio_decoder_types",
512518
lambda: (_UnpatchedFakeAudioDecoder,),
513519
)
514520

0 commit comments

Comments
 (0)