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
« 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
5class A(CacheablePropertiesMixin):
6 @cached_property
7 def x(self):
8 return 1
10 @cached_property
11 def y(self):
12 return 2
14 @property
15 def z(self):
16 return 3
18 def regular_method(self):
19 pass
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")
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
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
45 a._invalidate_cache()
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
54def test_invalidate_cache_idempotency():
55 """Test that calling _invalidate_cache() multiple times is safe."""
56 a = A()
58 # Cache some properties
59 _ = a.x
60 _ = a.y
61 assert "x" in a.__dict__
62 assert "y" in a.__dict__
64 # First invalidation
65 a._invalidate_cache()
66 assert "x" not in a.__dict__
67 assert "y" not in a.__dict__
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__
74 # Third invalidation should also be safe
75 a._invalidate_cache()
77 # Properties should still be recomputable
78 assert a.x == 1
79 assert a.y == 2
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
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
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
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
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
112 e = Empty()
113 names = e._all_cached_properties_names
114 assert isinstance(names, frozenset)
115 assert len(names) == 0
117 # Should not raise, even with no cached properties
118 e._invalidate_cache()
120 status = e._get_all_cached_properties_status()
121 assert isinstance(status, dict)
122 assert len(status) == 0
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")
131 @cached_property
132 def working_prop(self):
133 return 42
135 r = RaisingProp()
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
141 # Access working property
142 assert r.working_prop == 42
143 assert "working_prop" in r.__dict__
145 # failing_prop should raise
146 with pytest.raises(ValueError, match="computation failed"):
147 _ = r.failing_prop
149 # failing_prop should not be cached (cached_property only caches on success)
150 assert "failing_prop" not in r.__dict__
152 # Invalidate should work regardless
153 r._invalidate_cache()
154 assert "working_prop" not in r.__dict__
156def test_inheritance():
157 class Base(CacheablePropertiesMixin):
158 @cached_property
159 def base_prop(self):
160 return "base"
162 class Child(Base):
163 @cached_property
164 def child_prop(self):
165 return "child"
167 c = Child()
168 names = c._all_cached_properties_names
169 assert "base_prop" in names
170 assert "child_prop" in names
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__
178 c._invalidate_cache()
179 assert "base_prop" not in c.__dict__
180 assert "child_prop" not in c.__dict__
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"
189 class Left(Base):
190 @cached_property
191 def left_prop(self):
192 return "left"
194 class Right(Base):
195 @cached_property
196 def right_prop(self):
197 return "right"
199 class Diamond(Left, Right):
200 @cached_property
201 def diamond_prop(self):
202 return "diamond"
204 d = Diamond()
205 names = d._all_cached_properties_names
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
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"
219 # All should be cached
220 assert all(name in d.__dict__ for name in names)
222 # Invalidate should clear all
223 d._invalidate_cache()
224 assert all(name not in d.__dict__ for name in names)
226def test_slots_without_dict():
227 """Test that classes with __slots__ but no __dict__ raise clear errors."""
228 class SlotsOnly(CacheablePropertiesMixin):
229 __slots__ = ("_val",)
231 @cached_property
232 def val(self):
233 return 1
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
240 with pytest.raises(TypeError, match="lacks __dict__"):
241 s._invalidate_cache()
243def test_slots_with_dict():
244 class SlotsWithDict(CacheablePropertiesMixin):
245 __slots__ = ("__dict__",)
247 @cached_property
248 def val(self):
249 return 42
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__