Coverage for src/jquantstats/_cache.py: 100%
16 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-23 06:13 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-23 06:13 +0000
1"""Slot-backed caching for frozen, slotted dataclasses.
3`Portfolio` is a frozen dataclass with ``slots=True``, so neither
4`functools.cached_property` (needs ``__dict__``) nor plain attribute
5assignment (frozen) works for memoising derived values. Instead, every cache
6lives in an explicitly declared slot field that ``__post_init__`` initialises
7to ``None``, and `cached_in_slot` fills it via ``object.__setattr__`` on
8first access.
10Caching is not thread-safe: concurrent first accesses may compute the value
11redundantly, but never produce incorrect results because every thread stores
12the same deterministic value.
13"""
15from __future__ import annotations
17import functools
18from collections.abc import Callable
19from typing import Any, TypeVar
21T = TypeVar("T")
24def cached_in_slot(slot: str) -> Callable[[Callable[[Any], T]], Callable[[Any], T]]:
25 """Cache a zero-argument method's result in the slot field named *slot*.
27 Apply below ``@property`` so the property getter is the wrapped function:
29 ```python
30 @property
31 @cached_in_slot("_profits_cache")
32 def profits(self) -> pl.DataFrame: ...
33 ```
35 Args:
36 slot: Name of the declared slot field used as the cache. The field
37 must be initialised to ``None`` before first access (Portfolio
38 does this in ``__post_init__``); a ``None`` value means
39 "not yet computed".
41 Returns:
42 A decorator that wraps the getter with read-through caching.
43 """
45 def decorator(fn: Callable[[Any], T]) -> Callable[[Any], T]:
46 """Wrap *fn* with read-through caching against the configured slot."""
48 @functools.wraps(fn)
49 def wrapper(self: Any) -> T:
50 """Return the cached value, computing and storing it on first access."""
51 cache = getattr(self, slot, None)
52 if cache is None:
53 cache = fn(self)
54 # Direct write is safe: the owner is a frozen, slotted
55 # dataclass that declares every cache field, so
56 # object.__setattr__ cannot fail here.
57 object.__setattr__(self, slot, cache)
58 return cache
60 return wrapper
62 return decorator