Coverage for tests / test_compat.py: 100%

76 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-18 19:31 +0000

1import importlib 

2import sys 

3import warnings as warnings_module 

4from types import ModuleType 

5 

6import pytest 

7from vivarium_compat._compat import ( 

8 _CompatFinder, 

9 _CompatLoader, 

10 _resolving, 

11 install_compat_finder, 

12) 

13 

14 

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] 

25 

26 

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() 

35 

36 

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 

41 

42 

43def test_finder_ignores_unknown_modules(): 

44 assert _CompatFinder().find_spec("some_unknown_module", None) is None 

45 

46 

47def test_redirect_resolves_to_target(patched_redirects): 

48 result = importlib.import_module("_test_old_json") 

49 import json 

50 

51 assert result is json 

52 

53 

54def test_deprecation_warning_emitted(patched_redirects): 

55 with pytest.warns(DeprecationWarning, match="_test_old_json.*json"): 

56 importlib.import_module("_test_old_json") 

57 

58 

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") 

64 

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") 

69 

70 assert not any(issubclass(w.category, DeprecationWarning) for w in caught) 

71 

72 

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] 

77 

78 

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 

83 

84 

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") 

97 

98 

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 

104 

105 

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() 

114 

115 with pytest.raises(ModuleNotFoundError): 

116 with pytest.warns(DeprecationWarning): 

117 importlib.import_module("_nonexistent_old") 

118 

119 

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. 

122 

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() 

133 

134 # Drop the cached `json` so the hook actually fires on next import. 

135 sys.modules.pop("json", None) 

136 

137 with pytest.warns(DeprecationWarning): 

138 result = importlib.import_module("json") 

139 

140 # Fallback resolved to the real json module despite the (missing) redirect target. 

141 assert hasattr(result, "loads") 

142 assert hasattr(result, "dumps")