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

1"""Slot-backed caching for frozen, slotted dataclasses. 

2 

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. 

9 

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""" 

14 

15from __future__ import annotations 

16 

17import functools 

18from collections.abc import Callable 

19from typing import Any, TypeVar 

20 

21T = TypeVar("T") 

22 

23 

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*. 

26 

27 Apply below ``@property`` so the property getter is the wrapped function: 

28 

29 ```python 

30 @property 

31 @cached_in_slot("_profits_cache") 

32 def profits(self) -> pl.DataFrame: ... 

33 ``` 

34 

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". 

40 

41 Returns: 

42 A decorator that wraps the getter with read-through caching. 

43 """ 

44 

45 def decorator(fn: Callable[[Any], T]) -> Callable[[Any], T]: 

46 """Wrap *fn* with read-through caching against the configured slot.""" 

47 

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 

59 

60 return wrapper 

61 

62 return decorator