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,36 @@
1
+ """Caching decorators for methods and functions, built on cachetools.
2
+
3
+ Per-instance caches for methods, argument omission, passthrough (e.g. for RNGs), key converters, and
4
+ key helpers for arrays, containers and pandas objects.
5
+ """
6
+
7
+ from .copying.instance_copying import CacheCopyWarning, copy_instance
8
+ from .decorators.cache_controls import CacheControls, cache_controls, clear_caches
9
+ from .decorators.function_caching import cached_function
10
+ from .decorators.method_caching import cached_method
11
+ from .decorators.rng_caching import (
12
+ rng_omit_cached_function,
13
+ rng_omit_cached_method,
14
+ rng_passthrough_cached_function,
15
+ rng_passthrough_cached_method,
16
+ )
17
+ from .keys.converters import array_key, container_key, dataframe_key, index_key, series_key
18
+
19
+ __all__ = [
20
+ "CacheControls",
21
+ "CacheCopyWarning",
22
+ "array_key",
23
+ "cache_controls",
24
+ "cached_function",
25
+ "cached_method",
26
+ "clear_caches",
27
+ "container_key",
28
+ "copy_instance",
29
+ "dataframe_key",
30
+ "index_key",
31
+ "rng_omit_cached_function",
32
+ "rng_omit_cached_method",
33
+ "rng_passthrough_cached_function",
34
+ "rng_passthrough_cached_method",
35
+ "series_key",
36
+ ]
@@ -0,0 +1 @@
1
+ """Copying instances with cached methods: ``copy_instance``, and the copy handling ``cached_method`` installs."""
@@ -0,0 +1,231 @@
1
+ """Copy handling for instances with cached methods: the ``__copy__``/``__deepcopy__`` ``cached_method`` installs.
2
+
3
+ Python's default copy would duplicate the instance ``__dict__``, which holds our caches and
4
+ cachetools' bound wrappers (bound to the original instance). The installed methods do the
5
+ default copy (honoring ``__getstate__``, ``__reduce__``, ``__slots__``), but first drop the
6
+ wrappers (recreated for the copy on first use) and empty or copy each cache according to its
7
+ method's ``on_copy``.
8
+ """
9
+
10
+ import copy
11
+ import os
12
+ import sys
13
+ import warnings
14
+ import weakref
15
+ from collections.abc import Iterator, MutableMapping
16
+ from typing import Any, Literal, NamedTuple
17
+
18
+ import cachetools
19
+
20
+ OnCopy = Literal["empty", "copy", "manual"]
21
+ ON_COPY_VALUES = ("empty", "copy", "manual")
22
+
23
+ CACHE_ATTR = "_mcrotk_cache_attr" # on our descriptors: the name of the instance attribute holding the cache
24
+ ON_COPY = "_mcrotk_on_copy" # on our descriptors: (on_copy value, whether it was given explicitly)
25
+ _OURS = "_mcrotk_copy" # on the __copy__/__deepcopy__ functions we install
26
+
27
+ _WRAPPERS = "__cachetools_cachedmethods" # cachetools' per-instance wrappers for super() lookups
28
+
29
+
30
+ class CacheCopyWarning(UserWarning):
31
+ """A class with cached methods defines its own ``__copy__`` or ``__deepcopy__``.
32
+
33
+ Then ``cached_method`` can't install its copy handling, and whether copies are correct
34
+ depends on that method (:func:`copy_instance` can do the copying inside it). Silence with
35
+ ``on_copy='manual'`` on the cached methods, or with
36
+ ``warnings.filterwarnings('ignore', category=CacheCopyWarning)``.
37
+ """
38
+
39
+
40
+ class CachedMethodInfo(NamedTuple):
41
+ """A cached method found in a class, with what copying needs to know about it."""
42
+
43
+ name: str # the method's name in its class (also the key of its cachetools wrapper in instances)
44
+ cache_attr: str
45
+ on_copy: str
46
+ explicit: bool
47
+
48
+
49
+ def cached_methods(cls: type) -> Iterator[tuple[type, CachedMethodInfo]]:
50
+ """Our cached methods in ``cls`` and its bases, with the class each is defined in."""
51
+ for klass in cls.__mro__:
52
+ for name, value in vars(klass).items():
53
+ attr = getattr(value, CACHE_ATTR, None)
54
+ if isinstance(attr, str):
55
+ on_copy, explicit = getattr(value, ON_COPY, ("empty", False))
56
+ yield klass, CachedMethodInfo(name, attr, on_copy, explicit)
57
+
58
+
59
+ # --- Installing at class creation ---
60
+
61
+
62
+ class InstallsCopyMethods:
63
+ """Mixin for ``cached_method``'s descriptor: copy handling for the class, set up when the class is created."""
64
+
65
+ def __set_name__(self, owner: type, name: str) -> None:
66
+ super().__set_name__(owner, name) # pyright: ignore[reportAttributeAccessIssue] (cachetools' descriptor)
67
+ on_copy, explicit = getattr(self, ON_COPY)
68
+ theirs = user_copy_methods(owner)
69
+ _check_on_copy(owner, name, on_copy, explicit, theirs)
70
+ if on_copy != "manual":
71
+ _install_copy_methods(owner, theirs)
72
+
73
+
74
+ def _check_on_copy(owner: type, name: str, on_copy: str, explicit: bool, theirs: list[str]) -> None:
75
+ """Errors for an ``on_copy`` that conflicts with the class's own copy methods, or the warning for the default."""
76
+ where = f"{owner.__qualname__}.{name}"
77
+ if on_copy == "manual":
78
+ if "__copy__" not in theirs:
79
+ raise TypeError(
80
+ f"{where}: on_copy='manual', but {owner.__qualname__} has no __copy__ of its own, so copy.copy "
81
+ "would give the copy the original's cache and run its cached methods on the original. Define "
82
+ "__copy__, or use on_copy='empty' or 'copy'."
83
+ )
84
+ elif theirs and explicit:
85
+ raise TypeError(
86
+ f"{where}: on_copy={on_copy!r} can't take effect: {owner.__qualname__} defines its own "
87
+ f"{' and '.join(theirs)}. Use on_copy='manual' and handle the cache there."
88
+ )
89
+ elif theirs:
90
+ _warn_once(owner, theirs)
91
+
92
+
93
+ def _install_copy_methods(owner: type, theirs: list[str]) -> None:
94
+ """Give ``owner`` our ``__copy__`` and ``__deepcopy__``, except where it has its own."""
95
+ for dunder, function in (("__copy__", _copy), ("__deepcopy__", _deepcopy)):
96
+ if dunder not in theirs and getattr(owner, dunder, None) is not function:
97
+ setattr(owner, dunder, function)
98
+
99
+
100
+ def user_copy_methods(cls: type) -> list[str]:
101
+ """Which of ``__copy__``/``__deepcopy__`` ``cls`` gets from its own code (itself or a base), not from us."""
102
+ return [
103
+ dunder
104
+ for dunder in ("__copy__", "__deepcopy__")
105
+ if (method := getattr(cls, dunder, None)) is not None and not getattr(method, _OURS, False)
106
+ ]
107
+
108
+
109
+ _checked: "weakref.WeakSet[type]" = weakref.WeakSet()
110
+ _warned: "weakref.WeakSet[type]" = weakref.WeakSet()
111
+
112
+
113
+ def check_copy_handling(cls: type) -> None:
114
+ """At an instance's first cache: warn (once per class) if ``cls`` replaced our copy methods.
115
+
116
+ Catches subclasses that define ``__copy__``/``__deepcopy__``, which class creation can't see.
117
+ """
118
+ if cls in _checked:
119
+ return
120
+ _checked.add(cls)
121
+ theirs = user_copy_methods(cls)
122
+ if theirs and any(info.on_copy != "manual" for _, info in cached_methods(cls)):
123
+ _warn_once(cls, theirs)
124
+
125
+
126
+ def _warn_once(cls: type, theirs: list[str]) -> None:
127
+ """Give a :class:`CacheCopyWarning` for ``cls``, which defines its own ``theirs`` methods, once per class."""
128
+ if cls in _warned:
129
+ return
130
+ _warned.add(cls)
131
+ warnings.warn(
132
+ f"{cls.__qualname__} defines its own {' and '.join(theirs)}, so copies of its instances don't get "
133
+ "mcrotk_cache_utils' handling of its cached methods' caches. Make sure "
134
+ f"{'it doesn' if len(theirs) == 1 else 'they don'}'t give the copy the original's caches or the "
135
+ "cachetools wrappers in the instance __dict__ (bound to the original): copy_instance(self) makes "
136
+ "such a copy correctly. Then set on_copy='manual' on the cached methods to silence this warning.",
137
+ CacheCopyWarning,
138
+ stacklevel=_caller_outside_libraries(),
139
+ )
140
+
141
+
142
+ _PACKAGE_DIR = os.path.dirname(os.path.dirname(__file__)) # mcrotk_cache_utils/, above copying/
143
+ _SKIP_DIRS = (_PACKAGE_DIR + os.sep, os.path.dirname(cachetools.__file__) + os.sep)
144
+ _SKIP_FILES = (getattr(sys.modules.get("abc"), "__file__", None) or "",) # ABCMeta.__new__ runs __set_name__
145
+
146
+
147
+ def _caller_outside_libraries() -> int:
148
+ """The stacklevel of the nearest frame outside this package (any of its folders), cachetools and abc."""
149
+ level, frame = 2, sys._getframe(2) # 1: _warn_once; 2: its caller
150
+ while frame is not None and (
151
+ frame.f_code.co_filename.startswith(_SKIP_DIRS) or frame.f_code.co_filename in _SKIP_FILES
152
+ ):
153
+ level, frame = level + 1, frame.f_back
154
+ return level
155
+
156
+
157
+ # --- The installed methods ---
158
+
159
+
160
+ def _copy(self: Any) -> Any:
161
+ return copy_with_caches(self, None)
162
+
163
+
164
+ def _deepcopy(self: Any, memo: dict[int, Any]) -> Any:
165
+ return copy_with_caches(self, memo)
166
+
167
+
168
+ _copy.__name__ = _copy.__qualname__ = "__copy__"
169
+ _deepcopy.__name__ = _deepcopy.__qualname__ = "__deepcopy__"
170
+ _copy.__doc__ = "Shallow copy; cached methods' caches are emptied or copied according to their on_copy."
171
+ _deepcopy.__doc__ = "Deep copy; cached methods' caches are emptied or deep-copied according to their on_copy."
172
+ setattr(_copy, _OURS, True)
173
+ setattr(_deepcopy, _OURS, True)
174
+
175
+
176
+ def copy_with_caches(obj: Any, memo: dict[int, Any] | None, methods: list[CachedMethodInfo] | None = None) -> Any:
177
+ """Python's default copy (deep if ``memo`` is given), with the cache handling applied first."""
178
+ deep = memo is not None
179
+ if methods is None:
180
+ methods = [info for _, info in cached_methods(type(obj))]
181
+ rv = obj.__reduce_ex__(4)
182
+ if isinstance(rv, str): # a global (copied by reference), as in copy.copy
183
+ return obj
184
+ func, args, state, *rest = rv
185
+ if isinstance(state, dict):
186
+ state, prepared = _prepare(state, methods, deep), True
187
+ elif isinstance(state, tuple) and len(state) == 2 and isinstance(state[0], dict): # (__dict__, __slots__ values)
188
+ state, prepared = (_prepare(state[0], methods, deep), state[1]), True
189
+ else:
190
+ prepared = False # a custom __getstate__/__reduce__ state: fix the copy's __dict__ afterwards
191
+ new = copy._reconstruct(obj, memo, func, args, state, *rest) # pyright: ignore[reportAttributeAccessIssue]
192
+ if not prepared and isinstance(getattr(new, "__dict__", None), dict):
193
+ fixed = _prepare(new.__dict__, methods, deep)
194
+ new.__dict__.clear()
195
+ new.__dict__.update(fixed)
196
+ return new
197
+
198
+
199
+ def _prepare(instance_dict: dict[str, Any], methods: list[CachedMethodInfo], deep: bool) -> dict[str, Any]:
200
+ """A copy of the instance's ``__dict__`` for the new instance: no wrappers; caches emptied or copied."""
201
+ result = dict(instance_dict)
202
+ result.pop(_WRAPPERS, None)
203
+ for info in methods:
204
+ if type(result.get(info.name)).__module__.startswith("cachetools"):
205
+ del result[info.name] # cachetools' wrapper, bound to the original
206
+ if info.cache_attr not in result or info.on_copy == "manual":
207
+ continue
208
+ if info.on_copy == "empty":
209
+ del result[info.cache_attr] # created again on first use
210
+ elif not deep: # "copy", shallow (a deep copy copies the cache with the rest of the state)
211
+ result[info.cache_attr] = copy_cache(result[info.cache_attr])
212
+ return result
213
+
214
+
215
+ def copy_cache(cache: MutableMapping) -> MutableMapping:
216
+ """A new cache with the same entries: the cached keys and results are shared, not copied.
217
+
218
+ cachetools' caches don't support ``copy.copy`` (the copy would share their internal
219
+ structures), so the cache is deep-copied with its keys and results kept as they are. This
220
+ keeps e.g. an LRUCache's order and a TTLCache's expiry times.
221
+ """
222
+ if type(cache) is dict:
223
+ return cache.copy()
224
+ keys = list(cache)
225
+ if isinstance(cache, cachetools.Cache):
226
+ # Read the values without "using" them: LRUCache.__getitem__ would change the original's order.
227
+ values = [cachetools.Cache.__getitem__(cache, key) for key in keys]
228
+ else:
229
+ values = [cache[key] for key in keys]
230
+ memo: dict[int, Any] = {id(obj): obj for obj in (*keys, *values)}
231
+ return copy.deepcopy(cache, memo)
@@ -0,0 +1,54 @@
1
+ """Copying an instance with cached methods, choosing what the copy gets in their caches.
2
+
3
+ The machinery (the ``__copy__``/``__deepcopy__`` that ``cached_method`` installs, and their checks)
4
+ is in ``copy_handling``; ``CacheCopyWarning`` is defined there, where it's given.
5
+ """
6
+
7
+ from typing import Any, Literal, TypeVar
8
+
9
+ from .copy_handling import CacheCopyWarning, cached_methods, copy_with_caches
10
+
11
+ __all__ = ["CacheCopyWarning", "copy_instance"]
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ def copy_instance(
17
+ obj: T,
18
+ caches: Literal["empty", "copy"] | None = None,
19
+ *,
20
+ deep: bool = False,
21
+ memo: dict[int, Any] | None = None,
22
+ ) -> T:
23
+ """Copy an instance with cached methods, choosing what the copy gets in their caches.
24
+
25
+ Makes the same copy as ``copy.copy(obj)`` (or ``copy.deepcopy(obj)`` with ``deep=True``)
26
+ does with the copy methods ``cached_method`` installs, but never calls the class's own
27
+ ``__copy__``/``__deepcopy__``, so it can be used inside them::
28
+
29
+ @cached_method(on_copy="manual")
30
+ def simulate(self, n): ...
31
+
32
+ def __copy__(self):
33
+ new = copy_instance(self)
34
+ ... # anything else your copy needs
35
+ return new
36
+
37
+ Args:
38
+ obj: The instance to copy.
39
+ caches: ``'empty'`` or ``'copy'`` for every cached method's cache in the copy (see
40
+ ``on_copy`` in :func:`cached_method`), or ``None`` (default) to follow each method's
41
+ ``on_copy``, with ``'manual'`` methods' caches emptied.
42
+ deep: Make a deep copy.
43
+ memo: Inside your own ``__deepcopy__(self, memo)``, pass its ``memo`` (implies ``deep``),
44
+ so references shared with other parts of the object being copied stay shared.
45
+ """
46
+ if caches not in (None, "empty", "copy"):
47
+ raise ValueError(f"copy_instance: caches must be 'empty', 'copy' or None, got {caches!r}")
48
+ if memo is None and deep:
49
+ memo = {}
50
+ methods = [
51
+ info._replace(on_copy=caches or ("empty" if info.on_copy == "manual" else info.on_copy))
52
+ for _, info in cached_methods(type(obj))
53
+ ]
54
+ return copy_with_caches(obj, memo, methods)
@@ -0,0 +1 @@
1
+ """The decorators, and ``cache_controls``/``clear_caches`` for what they return."""
@@ -0,0 +1,82 @@
1
+ """Working with the caches of cached functions and methods: ``cache_controls`` and ``clear_caches``.
2
+
3
+ ``cache_controls`` gives typed access to a cached function's or method's cache attributes
4
+ (``cache_clear``, ``cache_info``, ...).
5
+
6
+ The decorators keep the decorated function's own type, so type checkers see a plain function and
7
+ don't know about the attributes cachetools adds. ``cache_controls`` gives them a type.
8
+ """
9
+
10
+ from collections.abc import Callable
11
+ from typing import Any, Protocol
12
+
13
+ from cachetools import CacheInfo
14
+
15
+ from ..copying.copy_handling import CACHE_ATTR
16
+
17
+
18
+ class CacheControls(Protocol):
19
+ """The cache attributes of a cached function, or of a cached method on an instance."""
20
+
21
+ @property
22
+ def cache(self) -> Any:
23
+ """The cache itself (what ``cache_factory`` made)."""
24
+ ...
25
+
26
+ @property
27
+ def cache_key(self) -> Callable[..., Any]: ...
28
+ @property
29
+ def cache_lock(self) -> Any: ...
30
+ @property
31
+ def cache_condition(self) -> Any: ...
32
+ def cache_clear(self) -> None:
33
+ """Empty the cache (and reset the ``info`` counters)."""
34
+ ...
35
+
36
+ def cache_info(self) -> CacheInfo:
37
+ """Hit and miss counts. Only with ``info=True``: otherwise an ``AttributeError``."""
38
+ ...
39
+
40
+
41
+ def cache_controls(cached: Callable[..., Any]) -> CacheControls:
42
+ """The cache attributes of ``cached``, typed for type checkers.
43
+
44
+ ``cached`` is a function decorated with ``cached_function`` (including a static or class
45
+ method, accessed as ``Class.method``), or a method decorated with ``cached_method`` accessed
46
+ on an instance (``obj.method``)::
47
+
48
+ cache_controls(fit).cache_clear()
49
+ cache_controls(model.fit).cache_info()
50
+
51
+ The attributes are the same as ``fit.cache_clear()`` etc., which also work at runtime but
52
+ which type checkers report as unknown. To empty an instance's caches, :func:`clear_caches`
53
+ also works.
54
+ """
55
+ if not callable(getattr(cached, "cache_clear", None)):
56
+ hint = (
57
+ " (pass a cached method accessed on an instance, obj.method, not on the class)"
58
+ if type(cached).__module__.startswith("cachetools") # cachedmethod's class-level object
59
+ else ""
60
+ )
61
+ raise TypeError(f"cache_controls: {cached!r} is not a cached function or method{hint}")
62
+ return cached # type: ignore[return-value] # cachetools' wrapper (or a method bound to one) has these attributes
63
+
64
+
65
+ def clear_caches(obj: Any, *methods: str) -> None:
66
+ """Empty ``obj``'s caches for the named cached methods, or for all of them if none are named.
67
+
68
+ Only caches made by this library's decorators are touched. Use after changing an
69
+ attribute that affects cached results, e.g. in a copy made with ``on_copy='copy'``.
70
+ """
71
+ found = set()
72
+ for cls in type(obj).__mro__:
73
+ for name, value in vars(cls).items():
74
+ attr = getattr(value, CACHE_ATTR, None)
75
+ if not isinstance(attr, str) or (methods and name not in methods):
76
+ continue
77
+ found.add(name)
78
+ if attr in getattr(obj, "__dict__", {}):
79
+ value.__get__(obj, cls).cache_clear() # through cachetools, so locks and info counters are handled
80
+ unknown = set(methods) - found
81
+ if unknown:
82
+ raise ValueError(f"{type(obj).__name__} has no cached method(s) named {sorted(unknown)}")
@@ -0,0 +1,94 @@
1
+ """Caching decorators for functions, including static and class methods, with one cache per function."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterable, Mapping, MutableMapping
6
+ from typing import TYPE_CHECKING, Any, TypeVar, overload
7
+
8
+ import cachetools
9
+
10
+ from ..keys.cache_keys import ArgRef, ArgSpec, Passthrough
11
+ from ..keys.passthrough import make_view
12
+ from .support.decorator_options import FunctionOptions
13
+ from .support.placement_checks import FunctionNotInClassBody, add_check, reject_static_or_class_method
14
+
15
+ if TYPE_CHECKING:
16
+ from typing_extensions import Unpack
17
+
18
+ F = TypeVar("F", bound=Callable[..., Any]) # the decorated function: its type is kept as it is
19
+
20
+
21
+ @overload
22
+ def cached_function(func: F, /, **options: Unpack[FunctionOptions]) -> F: ...
23
+ @overload
24
+ def cached_function(func: None = None, /, **options: Unpack[FunctionOptions]) -> Callable[[F], F]: ...
25
+ def cached_function(
26
+ func: Callable[..., Any] | None = None,
27
+ /,
28
+ *,
29
+ cache_factory: Callable[[], MutableMapping] = dict,
30
+ omit: ArgRef | Iterable[ArgRef] = (),
31
+ passthrough: Mapping[ArgRef, int | Passthrough] | None = None,
32
+ key_converters: Mapping[ArgRef, Callable[[Any], Any]] | None = None,
33
+ lock: Any = None,
34
+ condition: Any = None,
35
+ info: bool = False,
36
+ ) -> Any:
37
+ """Cache a function's results.
38
+
39
+ Use as ``@cached_function`` or ``@cached_function(...)``. Built on
40
+ :func:`cachetools.cached`, which does the calling, locking and statistics. The options are
41
+ the same as for :func:`cached_method`, except that ``lock`` and ``condition`` are objects
42
+ rather than functions of ``self``.
43
+
44
+ **The cache.** One per function, made by ``cache_factory`` when the decorator is applied,
45
+ and available as ``cache_controls(f).cache``. It lives as long as the function does,
46
+ usually the whole program, so with the default (an unbounded ``dict``) it keeps every
47
+ result until ``cache_controls(f).cache_clear()`` is called. Pass e.g.
48
+ ``cache_factory=lambda: cachetools.LRUCache(maxsize=128)`` to bound it.
49
+
50
+ **The key.** Built from the call's arguments bound to the signature with defaults filled
51
+ in, so ``f(1)``, ``f(x=1)`` and ``f()`` with default ``x=1`` are the same call. Unhashable
52
+ arrays and containers are converted as for :func:`cached_method`. Positions in the options
53
+ count the parameters as in the ``def`` line, from 0.
54
+
55
+ **Types.** Type checkers see the decorated function's own type (its signature and return
56
+ type). The cache attributes cachetools adds (``f.cache_clear()``, ...) work, but aren't part
57
+ of that type: :func:`cache_controls` gives typed access to them.
58
+
59
+ **Static and class methods.** Put ``@staticmethod`` or ``@classmethod`` above this decorator.
60
+ For a class method, ``cls`` is part of the key, so subclasses get their own entries (in one
61
+ shared cache); ``omit=['cls']`` makes them share results. For ordinary methods use
62
+ :func:`cached_method`: using this decorator directly in a class body is an error.
63
+
64
+ Args:
65
+ cache_factory: Makes the cache. Default ``dict`` (unbounded).
66
+ omit, passthrough, key_converters: As for :func:`cached_method`.
67
+ lock, condition, info: Passed to :func:`cachetools.cached`. A ``dict`` cache is safe
68
+ to use from several threads without a lock (at worst two threads compute the same
69
+ result); cachetools' caches (``LRUCache`` etc.) are not, so pass e.g.
70
+ ``lock=threading.Lock()`` with them. ``condition`` also stops two threads from
71
+ computing the same result. With ``info=True``, ``cache_controls(f).cache_info()``
72
+ gives hit and miss counts.
73
+
74
+ Caveats:
75
+ - Results are returned as stored, not copied: changing a returned object in place
76
+ changes the cached result.
77
+ - Each process has its own cache: worker processes (e.g. in a ``ProcessPoolExecutor``)
78
+ start with empty caches, and their results don't reach the parent's cache.
79
+ """
80
+
81
+ def decorate(func: Callable[..., Any]) -> Any:
82
+ """Wrap ``func`` with cachetools, with its key spec and its one cache."""
83
+ reject_static_or_class_method(
84
+ func, "cached_function", "Put @staticmethod/@classmethod above @cached_function instead."
85
+ )
86
+ spec = ArgSpec(func, has_self=False, omit=omit, passthrough=passthrough, key_converters=key_converters)
87
+ cache = cache_factory()
88
+ if spec.passthrough:
89
+ cache = make_view(cache, spec.qualname)
90
+ wrapper = cachetools.cached(cache, key=spec.make_key, lock=lock, condition=condition, info=info)(func)
91
+ add_check(wrapper, FunctionNotInClassBody)
92
+ return wrapper
93
+
94
+ return decorate if func is None else decorate(func)