Coverage for Users / vladimirpavlov / PycharmProjects / parameterizable / src / mixinforge / cacheable_properties_mixin.py: 100%
71 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
1"""Mixin for managing cached properties with automatic discovery and invalidation.
3This module provides CacheablePropertiesMixin, which adds functionality to track
4and invalidate functools.cached_property attributes across a class hierarchy.
5The mixin enables efficient cache management by automatically discovering
6all cached properties and providing methods to inspect, set, and clear their values.
8Note:
9 CacheablePropertiesMixin is not thread-safe and should not be used with dynamically
10 modified classes. The implementation relies on functools.cached_property
11 internals; any refactoring should begin with reviewing those implementation
12 details.
13"""
14from functools import cached_property, cache
15from typing import Any
18class CacheablePropertiesMixin:
19 """Mixin class for automatic management of cached properties.
21 Provides methods to discover all functools.cached_property attributes
22 in the class hierarchy and to inspect, set, and invalidate their cached
23 values. This enables efficient cache management without manual tracking
24 of individual cached properties.
26 Note:
27 This class is not thread-safe and should not be used with dynamically
28 modified classes.
30 Subclasses using __slots__ MUST include '__dict__' to support
31 functools.cached_property, as enforced by _ensure_cache_storage_supported().
32 """
33 # Use __slots__ = () to prevent implicit addition of __dict__ or __weakref__,
34 # allowing subclasses to use __slots__ for memory optimization.
35 __slots__ = ()
37 def _ensure_cache_storage_supported(self) -> None:
38 """Ensure the instance can store cached_property values.
40 Raises:
41 TypeError: If the instance lacks __dict__, which is required
42 for functools.cached_property storage.
43 """
44 if not hasattr(self, "__dict__"):
45 cls_name = type(self).__name__
46 raise TypeError(
47 f"{cls_name} does not support cached_property caching because "
48 f"it lacks __dict__; add __slots__ = (..., '__dict__') or "
49 f"avoid cached_property on this class.")
52 @property
53 def _all_cached_properties_names(self) -> frozenset[str]:
54 """Names of all cached properties in the class hierarchy.
56 Returns:
57 Frozenset containing names of all functools.cached_property attributes
58 in the current class and all its parents.
59 """
60 self._ensure_cache_storage_supported()
61 return self._get_cached_properties_names_for_class(type(self))
64 @staticmethod
65 @cache
66 def _get_cached_properties_names_for_class(cls: type) -> frozenset[str]:
67 """Discover and cache all cached_property names for a class.
69 Traverses the MRO to find all functools.cached_property attributes,
70 including those wrapped by decorators that properly set __wrapped__.
72 Args:
73 cls: The class to inspect.
75 Returns:
76 Frozenset of cached property names.
78 Note:
79 Detection of wrapped cached_property relies on decorators using
80 functools.wraps or manually setting __wrapped__. Unwrapping is
81 limited to 100 levels to prevent infinite loops.
82 """
83 cached_names: set[str] = set()
84 seen_names: set[str] = set()
86 for curr_cls in cls.mro():
87 for name, attr in curr_cls.__dict__.items():
88 if name in seen_names:
89 continue
90 seen_names.add(name)
92 if isinstance(attr, cached_property):
93 cached_names.add(name)
94 continue
96 # Unwrap decorators to find cached_property
97 candidate = attr
98 for _ in range(100): # Prevent infinite loops
99 wrapped = getattr(candidate, "__wrapped__", None)
100 if wrapped is None:
101 break
102 candidate = wrapped
104 if isinstance(candidate, cached_property):
105 cached_names.add(name)
107 return frozenset(cached_names)
110 def _get_all_cached_properties_status(self) -> dict[str, bool]:
111 """Get caching status for all cached properties.
113 Returns:
114 Dictionary mapping property names to their caching status. True indicates
115 the property has a cached value, False indicates it needs computation.
116 """
117 self._ensure_cache_storage_supported()
119 return {name: name in self.__dict__
120 for name in self._all_cached_properties_names}
123 def _get_all_cached_properties(self) -> dict[str, Any]:
124 """Retrieve currently cached values for all cached properties.
126 Returns:
127 Dictionary mapping property names to their cached values.
128 Only includes properties that currently have cached values.
129 """
130 self._ensure_cache_storage_supported()
132 vars_dict = self.__dict__
133 cached_names = self._all_cached_properties_names
135 return {name: vars_dict[name]
136 for name in cached_names
137 if name in vars_dict}
140 def _get_cached_property(self, name: str) -> Any:
141 """Retrieve the cached value for a single cached property.
143 Args:
144 name: The name of the cached property to retrieve.
146 Returns:
147 The cached value for the specified property.
149 Raises:
150 ValueError: If the name is not a recognized cached property.
151 KeyError: If the property exists but doesn't have a cached value yet.
152 """
153 self._ensure_cache_storage_supported()
155 if name not in self._all_cached_properties_names:
156 raise ValueError(
157 f"'{name}' is not a cached property")
159 if name not in self.__dict__:
160 raise KeyError(
161 f"Cached property '{name}' has not been computed yet")
163 return self.__dict__[name]
166 def _get_cached_property_status(self, name: str) -> bool:
167 """Check if a cached property has a cached value.
169 Args:
170 name: The name of the cached property to check.
172 Returns:
173 True if the property has a cached value, False if it needs computation.
175 Raises:
176 ValueError: If the name is not a recognized cached property.
177 """
178 self._ensure_cache_storage_supported()
180 if name not in self._all_cached_properties_names:
181 raise ValueError(
182 f"'{name}' is not a cached property")
184 return name in self.__dict__
187 def _set_cached_properties(self, **names_values: Any) -> None:
188 """Set cached values for cached properties directly.
190 Bypasses property computation by writing values directly to __dict__.
191 This is useful for restoring cached state or for testing purposes.
193 Args:
194 **names_values: Property names as keys and their values to cache.
196 Raises:
197 ValueError: If any provided name is not a recognized cached property.
198 """
199 self._ensure_cache_storage_supported()
201 cached_names = self._all_cached_properties_names
203 invalid_names = [name for name in names_values if name not in cached_names]
204 if invalid_names:
205 raise ValueError(
206 f"Cannot set cached values for non-cached properties: {invalid_names}")
208 vars_dict = self.__dict__
209 for name, value in names_values.items():
210 vars_dict[name] = value
213 def _invalidate_cache(self) -> None:
214 """Clear all cached property values.
216 Removes cached values from __dict__, forcing re-computation on next access.
217 This is more efficient than delattr as it avoids triggering custom
218 __delattr__ logic in subclasses.
219 """
220 self._ensure_cache_storage_supported()
222 vars_dict = self.__dict__
223 cached_names = self._all_cached_properties_names
225 keys_to_delete = [k for k in vars_dict if k in cached_names]
227 for name in keys_to_delete:
228 if name in vars_dict:
229 del vars_dict[name]