kapro 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kapro/__init__.py ADDED
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypeVar, final, overload, override
4
+
5
+ from kapro.cached import (
6
+ cached_property,
7
+ class_cached_property,
8
+ mixed_cached_property,
9
+ post_cached_property,
10
+ pre_cached_property,
11
+ )
12
+ from kapro.class_property import class_property, mixed_property
13
+ from kapro.primitives import (
14
+ ContextFaultError,
15
+ Nothing,
16
+ bound_property,
17
+ )
18
+
19
+ T_co = TypeVar("T_co", covariant=True)
20
+
21
+
22
+ @final
23
+ class pin[T_co](bound_property[T_co]): # noqa: N801
24
+ native = cached_property
25
+ cls = class_cached_property
26
+ any = mixed_cached_property
27
+ pre = pre_cached_property
28
+ post = post_cached_property
29
+
30
+ @overload
31
+ def __get__(
32
+ self,
33
+ node: None,
34
+ klass: Any = ..., # noqa: ANN401
35
+ ) -> pin[T_co]: ...
36
+
37
+ @overload
38
+ def __get__(
39
+ self,
40
+ node: object,
41
+ klass: Any = ..., # noqa: ANN401
42
+ ) -> T_co: ...
43
+
44
+ @override
45
+ def __get__(
46
+ self,
47
+ node: object | None,
48
+ klass: Any = Nothing,
49
+ ) -> pin[T_co] | T_co:
50
+ if node is None:
51
+ raise ContextFaultError(self.header_with_context(klass))
52
+
53
+ cache = getattr(node, "__dict__", None)
54
+ if cache is None:
55
+ raise TypeError(
56
+ f"{self.header_with_context(node)} has no __dict__",
57
+ )
58
+
59
+ try:
60
+ return cache[self.name] # type: ignore[no-any-return, index]
61
+
62
+ except KeyError:
63
+ value = self.function(node)
64
+ cache[self.name] = value # type: ignore[index]
65
+ return value # type: ignore[no-any-return]
66
+
67
+
68
+ __all__ = (
69
+ "class_property",
70
+ "mixed_property",
71
+ "pin",
72
+ )
@@ -0,0 +1,29 @@
1
+ from kapro.cached.instance import cached_property
2
+ from kapro.cached.klass import (
3
+ class_cached_property,
4
+ class_parent_cached_property,
5
+ )
6
+ from kapro.cached.mixed import (
7
+ mixed_cached_property,
8
+ mixed_parent_cached_property,
9
+ )
10
+ from kapro.cached.post import (
11
+ post_cached_property,
12
+ post_parent_cached_property,
13
+ )
14
+ from kapro.cached.pre import (
15
+ pre_cached_property,
16
+ pre_parent_cached_property,
17
+ )
18
+
19
+ __all__ = (
20
+ "cached_property",
21
+ "class_cached_property",
22
+ "class_parent_cached_property",
23
+ "mixed_cached_property",
24
+ "mixed_parent_cached_property",
25
+ "post_cached_property",
26
+ "post_parent_cached_property",
27
+ "pre_cached_property",
28
+ "pre_parent_cached_property",
29
+ )
@@ -0,0 +1,108 @@
1
+ """Instance-level cached property descriptor.
2
+
3
+ This module defines :class:`cached_property` — the caching equivalent of
4
+ :class:`kapro.primitives.bound_property`. It stores computed
5
+ values in a per-instance dictionary named ``__instance_memoized__`` and
6
+ supports parent-aware resolution (via inheritance from
7
+ :class:`class_parent_cached_property`).
8
+ """
9
+
10
+ # ruff: noqa: ANN401, N801
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Callable
15
+ from contextlib import suppress
16
+ from functools import cached_property as base_cached_property
17
+ from types import NoneType
18
+ from typing import (
19
+ TYPE_CHECKING,
20
+ Any,
21
+ ClassVar,
22
+ TypeVar,
23
+ cast,
24
+ overload,
25
+ override,
26
+ )
27
+
28
+ from kain import Is, Who
29
+
30
+ from kapro.cached.klass import class_parent_cached_property
31
+ from kapro.primitives import ContextFaultError, parent_call
32
+
33
+ __all__ = ("cached_property",)
34
+
35
+ T_co = TypeVar("T_co", covariant=True)
36
+ R = TypeVar("R")
37
+
38
+
39
+ class cached_property[T_co](class_parent_cached_property[T_co]):
40
+ klass: ClassVar[bool | NoneType] = False
41
+
42
+ @base_cached_property
43
+ @override
44
+ def title(self) -> str:
45
+ return f"instance data-descriptor {Who.Addr(self)}".strip()
46
+
47
+ @override
48
+ def get_node(self, node: Any) -> Any:
49
+ if node is None or Is.Class(node):
50
+ msg = f"{self.header_with_context(node)}, node={node!r}"
51
+ if node is None and (not self.klass):
52
+ msg = f"{msg}; looks like a non-instance invocation"
53
+ raise ContextFaultError(msg)
54
+ return node
55
+
56
+ @override
57
+ def get_cache(self, node: Any) -> dict[str, Any]:
58
+ self.get_node(node)
59
+ name = "__instance_memoized__"
60
+ cache: dict[str, Any] = {}
61
+
62
+ if hasattr(node, "__dict__"):
63
+ with suppress(KeyError):
64
+ return node.__dict__[name]
65
+ setattr(node, name, cache)
66
+
67
+ return cache
68
+
69
+ @overload
70
+ def __get__(
71
+ self,
72
+ instance: None,
73
+ klass: Any = ...,
74
+ ) -> cached_property[T_co]: ...
75
+
76
+ @overload
77
+ def __get__(
78
+ self,
79
+ instance: object,
80
+ klass: Any = ...,
81
+ ) -> T_co: ...
82
+
83
+ @override
84
+ def __get__(
85
+ self,
86
+ instance: object | None,
87
+ klass: Any = None,
88
+ ) -> cached_property[T_co] | T_co:
89
+ if instance is None:
90
+ raise ContextFaultError(self.header_with_context(klass))
91
+ return self.call(instance)
92
+
93
+ if TYPE_CHECKING:
94
+
95
+ @staticmethod
96
+ @override
97
+ def with_parent(function: Callable[..., R]) -> cached_property[R]: ...
98
+
99
+
100
+ def _cached_property_with_parent[R](
101
+ function: Callable[..., R],
102
+ ) -> cached_property[R]:
103
+ return cached_property(cast("Callable[[Any], R]", parent_call(function)))
104
+
105
+
106
+ cached_property.with_parent = staticmethod( # type: ignore[method-assign]
107
+ _cached_property_with_parent,
108
+ )
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from functools import cached_property as base_cached_property
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ TypeVar,
9
+ overload,
10
+ override,
11
+ )
12
+
13
+ from kapro.cached.klass import class_parent_cached_property
14
+
15
+ __all__ = ("cached_property",)
16
+ T_co: Any
17
+ R = TypeVar("R")
18
+
19
+ class cached_property[T_co](class_parent_cached_property[T_co]):
20
+ @base_cached_property
21
+ @override
22
+ def title(self) -> str: ...
23
+ @override
24
+ def get_node(self, node: Any) -> Any: ...
25
+ @override
26
+ def get_cache(self, node: Any) -> dict[str, Any]: ...
27
+ @overload
28
+ def __get__(
29
+ self,
30
+ instance: None,
31
+ klass: Any = ...,
32
+ ) -> cached_property[T_co]: ...
33
+ @overload
34
+ def __get__(
35
+ self,
36
+ instance: object,
37
+ klass: Any = ...,
38
+ ) -> T_co: ...
39
+ @override # type: ignore[misc]
40
+ def __get__(
41
+ self,
42
+ instance: object | None,
43
+ klass: Any = None,
44
+ ) -> cached_property[T_co] | T_co: ...
45
+
46
+ if TYPE_CHECKING:
47
+ @staticmethod
48
+ @override
49
+ def with_parent(function: Callable[..., R]) -> cached_property[R]: ...
50
+
51
+ def _cached_property_with_parent[R](
52
+ function: Callable[..., R],
53
+ ) -> cached_property[R]: ...
kapro/cached/klass.py ADDED
@@ -0,0 +1,240 @@
1
+ """Class-level cached property descriptors.
2
+
3
+ This module implements the ``class_cached_property`` family. Both variants
4
+ store their cache in a dictionary named ``__class_memoized__`` attached to
5
+ the class object (either the concrete class or the owning parent class,
6
+ depending on the variant).
7
+
8
+ In addition, :class:`CustomCallbackMixin` provides a pluggable cache
9
+ invalidation mechanism via the ``is_actual`` callback.
10
+ """
11
+
12
+ # ruff: noqa: ANN401, N801
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Callable
17
+ from contextlib import suppress
18
+ from functools import cached_property, partial
19
+ from time import time
20
+ from typing import TYPE_CHECKING, Any, TypeVar, cast, override
21
+
22
+ from kain import Is, Who
23
+ from kain.internals import get_owner
24
+
25
+ from kapro.primitives import (
26
+ AttributeExceptionError,
27
+ BaseProperty,
28
+ ContextFaultError,
29
+ Nothing,
30
+ parent_call,
31
+ )
32
+
33
+ __all__ = ("class_cached_property", "class_parent_cached_property")
34
+
35
+ T_co = TypeVar("T_co", covariant=True)
36
+ R = TypeVar("R")
37
+
38
+
39
+ class CustomCallbackMixin:
40
+ """Mixin that adds TTL and custom callback support to cached descriptors.
41
+
42
+ The mixin is designed to be combined with a descriptor class (e.g.
43
+ ``class_parent_cached_property``). It exposes two factory classmethods:
44
+
45
+ * :meth:`by` / :meth:`expired_by` - supply your own ``is_actual``
46
+ predicate.
47
+ * :meth:`ttl` - supply a numeric lifetime in seconds.
48
+
49
+ ``is_actual`` signature
50
+ -----------------------
51
+ ``is_actual(self, node, stamp) -> bool | float``
52
+
53
+ * When called **without** ``stamp`` (during ``__set__``), it should
54
+ return the *new* stamp to store alongside the cached value.
55
+ * When called **with** ``stamp`` (during ``call`` / cache lookup), it
56
+ should return ``True`` if the cached value is still valid.
57
+
58
+ If ``is_actual`` is ``Nothing`` (the default), the cache never expires.
59
+ """
60
+
61
+ @classmethod
62
+ def by(cls, callback: Callable[..., Any]) -> Callable[..., Any]:
63
+ return partial(cls, is_actual=callback) # type: ignore[call-arg]
64
+
65
+ @classmethod
66
+ def ttl(cls, expire: float) -> Callable[..., Any]:
67
+ if not isinstance(expire, float | int): # type: ignore[redundant-expr]
68
+ msg = f"expire must be float or int, not {Who.Cast(expire)}"
69
+ raise TypeError(msg)
70
+ if expire <= 0:
71
+ msg = f"expire must be a positive number, not {Who.Cast(expire)}"
72
+ raise ValueError(msg)
73
+
74
+ def is_actual(_self: Any, _node: Any, value: Any = Nothing) -> Any:
75
+ if isinstance(value, float):
76
+ return value + expire > time()
77
+ return time()
78
+
79
+ return cls.by(is_actual)
80
+
81
+
82
+ class class_parent_cached_property[T_co](
83
+ BaseProperty[T_co],
84
+ CustomCallbackMixin,
85
+ ):
86
+ """Class-level cached descriptor that stores cache on the *owning* class.
87
+
88
+ The *owning* class is determined by :func:`kapro.internals.get_owner`,
89
+ which walks the MRO to find the class that actually defines this
90
+ descriptor. This means that if ``Base`` defines the property and
91
+ ``Child(Base)`` is accessed, the cache lives on ``Base`` — so all
92
+ subclasses share the same cached value.
93
+
94
+ Cache structure
95
+ ---------------
96
+ The cache dict is stored in the class ``__dict__`` under the key
97
+ ``__class_memoized__``. Each entry is keyed by the property ``name``.
98
+
99
+ * Without ``is_actual`` → ``cache[name] = value``
100
+ * With ``is_actual`` → ``cache[name] = (value, stamp)``
101
+ """
102
+
103
+ def __init__(
104
+ self,
105
+ function: Callable[[Any], T_co],
106
+ is_actual: Any = Nothing,
107
+ ) -> None:
108
+ super().__init__(function)
109
+ if method := getattr(Is.classOf(self), "is_actual", None):
110
+ if is_actual:
111
+ msg = (
112
+ f"{Who.Is(self)}.is_actual method ({Who.Cast(method)}) "
113
+ "can't be overridden by the is_actual keyword argument"
114
+ )
115
+ raise TypeError(msg)
116
+ is_actual = method
117
+ self.is_actual: Any = is_actual
118
+
119
+ @cached_property
120
+ @override
121
+ def title(self) -> str:
122
+ return f"class data-descriptor {Who.Addr(self)}".strip()
123
+
124
+ @override
125
+ def header_with_context(self, node: Any) -> str:
126
+ return self.footer(node)
127
+
128
+ def get_node(self, node: Any) -> Any:
129
+ if node is None or not Is.Class(node):
130
+ msg = f"{self.header_with_context(node)}, node={node!r}"
131
+ raise ContextFaultError(msg)
132
+ result = get_owner(node, self.name)
133
+ return result if result is not None else node
134
+
135
+ def get_cache(self, node: Any) -> dict[str, Any]:
136
+ self.get_node(node)
137
+ name = "__class_memoized__"
138
+ if hasattr(node, "__dict__"):
139
+ with suppress(KeyError):
140
+ return cast("dict[str, Any]", node.__dict__[name])
141
+ cache: dict[str, Any] = {}
142
+ setattr(node, name, cache)
143
+ return cache
144
+
145
+ @override
146
+ def call(self, node: Any, *args: Any, **kw: Any) -> T_co:
147
+ pivot = self.get_node(node)
148
+ with suppress(KeyError):
149
+ stored = self.get_cache(pivot)[self.name]
150
+ if not self.is_actual:
151
+ return stored
152
+ value, stamp = stored
153
+ if self.is_actual(self, pivot, stamp) is True:
154
+ return value
155
+ try:
156
+ value = self.function(node)
157
+ except AttributeError as e:
158
+ raise AttributeExceptionError(e) from e
159
+ return self.__set__(pivot, value)
160
+
161
+ def __get__(self, instance: Any, klass: Any = None) -> T_co:
162
+ return self.call(klass)
163
+
164
+ def __set__(self, node: Any, value: Any) -> Any:
165
+ cache = self.get_cache(node)
166
+ if not self.is_actual:
167
+ cache[self.name] = value
168
+ else:
169
+ cache[self.name] = (value, self.is_actual(self, node))
170
+ return value
171
+
172
+ def __delete__(self, node: Any) -> None:
173
+ cache = self.get_cache(node)
174
+ with suppress(KeyError):
175
+ del cache[self.name]
176
+
177
+ if TYPE_CHECKING:
178
+
179
+ @staticmethod
180
+ @override
181
+ def with_parent( # pyright: ignore[reportIncompatibleMethodOverride]
182
+ function: Callable[..., R],
183
+ ) -> class_parent_cached_property[R]: ...
184
+
185
+
186
+ def _class_parent_cached_property_with_parent[R](
187
+ function: Callable[..., R],
188
+ ) -> class_parent_cached_property[R]:
189
+ return class_parent_cached_property(
190
+ cast("Callable[[Any], R]", parent_call(function)),
191
+ )
192
+
193
+
194
+ class_parent_cached_property.with_parent = staticmethod( # type: ignore[method-assign]
195
+ _class_parent_cached_property_with_parent,
196
+ )
197
+
198
+
199
+ class class_cached_property[T_co](
200
+ class_parent_cached_property[T_co],
201
+ ):
202
+ """Class-level cached descriptor that stores cache on the accessed class.
203
+
204
+ This is the "plain" variant of ``class_parent_cached_property``. Rather
205
+ than resolving the *owning* parent via ``get_owner``, it caches directly
206
+ on ``node`` (the class that was passed to ``__get__``). Consequently,
207
+ each subclass maintains its own independent cache.
208
+ """
209
+
210
+ @override
211
+ def get_node(self, node: Any) -> Any:
212
+ if node is None or not Is.Class(node):
213
+ msg = f"{self.header_with_context(node)}, node={node!r}"
214
+ raise ContextFaultError(msg)
215
+ return node
216
+
217
+ @class_parent_cached_property
218
+ def here(cls) -> type[class_parent_cached_property[Any]]: # noqa: N805
219
+ return class_parent_cached_property
220
+
221
+ if TYPE_CHECKING:
222
+
223
+ @staticmethod
224
+ @override
225
+ def with_parent(
226
+ function: Callable[..., R],
227
+ ) -> class_cached_property[R]: ...
228
+
229
+
230
+ def _class_cached_property_with_parent[R](
231
+ function: Callable[..., R],
232
+ ) -> class_cached_property[R]:
233
+ return class_cached_property(
234
+ cast("Callable[[Any], R]", parent_call(function)),
235
+ )
236
+
237
+
238
+ class_cached_property.with_parent = staticmethod( # type: ignore[method-assign]
239
+ _class_cached_property_with_parent,
240
+ )
kapro/cached/klass.pyi ADDED
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from functools import cached_property
5
+ from typing import TYPE_CHECKING, Any, TypeVar, override
6
+
7
+ from kapro.primitives import (
8
+ BaseProperty,
9
+ Nothing,
10
+ )
11
+
12
+ __all__ = ("class_cached_property", "class_parent_cached_property")
13
+ T_co: Any
14
+ R = TypeVar("R")
15
+
16
+ class CustomCallbackMixin:
17
+ @classmethod
18
+ def by(cls, callback: Callable[..., Any]) -> Callable[..., Any]: ...
19
+ @classmethod
20
+ def ttl(cls, expire: float) -> Callable[..., Any]: ...
21
+
22
+ class class_parent_cached_property[T_co](
23
+ BaseProperty[T_co],
24
+ CustomCallbackMixin,
25
+ ):
26
+ def __init__(
27
+ self: Any,
28
+ function: Callable[[Any], T_co],
29
+ is_actual: Any = Nothing,
30
+ ) -> None: ...
31
+ @cached_property
32
+ @override
33
+ def title(self) -> str: ...
34
+ @override
35
+ def header_with_context(self, node: Any) -> str: ...
36
+ def get_node(self, node: Any) -> Any: ...
37
+ def get_cache(self, node: Any) -> dict[str, Any]: ...
38
+ @override
39
+ def call(self, node: Any, *args: Any, **kw: Any) -> T_co: ...
40
+ def __get__(self, instance: Any, klass: Any = None) -> T_co: ...
41
+ def __set__(self, node: Any, value: Any) -> Any: ...
42
+ def __delete__(self, node: Any) -> None: ...
43
+
44
+ if TYPE_CHECKING:
45
+ @staticmethod
46
+ @override
47
+ def with_parent( # pyright: ignore[reportIncompatibleMethodOverride]
48
+ function: Callable[..., R],
49
+ ) -> class_parent_cached_property[R]: ...
50
+
51
+ def _class_parent_cached_property_with_parent[R](
52
+ function: Callable[..., R],
53
+ ) -> class_parent_cached_property[R]: ...
54
+
55
+ class class_cached_property[T_co](
56
+ class_parent_cached_property[T_co],
57
+ ):
58
+ @override
59
+ def get_node(self, node: Any) -> Any: ...
60
+ @class_parent_cached_property
61
+ def here(cls) -> type[class_parent_cached_property[Any]]: ...
62
+
63
+ if TYPE_CHECKING:
64
+ @staticmethod
65
+ @override
66
+ def with_parent(
67
+ function: Callable[..., R],
68
+ ) -> class_cached_property[R]: ...
69
+
70
+ def _class_cached_property_with_parent[R](
71
+ function: Callable[..., R],
72
+ ) -> class_cached_property[R]: ...