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

1 from functools import cached_property

2 from mixinforge import CacheablePropertiesMixin

3 import pytest

4

5 class 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

21 def 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

34 def 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

54 def 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

81 def 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

106 def 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

124 def 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

156 def 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

182 def 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

226 def 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

243 def 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__