mcrotk-cache-utils 0.1.0__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.
- mcrotk_cache_utils/__init__.py +36 -0
- mcrotk_cache_utils/copying/__init__.py +1 -0
- mcrotk_cache_utils/copying/copy_handling.py +231 -0
- mcrotk_cache_utils/copying/instance_copying.py +54 -0
- mcrotk_cache_utils/decorators/__init__.py +1 -0
- mcrotk_cache_utils/decorators/cache_controls.py +82 -0
- mcrotk_cache_utils/decorators/function_caching.py +94 -0
- mcrotk_cache_utils/decorators/method_caching.py +204 -0
- mcrotk_cache_utils/decorators/rng_caching.py +194 -0
- mcrotk_cache_utils/decorators/support/__init__.py +1 -0
- mcrotk_cache_utils/decorators/support/decorator_options.py +55 -0
- mcrotk_cache_utils/decorators/support/placement_checks.py +61 -0
- mcrotk_cache_utils/keys/__init__.py +1 -0
- mcrotk_cache_utils/keys/cache_keys.py +217 -0
- mcrotk_cache_utils/keys/converters/__init__.py +12 -0
- mcrotk_cache_utils/keys/converters/arrays.py +47 -0
- mcrotk_cache_utils/keys/converters/containers.py +45 -0
- mcrotk_cache_utils/keys/converters/pandas_objects.py +134 -0
- mcrotk_cache_utils/keys/passthrough.py +169 -0
- mcrotk_cache_utils/py.typed +0 -0
- mcrotk_cache_utils-0.1.0.dist-info/METADATA +190 -0
- mcrotk_cache_utils-0.1.0.dist-info/RECORD +24 -0
- mcrotk_cache_utils-0.1.0.dist-info/WHEEL +4 -0
- mcrotk_cache_utils-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Caching decorators for methods, with a separate cache per instance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable, Iterable, Mapping, MutableMapping
|
|
7
|
+
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
8
|
+
|
|
9
|
+
import cachetools
|
|
10
|
+
|
|
11
|
+
from ..copying.copy_handling import (
|
|
12
|
+
CACHE_ATTR,
|
|
13
|
+
ON_COPY,
|
|
14
|
+
ON_COPY_VALUES,
|
|
15
|
+
InstallsCopyMethods,
|
|
16
|
+
OnCopy,
|
|
17
|
+
check_copy_handling,
|
|
18
|
+
)
|
|
19
|
+
from ..keys.cache_keys import ArgRef, ArgSpec, Passthrough
|
|
20
|
+
from ..keys.passthrough import make_view
|
|
21
|
+
from .support.decorator_options import MethodOptions
|
|
22
|
+
from .support.placement_checks import MethodInClassBody, add_check, reject_static_or_class_method
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _DescriptorChecks(MethodInClassBody, InstallsCopyMethods):
|
|
26
|
+
"""What we add to cachetools' method descriptor: the placement error and copy handling."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from typing_extensions import Unpack
|
|
31
|
+
|
|
32
|
+
F = TypeVar("F", bound=Callable[..., Any]) # the decorated function: its type is kept as it is
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@overload
|
|
36
|
+
def cached_method(func: F, /, **options: Unpack[MethodOptions]) -> F: ...
|
|
37
|
+
@overload
|
|
38
|
+
def cached_method(func: None = None, /, **options: Unpack[MethodOptions]) -> Callable[[F], F]: ...
|
|
39
|
+
def cached_method(
|
|
40
|
+
func: Callable[..., Any] | None = None,
|
|
41
|
+
/,
|
|
42
|
+
*,
|
|
43
|
+
cache_factory: Callable[[], MutableMapping] = dict,
|
|
44
|
+
omit: ArgRef | Iterable[ArgRef] = (),
|
|
45
|
+
passthrough: Mapping[ArgRef, int | Passthrough] | None = None,
|
|
46
|
+
key_converters: Mapping[ArgRef, Callable[[Any], Any]] | None = None,
|
|
47
|
+
lock: Callable[[Any], Any] | None = None,
|
|
48
|
+
condition: Callable[[Any], Any] | None = None,
|
|
49
|
+
info: bool = False,
|
|
50
|
+
on_copy: OnCopy | None = None,
|
|
51
|
+
) -> Any:
|
|
52
|
+
"""Cache a method's results, with a separate cache for each instance.
|
|
53
|
+
|
|
54
|
+
Use as ``@cached_method`` or ``@cached_method(...)``. Built on
|
|
55
|
+
:func:`cachetools.cachedmethod`, which does the calling, binding, locking and statistics.
|
|
56
|
+
For ordinary methods only: for functions, static methods and class methods use
|
|
57
|
+
:func:`cached_function` (using this decorator on them is an error).
|
|
58
|
+
|
|
59
|
+
**The cache.** Created on an instance's first call and stored in its ``__dict__`` as
|
|
60
|
+
``_<Class>_<method>_cache`` (``<Class>`` is the class the method is defined in, so a
|
|
61
|
+
decorated override and the parent method it calls with ``super()`` don't share a cache).
|
|
62
|
+
It is not a dataclass field, so it doesn't affect ``==``, ``repr`` or frozen dataclasses,
|
|
63
|
+
and it disappears with the instance. The class needs a ``__dict__`` (no ``__slots__``-only
|
|
64
|
+
classes).
|
|
65
|
+
|
|
66
|
+
**The key.** Built from the call's arguments bound to the signature with defaults filled
|
|
67
|
+
in, so ``f(1)``, ``f(x=1)`` and ``f()`` with default ``x=1`` are the same call. ``self`` is
|
|
68
|
+
always left out. Instance attributes are not part of the key: if you change an attribute
|
|
69
|
+
that affects results, call :func:`clear_caches`.
|
|
70
|
+
|
|
71
|
+
**Types.** Type checkers see the method's own type, so calls are checked against its
|
|
72
|
+
signature and overriding works as usual (cached or not, in either class). The cache
|
|
73
|
+
attributes cachetools adds (``obj.method.cache_info()``, ...) work, but aren't part of that
|
|
74
|
+
type: :func:`cache_controls` gives typed access to them.
|
|
75
|
+
|
|
76
|
+
**Unhashable arguments** (also among ``*args`` and ``**kwargs``) are converted:
|
|
77
|
+
|
|
78
|
+
- numpy arrays (``ndarray`` and ``memmap``) by their contents: shape, dtype and a hash of
|
|
79
|
+
the data, computed on every call. So equal arrays share a result, and an array changed in
|
|
80
|
+
place gets a new key. Object arrays and other array types (masked arrays, ...) need
|
|
81
|
+
``key_converters``.
|
|
82
|
+
- lists, dicts and sets, and tuples and frozensets containing them, by
|
|
83
|
+
:func:`container_key` with ``type_tags=False``: lists become tuples, sets frozensets and
|
|
84
|
+
dicts frozensets of their ``(key, value)`` pairs, recursively. So ``[1, 2]`` and
|
|
85
|
+
``(1, 2)`` share a result. If the result depends on which was passed (e.g. it has the
|
|
86
|
+
same type), keep the types with ``key_converters={'x': container_key}``.
|
|
87
|
+
|
|
88
|
+
Anything else unhashable (including pandas objects) needs ``key_converters`` or ``omit``.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
cache_factory: Makes each instance's cache. Default ``dict`` (unbounded). Any
|
|
92
|
+
cachetools cache works, e.g. ``lambda: cachetools.LRUCache(maxsize=128)``.
|
|
93
|
+
omit: Arguments to leave out of the key, by name or by position in the signature
|
|
94
|
+
(positions count ``self``: in ``def f(self, a, b)``, ``a`` is 1; negative positions
|
|
95
|
+
count from the end). Only omit arguments that don't affect the result: calls that
|
|
96
|
+
differ only in an omitted argument get the same cached result.
|
|
97
|
+
passthrough: ``{argument: output_position}`` or ``{argument: (output_position, transform)}``.
|
|
98
|
+
For methods that take an argument and return it, possibly updated, as part of a
|
|
99
|
+
tuple (e.g. a random number generator). The argument is left out of the key. On a
|
|
100
|
+
miss the method's own outputs are returned. On a hit the stored result is returned
|
|
101
|
+
with the caller's current argument (after ``transform``, if given) at
|
|
102
|
+
``output_position``. The method must return a tuple; NamedTuples are rebuilt as the
|
|
103
|
+
same type.
|
|
104
|
+
key_converters: ``{argument: function}``: the function's result is used in the key in
|
|
105
|
+
place of the argument's value, e.g. to make an unhashable argument hashable, or to
|
|
106
|
+
key an argument by part of it (``{'model': lambda m: m.name}``). It replaces the
|
|
107
|
+
automatic handling of arrays and containers.
|
|
108
|
+
Ready-made converters: :func:`container_key` (lists, dicts, sets, nested),
|
|
109
|
+
:func:`array_key`, and for pandas objects (only when nothing simpler will do)
|
|
110
|
+
:func:`dataframe_key`, :func:`series_key` and :func:`index_key`.
|
|
111
|
+
lock, condition, info: Passed to :func:`cachetools.cachedmethod` (``lock`` and
|
|
112
|
+
``condition`` are functions of ``self`` returning the instance's lock/condition).
|
|
113
|
+
With ``info=True``, ``cache_controls(obj.method).cache_info()`` gives hit and
|
|
114
|
+
miss counts.
|
|
115
|
+
on_copy: What a copy of the instance (``copy.copy`` or ``copy.deepcopy``) gets in
|
|
116
|
+
this method's cache: ``'empty'`` (the default: never stale, even if the copy is
|
|
117
|
+
then changed), ``'copy'`` (the cached results: shared with the original by
|
|
118
|
+
``copy.copy``, deep-copied by ``deepcopy``; stale if the copy is changed, until
|
|
119
|
+
:func:`clear_caches`), or ``'manual'`` (the class's own ``__copy__`` handles it, e.g.
|
|
120
|
+
with :func:`copy_instance`). For a different choice for one copy, use
|
|
121
|
+
:func:`copy_instance`.
|
|
122
|
+
|
|
123
|
+
**Copying.** The decorator gives the class a ``__copy__`` and ``__deepcopy__`` that do
|
|
124
|
+
Python's usual copy but apply ``on_copy`` to each cached method's cache, and make the
|
|
125
|
+
copy's cached methods run on the copy. Without them, ``copy.copy`` would give the copy the
|
|
126
|
+
original's caches, and its cached methods would run on the original (a cachetools
|
|
127
|
+
behavior). If the class (or a base) defines its own ``__copy__`` or ``__deepcopy__``,
|
|
128
|
+
that one is kept: setting ``on_copy`` to ``'empty'`` or ``'copy'`` is then an error, and
|
|
129
|
+
the default gives a :class:`CacheCopyWarning` (once per class) unless the methods use
|
|
130
|
+
``on_copy='manual'``. ``'manual'`` requires a ``__copy__`` of the class's own. Copies
|
|
131
|
+
start with zeroed ``info`` counters. Pickling is unaffected: it carries the caches along.
|
|
132
|
+
``dataclasses.replace`` and ``copy.replace`` make new instances, with empty caches.
|
|
133
|
+
|
|
134
|
+
Errors in the options (unknown arguments, conflicts) are raised when the decorator is
|
|
135
|
+
applied. An unhashable key raises ``TypeError`` naming the argument.
|
|
136
|
+
|
|
137
|
+
Caveats:
|
|
138
|
+
- Results are returned as stored, not copied: changing a returned object in place
|
|
139
|
+
changes the cached result.
|
|
140
|
+
- Pickling an instance (e.g. sending it to worker processes) carries its caches along.
|
|
141
|
+
- Copying a cache for ``on_copy='copy'`` isn't thread-safe: don't copy an instance
|
|
142
|
+
while another thread is calling its cached methods.
|
|
143
|
+
- With ``passthrough``, cached entries record the type of the returned tuple. If that's
|
|
144
|
+
a NamedTuple class defined inside a function, an instance with cached results can't
|
|
145
|
+
be pickled (the same limitation as pickling any object referring to a local class).
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
if on_copy is not None and on_copy not in ON_COPY_VALUES:
|
|
149
|
+
raise ValueError(f"cached_method: on_copy must be one of {ON_COPY_VALUES}, got {on_copy!r}")
|
|
150
|
+
|
|
151
|
+
def decorate(func: Callable[..., Any]) -> Any:
|
|
152
|
+
"""Wrap ``func`` with cachetools' method descriptor, with its key spec and per-instance caches."""
|
|
153
|
+
reject_static_or_class_method(
|
|
154
|
+
func,
|
|
155
|
+
"cached_method",
|
|
156
|
+
"For a static or class method, use @cached_function under @staticmethod/@classmethod.",
|
|
157
|
+
)
|
|
158
|
+
spec = ArgSpec(func, has_self=True, omit=omit, passthrough=passthrough, key_converters=key_converters)
|
|
159
|
+
attr = _cache_attr_name(func)
|
|
160
|
+
get_cache = _cache_getter(spec, attr, cache_factory)
|
|
161
|
+
descriptor = cachetools.cachedmethod(get_cache, key=spec.make_key, lock=lock, condition=condition, info=info)(
|
|
162
|
+
func
|
|
163
|
+
)
|
|
164
|
+
_mark_descriptor(descriptor, attr, on_copy)
|
|
165
|
+
return descriptor
|
|
166
|
+
|
|
167
|
+
return decorate if func is None else decorate(func)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _cache_getter(
|
|
171
|
+
spec: ArgSpec, attr: str, cache_factory: Callable[[], MutableMapping]
|
|
172
|
+
) -> Callable[[Any], MutableMapping]:
|
|
173
|
+
"""The function cachetools calls to get an instance's cache: made on first use, stored in ``__dict__[attr]``."""
|
|
174
|
+
|
|
175
|
+
def get_cache(obj: Any) -> MutableMapping:
|
|
176
|
+
try:
|
|
177
|
+
instance_dict = obj.__dict__
|
|
178
|
+
except AttributeError:
|
|
179
|
+
raise TypeError(
|
|
180
|
+
f"{spec.qualname}: {type(obj).__name__} instances have no __dict__ to hold the cache"
|
|
181
|
+
) from None
|
|
182
|
+
cache = instance_dict.get(attr)
|
|
183
|
+
if cache is None:
|
|
184
|
+
check_copy_handling(type(obj))
|
|
185
|
+
cache = instance_dict.setdefault(attr, cache_factory()) # setdefault: another thread may have made one
|
|
186
|
+
elif not isinstance(cache, MutableMapping):
|
|
187
|
+
raise TypeError(f"{spec.qualname}: attribute {attr!r} is already used for something other than a cache")
|
|
188
|
+
return make_view(cache, spec.qualname) if spec.passthrough else cache
|
|
189
|
+
|
|
190
|
+
return get_cache
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _mark_descriptor(descriptor: Any, attr: str, on_copy: OnCopy | None) -> None:
|
|
194
|
+
"""Add our checks to cachetools' descriptor, and the markers ``clear_caches`` and copying look for."""
|
|
195
|
+
add_check(descriptor, _DescriptorChecks)
|
|
196
|
+
setattr(descriptor, CACHE_ATTR, attr)
|
|
197
|
+
setattr(descriptor, ON_COPY, ("empty", False) if on_copy is None else (on_copy, True))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _cache_attr_name(func: Callable[..., Any]) -> str:
|
|
201
|
+
"""``_<Class>_<method>_cache`` from the function's qualified name, e.g. ``Sim.draw`` -> ``_Sim_draw_cache``."""
|
|
202
|
+
qualname = getattr(func, "__qualname__", func.__name__)
|
|
203
|
+
qualname = qualname.rsplit("<locals>.", 1)[-1] # drop the enclosing function for local classes
|
|
204
|
+
return "_" + re.sub(r"\W", "_", qualname.replace(".", "_")) + "_cache"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Caching decorators for methods and functions that take a random number generator argument.
|
|
2
|
+
|
|
3
|
+
Two styles of such functions are covered:
|
|
4
|
+
|
|
5
|
+
- **omit** (``rng_omit_cached_method``, ``rng_omit_cached_function``): the function takes the
|
|
6
|
+
generator (or a seed) and draws from it, advancing the caller's generator in place, as in the
|
|
7
|
+
recommended practice (Scientific Python SPEC 7). The generator is just left out of the key.
|
|
8
|
+
- **passthrough** (``rng_passthrough_cached_method``, ``rng_passthrough_cached_function``): the
|
|
9
|
+
function also returns the generator with its result. On a hit, the caller's generator is
|
|
10
|
+
returned in place of the one stored with the result.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
16
|
+
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
17
|
+
|
|
18
|
+
from ..keys.cache_keys import ArgRef, Passthrough, _as_refs
|
|
19
|
+
from .function_caching import cached_function
|
|
20
|
+
from .method_caching import cached_method
|
|
21
|
+
from .support.decorator_options import (
|
|
22
|
+
RngOmitFunctionOptions,
|
|
23
|
+
RngOmitMethodOptions,
|
|
24
|
+
RngPassthroughFunctionOptions,
|
|
25
|
+
RngPassthroughMethodOptions,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from typing_extensions import Unpack
|
|
30
|
+
|
|
31
|
+
F = TypeVar("F", bound=Callable[..., Any]) # the decorated function: its type is kept as it is
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# --- omit: functions that advance the caller's generator in place ---
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def rng_omit_cached_method(func: F, /, **options: Unpack[RngOmitMethodOptions]) -> F: ...
|
|
39
|
+
@overload
|
|
40
|
+
def rng_omit_cached_method(func: None = None, /, **options: Unpack[RngOmitMethodOptions]) -> Callable[[F], F]: ...
|
|
41
|
+
def rng_omit_cached_method(
|
|
42
|
+
func: Callable[..., Any] | None = None,
|
|
43
|
+
/,
|
|
44
|
+
*,
|
|
45
|
+
rng_arg: ArgRef = "rng",
|
|
46
|
+
omit: ArgRef | Iterable[ArgRef] = (),
|
|
47
|
+
**kwargs: Any,
|
|
48
|
+
) -> Any:
|
|
49
|
+
"""Cache a method that takes a random number generator, leaving the generator out of the key.
|
|
50
|
+
|
|
51
|
+
Use as ``@rng_omit_cached_method`` or ``@rng_omit_cached_method(...)``. The same as
|
|
52
|
+
:func:`cached_method` with ``omit=rng_arg`` (other arguments can be added with ``omit=``);
|
|
53
|
+
all other options are as for :func:`cached_method`.
|
|
54
|
+
|
|
55
|
+
For methods that take the generator, or a seed, as an argument and draw from it::
|
|
56
|
+
|
|
57
|
+
@rng_omit_cached_method
|
|
58
|
+
def simulate(self, n, rng=None):
|
|
59
|
+
rng = np.random.default_rng(rng)
|
|
60
|
+
...
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
- Calls differing only in their generator share a cached result: the first call's random
|
|
64
|
+
draws are reused.
|
|
65
|
+
- On a miss, the method draws from the caller's generator, advancing it in place (when a
|
|
66
|
+
``Generator`` is passed). On a hit, nothing is drawn, so the generator is unchanged.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
rng_arg: The generator argument, by name or position (positions count ``self``).
|
|
70
|
+
Default ``'rng'``.
|
|
71
|
+
"""
|
|
72
|
+
return cached_method(func, omit=(rng_arg, *_as_refs(omit)), **kwargs)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@overload
|
|
76
|
+
def rng_omit_cached_function(func: F, /, **options: Unpack[RngOmitFunctionOptions]) -> F: ...
|
|
77
|
+
@overload
|
|
78
|
+
def rng_omit_cached_function(func: None = None, /, **options: Unpack[RngOmitFunctionOptions]) -> Callable[[F], F]: ...
|
|
79
|
+
def rng_omit_cached_function(
|
|
80
|
+
func: Callable[..., Any] | None = None,
|
|
81
|
+
/,
|
|
82
|
+
*,
|
|
83
|
+
rng_arg: ArgRef = "rng",
|
|
84
|
+
omit: ArgRef | Iterable[ArgRef] = (),
|
|
85
|
+
**kwargs: Any,
|
|
86
|
+
) -> Any:
|
|
87
|
+
"""Cache a function that takes a random number generator, leaving the generator out of the key.
|
|
88
|
+
|
|
89
|
+
The function version of :func:`rng_omit_cached_method`, built on :func:`cached_function`
|
|
90
|
+
(whose options it takes). Also for static and class methods, with ``@staticmethod`` or
|
|
91
|
+
``@classmethod`` above it.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
rng_arg: The generator argument, by name or position (as in the ``def`` line, from 0).
|
|
95
|
+
Default ``'rng'``.
|
|
96
|
+
"""
|
|
97
|
+
return cached_function(func, omit=(rng_arg, *_as_refs(omit)), **kwargs)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- passthrough: functions that also return the generator ---
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@overload
|
|
104
|
+
def rng_passthrough_cached_method(func: F, /, **options: Unpack[RngPassthroughMethodOptions]) -> F: ...
|
|
105
|
+
@overload
|
|
106
|
+
def rng_passthrough_cached_method(
|
|
107
|
+
func: None = None, /, **options: Unpack[RngPassthroughMethodOptions]
|
|
108
|
+
) -> Callable[[F], F]: ...
|
|
109
|
+
def rng_passthrough_cached_method(
|
|
110
|
+
func: Callable[..., Any] | None = None,
|
|
111
|
+
/,
|
|
112
|
+
*,
|
|
113
|
+
rng_arg: ArgRef = "rng",
|
|
114
|
+
rng_output: int = -1,
|
|
115
|
+
passthrough: Mapping[ArgRef, int | Passthrough] | None = None,
|
|
116
|
+
**kwargs: Any,
|
|
117
|
+
) -> Any:
|
|
118
|
+
"""Cache a method that takes a random number generator and returns it, leaving it out of the key.
|
|
119
|
+
|
|
120
|
+
Use as ``@rng_passthrough_cached_method`` or ``@rng_passthrough_cached_method(...)``. The
|
|
121
|
+
same as :func:`cached_method` with
|
|
122
|
+
``passthrough={rng_arg: (rng_output, numpy.random.default_rng)}`` (other passthrough
|
|
123
|
+
entries can be added with ``passthrough=``); all other options are as for
|
|
124
|
+
:func:`cached_method`. Needs numpy.
|
|
125
|
+
|
|
126
|
+
For methods that take the generator, or a seed, and also return the generator with their
|
|
127
|
+
result, so that a caller who passed a seed or ``None`` can continue the stream::
|
|
128
|
+
|
|
129
|
+
@rng_passthrough_cached_method
|
|
130
|
+
def simulate(self, n, rng=None):
|
|
131
|
+
rng = np.random.default_rng(rng)
|
|
132
|
+
...
|
|
133
|
+
return result, rng
|
|
134
|
+
|
|
135
|
+
- Calls differing only in their generator share a cached result: the first call's random
|
|
136
|
+
draws are reused.
|
|
137
|
+
- On a miss, the method's own outputs are returned, including the generator it advanced.
|
|
138
|
+
- On a hit, the stored result is returned with the caller's generator passed through
|
|
139
|
+
``numpy.random.default_rng``: a ``Generator`` comes back unchanged (nothing was
|
|
140
|
+
drawn), and a seed or ``None`` comes back as a new ``Generator`` made from it.
|
|
141
|
+
|
|
142
|
+
For methods that don't return the generator, use :func:`rng_omit_cached_method`.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
rng_arg: The generator argument, by name or position (positions count ``self``).
|
|
146
|
+
Default ``'rng'``.
|
|
147
|
+
rng_output: Position of the generator in the returned tuple. Default ``-1`` (last).
|
|
148
|
+
"""
|
|
149
|
+
combined = _rng_passthrough("rng_passthrough_cached_method", rng_arg, rng_output, passthrough)
|
|
150
|
+
return cached_method(func, passthrough=combined, **kwargs)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@overload
|
|
154
|
+
def rng_passthrough_cached_function(func: F, /, **options: Unpack[RngPassthroughFunctionOptions]) -> F: ...
|
|
155
|
+
@overload
|
|
156
|
+
def rng_passthrough_cached_function(
|
|
157
|
+
func: None = None, /, **options: Unpack[RngPassthroughFunctionOptions]
|
|
158
|
+
) -> Callable[[F], F]: ...
|
|
159
|
+
def rng_passthrough_cached_function(
|
|
160
|
+
func: Callable[..., Any] | None = None,
|
|
161
|
+
/,
|
|
162
|
+
*,
|
|
163
|
+
rng_arg: ArgRef = "rng",
|
|
164
|
+
rng_output: int = -1,
|
|
165
|
+
passthrough: Mapping[ArgRef, int | Passthrough] | None = None,
|
|
166
|
+
**kwargs: Any,
|
|
167
|
+
) -> Any:
|
|
168
|
+
"""Cache a function that takes a random number generator and returns it, leaving it out of the key.
|
|
169
|
+
|
|
170
|
+
The function version of :func:`rng_passthrough_cached_method`, built on
|
|
171
|
+
:func:`cached_function` (whose options it takes). Also for static and class methods, with
|
|
172
|
+
``@staticmethod`` or ``@classmethod`` above it. Needs numpy.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
rng_arg: The generator argument, by name or position (as in the ``def`` line, from 0).
|
|
176
|
+
Default ``'rng'``.
|
|
177
|
+
rng_output: Position of the generator in the returned tuple. Default ``-1`` (last).
|
|
178
|
+
"""
|
|
179
|
+
combined = _rng_passthrough("rng_passthrough_cached_function", rng_arg, rng_output, passthrough)
|
|
180
|
+
return cached_function(func, passthrough=combined, **kwargs)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _rng_passthrough(
|
|
184
|
+
decorator: str, rng_arg: ArgRef, rng_output: int, passthrough: Mapping[ArgRef, int | Passthrough] | None
|
|
185
|
+
) -> dict[ArgRef, int | Passthrough]:
|
|
186
|
+
"""The ``passthrough`` option for a passthrough RNG decorator: the RNG entry plus any others given."""
|
|
187
|
+
try:
|
|
188
|
+
from numpy.random import default_rng
|
|
189
|
+
except ImportError as e:
|
|
190
|
+
raise ImportError(f"{decorator} needs numpy: pip install 'mcrotk-cache-utils[numpy]'") from e
|
|
191
|
+
|
|
192
|
+
if passthrough is not None and rng_arg in passthrough:
|
|
193
|
+
raise ValueError(f"{decorator}: {rng_arg!r} is set by rng_arg/rng_output; don't also put it in passthrough")
|
|
194
|
+
return {rng_arg: (rng_output, default_rng), **(passthrough or {})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""What the decorators are built from: their options (for type checkers) and the placement checks."""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""The decorators' options, defined once for type checkers.
|
|
2
|
+
|
|
3
|
+
The decorators' typed overloads take ``**options: Unpack[...]`` of these, so each option's name
|
|
4
|
+
and type is written in one place. Only type checkers use them; the defaults are in the
|
|
5
|
+
decorators' own signatures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable, Iterable, Mapping, MutableMapping
|
|
11
|
+
from typing import Any, TypedDict
|
|
12
|
+
|
|
13
|
+
from ...copying.copy_handling import OnCopy
|
|
14
|
+
from ...keys.cache_keys import ArgRef, Passthrough
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _Options(TypedDict, total=False):
|
|
18
|
+
"""The options of both ``cached_method`` and ``cached_function``."""
|
|
19
|
+
|
|
20
|
+
cache_factory: Callable[[], MutableMapping]
|
|
21
|
+
omit: ArgRef | Iterable[ArgRef]
|
|
22
|
+
passthrough: Mapping[ArgRef, int | Passthrough] | None
|
|
23
|
+
key_converters: Mapping[ArgRef, Callable[[Any], Any]] | None
|
|
24
|
+
info: bool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MethodOptions(_Options, total=False):
|
|
28
|
+
"""``cached_method``'s options: ``lock`` and ``condition`` are functions of ``self``."""
|
|
29
|
+
|
|
30
|
+
lock: Callable[[Any], Any] | None
|
|
31
|
+
condition: Callable[[Any], Any] | None
|
|
32
|
+
on_copy: OnCopy | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FunctionOptions(_Options, total=False):
|
|
36
|
+
"""``cached_function``'s options: ``lock`` and ``condition`` are the objects themselves."""
|
|
37
|
+
|
|
38
|
+
lock: Any
|
|
39
|
+
condition: Any
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RngOmitMethodOptions(MethodOptions, total=False):
|
|
43
|
+
rng_arg: ArgRef
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RngOmitFunctionOptions(FunctionOptions, total=False):
|
|
47
|
+
rng_arg: ArgRef
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RngPassthroughMethodOptions(RngOmitMethodOptions, total=False):
|
|
51
|
+
rng_output: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RngPassthroughFunctionOptions(RngOmitFunctionOptions, total=False):
|
|
55
|
+
rng_output: int
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Clear errors when a decorator is used in the wrong place (e.g. ``cached_function`` on a method).
|
|
2
|
+
|
|
3
|
+
cachetools' wrappers and descriptors are ordinary instances, so we give each one a subclass
|
|
4
|
+
of its own class that adds the check, rather than putting another layer in front of it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_check(obj: Any, check: type) -> None:
|
|
12
|
+
"""Make ``obj`` an instance of a subclass of its class with the ``check`` mixin in front."""
|
|
13
|
+
obj.__class__ = _with_check(type(obj), check)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@functools.cache
|
|
17
|
+
def _with_check(cls: type, check: type) -> type:
|
|
18
|
+
"""A subclass of ``cls`` with the ``check`` mixin in front (cached: one class per pair)."""
|
|
19
|
+
# Same name and module as cachetools' class, so reprs and error messages don't change.
|
|
20
|
+
return type(cls.__name__, (check, cls), {"__module__": cls.__module__, "__qualname__": cls.__qualname__})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FunctionNotInClassBody:
|
|
24
|
+
"""Mixin for ``cached_function``'s wrapper: an error if it's placed directly in a class body.
|
|
25
|
+
|
|
26
|
+
Python calls ``__set_name__`` on objects in a class body when the class is created, but not
|
|
27
|
+
through ``staticmethod`` or ``classmethod``, so those (the right way to cache static and
|
|
28
|
+
class methods) are unaffected.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
32
|
+
raise TypeError(
|
|
33
|
+
f"{owner.__qualname__}.{name}: @cached_function can't cache a method: its one cache would be "
|
|
34
|
+
"shared by all instances, with self in the key. Use @cached_method instead (a cache per instance). "
|
|
35
|
+
"For a static or class method, put @staticmethod/@classmethod above @cached_function."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MethodInClassBody:
|
|
40
|
+
"""Mixin for ``cached_method``'s descriptor: an error if it's called directly.
|
|
41
|
+
|
|
42
|
+
cachetools' method descriptor is only called directly when it never became a method:
|
|
43
|
+
under ``@staticmethod`` or ``@classmethod`` (which don't call ``__set_name__``), or on a
|
|
44
|
+
function outside a class. cachetools' own error there only mentions class methods.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __call__(self, /, *args: Any, **kwargs: Any) -> Any:
|
|
48
|
+
qualname = getattr(self, "__qualname__", "cached method")
|
|
49
|
+
raise TypeError(
|
|
50
|
+
f"{qualname}: @cached_method only works on ordinary methods defined in a class body "
|
|
51
|
+
"(it keeps a cache per instance). For a function, a static method or a class method, "
|
|
52
|
+
"use @cached_function, with @staticmethod/@classmethod above it."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def reject_static_or_class_method(func: Any, decorator: str, advice: str) -> None:
|
|
57
|
+
"""Error for ``@decorator`` placed above ``@staticmethod``/``@classmethod`` (it receives their object)."""
|
|
58
|
+
if isinstance(func, (staticmethod, classmethod)):
|
|
59
|
+
kind = type(func).__name__
|
|
60
|
+
qualname = getattr(func, "__qualname__", repr(func))
|
|
61
|
+
raise TypeError(f"{qualname}: @{decorator} is above @{kind}. {advice}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""How a call's arguments become a cache entry: key building, passthrough, and the key converters."""
|