Coverage for tests / test_compat.py: 100%
76 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 21:38 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 21:38 +0000
1import importlib
2import sys
3import warnings as warnings_module
4from types import ModuleType
6import pytest
7from vivarium_compat._compat import (
8 _CompatFinder,
9 _CompatLoader,
10 _resolving,
11 install_compat_finder,
12)
15@pytest.fixture(autouse=True)
16def restore_import_state():
17 """Snapshot and restore sys.meta_path and sys.modules after each test."""
18 saved_meta_path = sys.meta_path[:]
19 saved_modules = set(sys.modules.keys())
20 yield
21 sys.meta_path[:] = saved_meta_path
22 for key in list(sys.modules.keys()):
23 if key not in saved_modules:
24 del sys.modules[key]
27@pytest.fixture
28def patched_redirects(monkeypatch):
29 """Patch _REDIRECTS with a stdlib target and reinstall the finder."""
30 monkeypatch.setattr(
31 "vivarium_compat._compat._REDIRECTS", {"_test_old_json": "json"}
32 )
33 sys.meta_path[:] = [f for f in sys.meta_path if not isinstance(f, _CompatFinder)]
34 install_compat_finder()
37def test_install_is_idempotent():
38 install_compat_finder()
39 install_compat_finder()
40 assert sum(1 for f in sys.meta_path if isinstance(f, _CompatFinder)) == 1
43def test_finder_ignores_unknown_modules():
44 assert _CompatFinder().find_spec("some_unknown_module", None) is None
47def test_redirect_resolves_to_target(patched_redirects):
48 result = importlib.import_module("_test_old_json")
49 import json
51 assert result is json
54def test_deprecation_warning_emitted(patched_redirects):
55 with pytest.warns(DeprecationWarning, match="_test_old_json.*json"):
56 importlib.import_module("_test_old_json")
59def test_no_warning_on_subsequent_import(patched_redirects):
60 # First import populates sys.modules; suppress its warning.
61 with warnings_module.catch_warnings():
62 warnings_module.simplefilter("ignore")
63 importlib.import_module("_test_old_json")
65 # Second import hits sys.modules cache — find_spec is never called.
66 with warnings_module.catch_warnings(record=True) as caught:
67 warnings_module.simplefilter("always")
68 importlib.import_module("_test_old_json")
70 assert not any(issubclass(w.category, DeprecationWarning) for w in caught)
73def test_prefix_matches_submodule(patched_redirects):
74 spec = _CompatFinder().find_spec("_test_old_json.encoder", None)
75 assert spec is not None
76 assert spec.loader._new_name == "json.encoder" # type: ignore[union-attr]
79def test_prefix_does_not_match_superstring(patched_redirects):
80 finder = _CompatFinder()
81 assert finder.find_spec("_test_old_json_extra", None) is None
82 assert finder.find_spec("_test_old_json2", None) is None
85def test_circular_redirect_raises_import_error():
86 """Guard prevents infinite recursion when a redirect target re-triggers the old name."""
87 loader = _CompatLoader("_test_circular", "_test_circular_target")
88 _resolving.add("_test_circular")
89 try:
90 with pytest.raises(
91 ImportError,
92 match="Circular redirect detected: '_test_circular' -> '_test_circular_target'",
93 ):
94 loader.exec_module(ModuleType("_test_circular"))
95 finally:
96 _resolving.discard("_test_circular")
99def test_circular_guard_cleans_up_on_success():
100 """_resolving must not retain entries after a successful redirect."""
101 loader = _CompatLoader("_test_clean_old", "json")
102 loader.exec_module(ModuleType("_test_clean_old"))
103 assert "_test_clean_old" not in _resolving
106def test_error_when_neither_target_nor_old_name_exists(monkeypatch):
107 """ModuleNotFoundError surfaces only when both new target and old name are missing."""
108 monkeypatch.setattr(
109 "vivarium_compat._compat._REDIRECTS",
110 {"_nonexistent_old": "_nonexistent_new_xyz_abc"},
111 )
112 sys.meta_path[:] = [f for f in sys.meta_path if not isinstance(f, _CompatFinder)]
113 install_compat_finder()
115 with pytest.raises(ModuleNotFoundError):
116 with pytest.warns(DeprecationWarning):
117 importlib.import_module("_nonexistent_old")
120def test_falls_back_when_target_missing_but_old_name_exists(monkeypatch):
121 """If the redirect target is not installed, fall back to the old name's real module.
123 This makes redirect entries safe to ship ahead of their target packages: existing
124 code that imports the old name keeps working against the still-on-disk old package,
125 with the DeprecationWarning fired to nudge migration.
126 """
127 monkeypatch.setattr(
128 "vivarium_compat._compat._REDIRECTS",
129 {"json": "_nonexistent_new_target_xyz"},
130 )
131 sys.meta_path[:] = [f for f in sys.meta_path if not isinstance(f, _CompatFinder)]
132 install_compat_finder()
134 # Drop the cached `json` so the hook actually fires on next import.
135 sys.modules.pop("json", None)
137 with pytest.warns(DeprecationWarning):
138 result = importlib.import_module("json")
140 # Fallback resolved to the real json module despite the (missing) redirect target.
141 assert hasattr(result, "loads")
142 assert hasattr(result, "dumps")