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