Coverage for Users / vladimirpavlov / PycharmProjects / parameterizable / tests / test_cacheable_properties_mixin_core.py: 98%

171 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-01 16:37 -0600

1from functools import cached_property 

2from mixinforge import CacheablePropertiesMixin 

3import pytest 

4 

5class A(CacheablePropertiesMixin): 

6 @cached_property 

7 def x(self): 

8 return 1 

9 

10 @cached_property 

11 def y(self): 

12 return 2 

13 

14 @property 

15 def z(self): 

16 return 3 

17 

18 def regular_method(self): 

19 pass 

20 

21def test_cached_properties_names_existence(): 

22 a = A() 

23 # The property is protected: _all_cached_properties_names 

24 assert hasattr(a, "_all_cached_properties_names") 

25 

26 names = a._all_cached_properties_names 

27 # Contract: must return frozenset specifically 

28 assert isinstance(names, frozenset) 

29 assert "x" in names 

30 assert "y" in names 

31 # Regular @property should not be included 

32 assert "z" not in names 

33 

34def test_invalidate_cache(): 

35 """Test that _invalidate_cache() removes all cached values from __dict__.""" 

36 a = A() 

37 # Access properties to cache them 

38 assert a.x == 1 

39 assert a.y == 2 

40 assert a.z == 3 # regular property 

41 assert "x" in a.__dict__ 

42 assert "y" in a.__dict__ 

43 assert "z" not in a.__dict__ # regular property doesn't cache 

44 

45 a._invalidate_cache() 

46 

47 assert "x" not in a.__dict__ 

48 assert "y" not in a.__dict__ 

49 # Verify they can be recomputed 

50 assert a.x == 1 

51 # Regular property still works 

52 assert a.z == 3 

53 

54def test_invalidate_cache_idempotency(): 

55 """Test that calling _invalidate_cache() multiple times is safe.""" 

56 a = A() 

57 

58 # Cache some properties 

59 _ = a.x 

60 _ = a.y 

61 assert "x" in a.__dict__ 

62 assert "y" in a.__dict__ 

63 

64 # First invalidation 

65 a._invalidate_cache() 

66 assert "x" not in a.__dict__ 

67 assert "y" not in a.__dict__ 

68 

69 # Second invalidation should be safe (no error) 

70 a._invalidate_cache() 

71 assert "x" not in a.__dict__ 

72 assert "y" not in a.__dict__ 

73 

74 # Third invalidation should also be safe 

75 a._invalidate_cache() 

76 

77 # Properties should still be recomputable 

78 assert a.x == 1 

79 assert a.y == 2 

80 

81def test_status_reporting(): 

82 a = A() 

83 # Initially nothing cached 

84 status = a._get_all_cached_properties_status() 

85 assert status["x"] is False 

86 assert status["y"] is False 

87 

88 # Cache x 

89 _ = a.x 

90 status = a._get_all_cached_properties_status() 

91 assert status["x"] is True 

92 assert status["y"] is False 

93 

94 # Cache y 

95 _ = a.y 

96 status = a._get_all_cached_properties_status() 

97 assert status["x"] is True 

98 assert status["y"] is True 

99 

100 # Invalidate 

101 a._invalidate_cache() 

102 status = a._get_all_cached_properties_status() 

103 assert status["x"] is False 

104 assert status["y"] is False 

105 

106def test_empty_class(): 

107 """Test that classes with no cached properties work correctly.""" 

108 class Empty(CacheablePropertiesMixin): 

109 def regular_method(self): 

110 return 1 

111 

112 e = Empty() 

113 names = e._all_cached_properties_names 

114 assert isinstance(names, frozenset) 

115 assert len(names) == 0 

116 

117 # Should not raise, even with no cached properties 

118 e._invalidate_cache() 

119 

120 status = e._get_all_cached_properties_status() 

121 assert isinstance(status, dict) 

122 assert len(status) == 0 

123 

124def test_property_that_raises(): 

125 """Test that cached properties raising exceptions don't break cache management.""" 

126 class RaisingProp(CacheablePropertiesMixin): 

127 @cached_property 

128 def failing_prop(self): 

129 raise ValueError("computation failed") 

130 

131 @cached_property 

132 def working_prop(self): 

133 return 42 

134 

135 r = RaisingProp() 

136 

137 # Discover properties even if they raise 

138 assert "failing_prop" in r._all_cached_properties_names 

139 assert "working_prop" in r._all_cached_properties_names 

140 

141 # Access working property 

142 assert r.working_prop == 42 

143 assert "working_prop" in r.__dict__ 

144 

145 # failing_prop should raise 

146 with pytest.raises(ValueError, match="computation failed"): 

147 _ = r.failing_prop 

148 

149 # failing_prop should not be cached (cached_property only caches on success) 

150 assert "failing_prop" not in r.__dict__ 

151 

152 # Invalidate should work regardless 

153 r._invalidate_cache() 

154 assert "working_prop" not in r.__dict__ 

155 

156def test_inheritance(): 

157 class Base(CacheablePropertiesMixin): 

158 @cached_property 

159 def base_prop(self): 

160 return "base" 

161 

162 class Child(Base): 

163 @cached_property 

164 def child_prop(self): 

165 return "child" 

166 

167 c = Child() 

168 names = c._all_cached_properties_names 

169 assert "base_prop" in names 

170 assert "child_prop" in names 

171 

172 # Access to cache 

173 assert c.base_prop == "base" 

174 assert c.child_prop == "child" 

175 assert "base_prop" in c.__dict__ 

176 assert "child_prop" in c.__dict__ 

177 

178 c._invalidate_cache() 

179 assert "base_prop" not in c.__dict__ 

180 assert "child_prop" not in c.__dict__ 

181 

182def test_multiple_inheritance_diamond(): 

183 """Test that cached properties are discovered correctly in diamond inheritance.""" 

184 class Base(CacheablePropertiesMixin): 

185 @cached_property 

186 def base_prop(self): 

187 return "base" 

188 

189 class Left(Base): 

190 @cached_property 

191 def left_prop(self): 

192 return "left" 

193 

194 class Right(Base): 

195 @cached_property 

196 def right_prop(self): 

197 return "right" 

198 

199 class Diamond(Left, Right): 

200 @cached_property 

201 def diamond_prop(self): 

202 return "diamond" 

203 

204 d = Diamond() 

205 names = d._all_cached_properties_names 

206 

207 # All properties should be discovered exactly once 

208 assert "base_prop" in names 

209 assert "left_prop" in names 

210 assert "right_prop" in names 

211 assert "diamond_prop" in names 

212 

213 # Access all properties 

214 assert d.base_prop == "base" 

215 assert d.left_prop == "left" 

216 assert d.right_prop == "right" 

217 assert d.diamond_prop == "diamond" 

218 

219 # All should be cached 

220 assert all(name in d.__dict__ for name in names) 

221 

222 # Invalidate should clear all 

223 d._invalidate_cache() 

224 assert all(name not in d.__dict__ for name in names) 

225 

226def test_slots_without_dict(): 

227 """Test that classes with __slots__ but no __dict__ raise clear errors.""" 

228 class SlotsOnly(CacheablePropertiesMixin): 

229 __slots__ = ("_val",) 

230 

231 @cached_property 

232 def val(self): 

233 return 1 

234 

235 s = SlotsOnly() 

236 # Should raise TypeError because __dict__ is missing 

237 with pytest.raises(TypeError, match="lacks __dict__"): 

238 _ = s._all_cached_properties_names 

239 

240 with pytest.raises(TypeError, match="lacks __dict__"): 

241 s._invalidate_cache() 

242 

243def test_slots_with_dict(): 

244 class SlotsWithDict(CacheablePropertiesMixin): 

245 __slots__ = ("__dict__",) 

246 

247 @cached_property 

248 def val(self): 

249 return 42 

250 

251 s = SlotsWithDict() 

252 assert s.val == 42 

253 assert "val" in s._all_cached_properties_names 

254 assert s._get_all_cached_properties_status()["val"] is True 

255 s._invalidate_cache() 

256 assert "val" not in s.__dict__