cachetools 7.2.1__tar.gz → 7.2.3__tar.gz
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.
- {cachetools-7.2.1 → cachetools-7.2.3}/CHANGELOG.rst +23 -0
- {cachetools-7.2.1/src/cachetools.egg-info → cachetools-7.2.3}/PKG-INFO +1 -1
- {cachetools-7.2.1 → cachetools-7.2.3}/pyproject.toml +1 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools/__init__.py +3 -2
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools/__init__.pyi +56 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools/_cached.py +13 -2
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools/_cachedmethod.py +7 -98
- cachetools-7.2.3/src/cachetools/_descriptor.py +126 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools/func.pyi +28 -2
- {cachetools-7.2.1 → cachetools-7.2.3/src/cachetools.egg-info}/PKG-INFO +1 -1
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools.egg-info/SOURCES.txt +1 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/__init__.py +2 -2
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_cached.py +60 -2
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_cachedmethod.py +112 -4
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_func.py +69 -2
- cachetools-7.2.3/tests/test_threading.py +103 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_ttl.py +16 -0
- cachetools-7.2.1/tests/test_threading.py +0 -122
- {cachetools-7.2.1 → cachetools-7.2.3}/LICENSE +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/MANIFEST.in +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/README.rst +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/docs/conf.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/docs/index.rst +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/setup.cfg +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools/func.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools/keys.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools/keys.pyi +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools/py.typed +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools.egg-info/dependency_links.txt +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/src/cachetools.egg-info/top_level.txt +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_cache.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_classmethod.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_fifo.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_keys.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_lfu.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_lru.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_rr.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tests/test_tlru.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.3}/tox.ini +0 -0
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
v7.2.3 (2026-10-17)
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
- Fix ``@cached`` decorator stacking behavior.
|
|
5
|
+
|
|
6
|
+
- Minor type stub improvements.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
v7.2.2 (2026-10-15)
|
|
10
|
+
===================
|
|
11
|
+
|
|
12
|
+
- Fix ``@cachedmethod`` for subclasses that override a decorated
|
|
13
|
+
method and call the base class implementation via ``super()``.
|
|
14
|
+
|
|
15
|
+
- Restore class access to instance methods decorated with
|
|
16
|
+
``@cachedmethod``.
|
|
17
|
+
|
|
18
|
+
- Improve ``TTLCache`` robustness when calculating expiration time
|
|
19
|
+
fails.
|
|
20
|
+
|
|
21
|
+
- Various unit test improvements.
|
|
22
|
+
|
|
23
|
+
|
|
1
24
|
v7.2.1 (2026-10-05)
|
|
2
25
|
===================
|
|
3
26
|
|
|
@@ -12,7 +12,7 @@ __all__ = (
|
|
|
12
12
|
"cachedmethod",
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
__version__ = "7.2.
|
|
15
|
+
__version__ = "7.2.3"
|
|
16
16
|
|
|
17
17
|
import collections
|
|
18
18
|
import collections.abc
|
|
@@ -495,6 +495,7 @@ class TTLCache(_TimedCache):
|
|
|
495
495
|
|
|
496
496
|
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
|
|
497
497
|
with self.timer as time:
|
|
498
|
+
expires = time + self.__ttl
|
|
498
499
|
self.expire(time)
|
|
499
500
|
cache_setitem(self, key, value)
|
|
500
501
|
try:
|
|
@@ -503,7 +504,7 @@ class TTLCache(_TimedCache):
|
|
|
503
504
|
self.__links[key] = link = TTLCache._Link(key)
|
|
504
505
|
else:
|
|
505
506
|
link.unlink()
|
|
506
|
-
link.expires =
|
|
507
|
+
link.expires = expires
|
|
507
508
|
link.next = root = self.__root
|
|
508
509
|
link.prev = prev = root.prev
|
|
509
510
|
prev.next = root.prev = link
|
|
@@ -29,7 +29,9 @@ __all__: Final = (
|
|
|
29
29
|
__version__: str
|
|
30
30
|
|
|
31
31
|
_P = ParamSpec("_P")
|
|
32
|
+
_Q = ParamSpec("_Q")
|
|
32
33
|
_R = TypeVar("_R")
|
|
34
|
+
_S = TypeVar("_S")
|
|
33
35
|
_T = TypeVar("_T")
|
|
34
36
|
_KT = TypeVar("_KT")
|
|
35
37
|
_VT = TypeVar("_VT")
|
|
@@ -153,16 +155,47 @@ class _AbstractCondition(AbstractContextManager[Any], Protocol):
|
|
|
153
155
|
class _cached_wrapper(Generic[_P, _R]):
|
|
154
156
|
__wrapped__: Callable[_P, _R]
|
|
155
157
|
__name__: str
|
|
158
|
+
__qualname__: str
|
|
156
159
|
__doc__: str | None
|
|
157
160
|
cache: MutableMapping[Any, Any] | None
|
|
158
161
|
cache_key: Callable[..., Any] = ...
|
|
159
162
|
cache_lock: AbstractContextManager[Any] | None = None
|
|
160
163
|
cache_condition: _AbstractCondition | None = None
|
|
164
|
+
@overload # class access, e.g. Cls.method
|
|
165
|
+
def __get__(
|
|
166
|
+
self, obj: None, objtype: type | None = None
|
|
167
|
+
) -> _cached_wrapper[_P, _R]: ...
|
|
168
|
+
@overload # instance access binds the first parameter
|
|
169
|
+
def __get__(
|
|
170
|
+
self: _cached_wrapper[Concatenate[_S, _Q], _R],
|
|
171
|
+
obj: _S,
|
|
172
|
+
objtype: type | None = None,
|
|
173
|
+
) -> _cached_wrapper[_Q, _R]: ...
|
|
174
|
+
@overload # fallback, e.g. when wrapped by @staticmethod or @classmethod
|
|
175
|
+
def __get__(
|
|
176
|
+
self, obj: object, objtype: type | None = None
|
|
177
|
+
) -> _cached_wrapper[_P, _R]: ...
|
|
161
178
|
def __call__(self, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
|
162
179
|
def cache_clear(self) -> None: ...
|
|
163
180
|
|
|
164
181
|
@type_check_only
|
|
165
182
|
class _cached_wrapper_info(_cached_wrapper[_P, _R]):
|
|
183
|
+
@overload
|
|
184
|
+
def __get__(
|
|
185
|
+
self, obj: None, objtype: type | None = None
|
|
186
|
+
) -> _cached_wrapper_info[_P, _R]: ...
|
|
187
|
+
@overload
|
|
188
|
+
def __get__(
|
|
189
|
+
self: _cached_wrapper_info[Concatenate[_S, _Q], _R],
|
|
190
|
+
obj: _S,
|
|
191
|
+
objtype: type | None = None,
|
|
192
|
+
) -> _cached_wrapper_info[_Q, _R]: ...
|
|
193
|
+
# pyright cannot match the self-typed base overload; each return type is
|
|
194
|
+
# the _cached_wrapper_info subtype of the corresponding base return type
|
|
195
|
+
@overload
|
|
196
|
+
def __get__( # pyright: ignore[reportIncompatibleMethodOverride]
|
|
197
|
+
self, obj: object, objtype: type | None = None
|
|
198
|
+
) -> _cached_wrapper_info[_P, _R]: ...
|
|
166
199
|
def cache_info(self) -> _CacheInfo: ...
|
|
167
200
|
|
|
168
201
|
@overload
|
|
@@ -191,16 +224,34 @@ def cached(
|
|
|
191
224
|
info: Literal[True],
|
|
192
225
|
) -> Callable[[Callable[_P, _R]], _cached_wrapper_info[_P, _R]]: ...
|
|
193
226
|
|
|
227
|
+
@type_check_only
|
|
228
|
+
class _cachedmethod_unbound(Generic[_P, _R]):
|
|
229
|
+
__wrapped__: Callable[Concatenate[Any, _P], _R]
|
|
230
|
+
__name__: str
|
|
231
|
+
__qualname__: str
|
|
232
|
+
__doc__: str | None
|
|
233
|
+
@overload
|
|
234
|
+
def __call__(self, obj: Any, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
|
235
|
+
# deprecated @classmethod use
|
|
236
|
+
@overload
|
|
237
|
+
def __call__(self, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
|
238
|
+
|
|
194
239
|
@type_check_only
|
|
195
240
|
class _cachedmethod_wrapper(Generic[_P, _R]):
|
|
196
241
|
__wrapped__: Callable[Concatenate[Any, _P], _R]
|
|
197
242
|
__name__: str
|
|
243
|
+
__qualname__: str
|
|
198
244
|
__doc__: str | None
|
|
199
245
|
cache: MutableMapping[Any, Any]
|
|
200
246
|
cache_key: Callable[..., Any] = ...
|
|
201
247
|
cache_lock: AbstractContextManager[Any] | None = None
|
|
202
248
|
cache_condition: _AbstractCondition | None = None
|
|
203
249
|
def __set_name__(self, owner: type, name: str) -> None: ...
|
|
250
|
+
@overload
|
|
251
|
+
def __get__(
|
|
252
|
+
self, obj: None, objtype: type | None = None
|
|
253
|
+
) -> _cachedmethod_unbound[_P, _R]: ...
|
|
254
|
+
@overload
|
|
204
255
|
def __get__(
|
|
205
256
|
self, obj: Any, objtype: type | None = None
|
|
206
257
|
) -> _cachedmethod_wrapper[_P, _R]: ...
|
|
@@ -209,6 +260,11 @@ class _cachedmethod_wrapper(Generic[_P, _R]):
|
|
|
209
260
|
|
|
210
261
|
@type_check_only
|
|
211
262
|
class _cachedmethod_wrapper_info(_cachedmethod_wrapper[_P, _R]):
|
|
263
|
+
@overload
|
|
264
|
+
def __get__(
|
|
265
|
+
self, obj: None, objtype: type | None = None
|
|
266
|
+
) -> _cachedmethod_unbound[_P, _R]: ...
|
|
267
|
+
@overload
|
|
212
268
|
def __get__(
|
|
213
269
|
self, obj: Any, objtype: type | None = None
|
|
214
270
|
) -> _cachedmethod_wrapper_info[_P, _R]: ...
|
|
@@ -11,6 +11,10 @@ import warnings
|
|
|
11
11
|
# over ease of maintenance, thus providing separate wrappers for
|
|
12
12
|
# all valid combinations of decorator parameters lock, condition and
|
|
13
13
|
# info.
|
|
14
|
+
#
|
|
15
|
+
# Each wrapper calls functools.update_wrapper() before setting its own
|
|
16
|
+
# attributes, so func.__dict__ cannot override wrapper state, e.g. when
|
|
17
|
+
# stacking decorators.
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
def _condition_info(func, cache, key, lock, cond, info):
|
|
@@ -52,6 +56,7 @@ def _condition_info(func, cache, key, lock, cond, info):
|
|
|
52
56
|
with lock:
|
|
53
57
|
return info(hits, misses)
|
|
54
58
|
|
|
59
|
+
functools.update_wrapper(wrapper, func)
|
|
55
60
|
wrapper.cache_clear = cache_clear
|
|
56
61
|
wrapper.cache_info = cache_info
|
|
57
62
|
return wrapper
|
|
@@ -90,6 +95,7 @@ def _locked_info(func, cache, key, lock, info):
|
|
|
90
95
|
with lock:
|
|
91
96
|
return info(hits, misses)
|
|
92
97
|
|
|
98
|
+
functools.update_wrapper(wrapper, func)
|
|
93
99
|
wrapper.cache_clear = cache_clear
|
|
94
100
|
wrapper.cache_info = cache_info
|
|
95
101
|
return wrapper
|
|
@@ -122,6 +128,7 @@ def _unlocked_info(func, cache, key, info):
|
|
|
122
128
|
def cache_info():
|
|
123
129
|
return info(hits, misses)
|
|
124
130
|
|
|
131
|
+
functools.update_wrapper(wrapper, func)
|
|
125
132
|
wrapper.cache_clear = cache_clear
|
|
126
133
|
wrapper.cache_info = cache_info
|
|
127
134
|
return wrapper
|
|
@@ -139,6 +146,7 @@ def _uncached_info(func, info):
|
|
|
139
146
|
nonlocal misses
|
|
140
147
|
misses = 0
|
|
141
148
|
|
|
149
|
+
functools.update_wrapper(wrapper, func)
|
|
142
150
|
wrapper.cache_clear = cache_clear
|
|
143
151
|
wrapper.cache_info = lambda: info(0, misses)
|
|
144
152
|
return wrapper
|
|
@@ -173,6 +181,7 @@ def _condition(func, cache, key, lock, cond):
|
|
|
173
181
|
with lock:
|
|
174
182
|
cache.clear()
|
|
175
183
|
|
|
184
|
+
functools.update_wrapper(wrapper, func)
|
|
176
185
|
wrapper.cache_clear = cache_clear
|
|
177
186
|
return wrapper
|
|
178
187
|
|
|
@@ -199,6 +208,7 @@ def _locked(func, cache, key, lock):
|
|
|
199
208
|
with lock:
|
|
200
209
|
cache.clear()
|
|
201
210
|
|
|
211
|
+
functools.update_wrapper(wrapper, func)
|
|
202
212
|
wrapper.cache_clear = cache_clear
|
|
203
213
|
return wrapper
|
|
204
214
|
|
|
@@ -217,6 +227,7 @@ def _unlocked(func, cache, key):
|
|
|
217
227
|
pass # value too large
|
|
218
228
|
return v
|
|
219
229
|
|
|
230
|
+
functools.update_wrapper(wrapper, func)
|
|
220
231
|
wrapper.cache_clear = lambda: cache.clear()
|
|
221
232
|
return wrapper
|
|
222
233
|
|
|
@@ -225,6 +236,7 @@ def _uncached(func):
|
|
|
225
236
|
def wrapper(*args, **kwargs):
|
|
226
237
|
return func(*args, **kwargs)
|
|
227
238
|
|
|
239
|
+
functools.update_wrapper(wrapper, func)
|
|
228
240
|
wrapper.cache_clear = lambda: None
|
|
229
241
|
return wrapper
|
|
230
242
|
|
|
@@ -265,5 +277,4 @@ def _wrapper(func, cache, key, lock=None, cond=None, info=None):
|
|
|
265
277
|
wrapper.cache_key = key
|
|
266
278
|
wrapper.cache_lock = lock if lock is not None else cond
|
|
267
279
|
wrapper.cache_condition = cond
|
|
268
|
-
|
|
269
|
-
return functools.update_wrapper(wrapper, func)
|
|
280
|
+
return wrapper
|
|
@@ -5,24 +5,9 @@
|
|
|
5
5
|
__all__ = ()
|
|
6
6
|
|
|
7
7
|
import functools
|
|
8
|
-
import warnings
|
|
9
8
|
import weakref
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
def _warn_classmethod(stacklevel):
|
|
13
|
-
warnings.warn(
|
|
14
|
-
"decorating class methods with @cachedmethod is deprecated",
|
|
15
|
-
DeprecationWarning,
|
|
16
|
-
stacklevel=stacklevel,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _warn_instance_dict(msg, stacklevel):
|
|
21
|
-
warnings.warn(
|
|
22
|
-
msg,
|
|
23
|
-
DeprecationWarning,
|
|
24
|
-
stacklevel=stacklevel,
|
|
25
|
-
)
|
|
10
|
+
from ._descriptor import _ClassMethodDescriptor, _MethodDescriptor, _warn_classmethod
|
|
26
11
|
|
|
27
12
|
|
|
28
13
|
def _none(_):
|
|
@@ -65,82 +50,6 @@ class _WrapperBase:
|
|
|
65
50
|
return self.__cond(self._obj)
|
|
66
51
|
|
|
67
52
|
|
|
68
|
-
class _DescriptorBase:
|
|
69
|
-
"""Descriptor base class implementing the basic descriptor protocol."""
|
|
70
|
-
|
|
71
|
-
def __init__(self, deprecated=False):
|
|
72
|
-
self.__attrname = None
|
|
73
|
-
self.__deprecated = deprecated
|
|
74
|
-
|
|
75
|
-
def __set_name__(self, owner, name):
|
|
76
|
-
if self.__attrname is None:
|
|
77
|
-
self.__attrname = name
|
|
78
|
-
elif name != self.__attrname:
|
|
79
|
-
raise TypeError(
|
|
80
|
-
"Cannot assign the same @cachedmethod to two different names "
|
|
81
|
-
f"({self.__attrname!r} and {name!r})."
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
def __get__(self, obj, objtype=None):
|
|
85
|
-
wrapper = self.Wrapper(obj) # type: ignore
|
|
86
|
-
if obj is None:
|
|
87
|
-
# Return the wrapper itself without modification when accessed
|
|
88
|
-
# through the class to support class-level introspection, such
|
|
89
|
-
# as for mocking with autospec=True in unittest.mock.
|
|
90
|
-
pass
|
|
91
|
-
elif self.__attrname is not None:
|
|
92
|
-
# replace descriptor instance with wrapper in instance dict
|
|
93
|
-
try:
|
|
94
|
-
# In case of a race condition where another thread already replaced
|
|
95
|
-
# the descriptor, prefer the initial wrapper.
|
|
96
|
-
wrapper = obj.__dict__.setdefault(self.__attrname, wrapper)
|
|
97
|
-
except AttributeError:
|
|
98
|
-
# not all objects have __dict__ (e.g. class defines slots)
|
|
99
|
-
msg = (
|
|
100
|
-
f"No '__dict__' attribute on {type(obj).__name__!r} "
|
|
101
|
-
f"instance to cache {self.__attrname!r} property."
|
|
102
|
-
)
|
|
103
|
-
if self.__deprecated:
|
|
104
|
-
_warn_instance_dict(msg, 3)
|
|
105
|
-
else:
|
|
106
|
-
raise TypeError(msg) from None
|
|
107
|
-
except TypeError:
|
|
108
|
-
msg = (
|
|
109
|
-
f"The '__dict__' attribute on {type(obj).__name__!r} "
|
|
110
|
-
f"instance does not support item assignment for "
|
|
111
|
-
f"caching {self.__attrname!r} property."
|
|
112
|
-
)
|
|
113
|
-
if self.__deprecated:
|
|
114
|
-
_warn_instance_dict(msg, 3)
|
|
115
|
-
else:
|
|
116
|
-
raise TypeError(msg) from None
|
|
117
|
-
elif self.__deprecated:
|
|
118
|
-
pass # deprecated @classmethod, warning already raised elsewhere
|
|
119
|
-
else:
|
|
120
|
-
msg = "Cannot use @cachedmethod instance without calling __set_name__ on it"
|
|
121
|
-
raise TypeError(msg) from None
|
|
122
|
-
return wrapper
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
class _DeprecatedDescriptorBase(_DescriptorBase):
|
|
126
|
-
"""Descriptor base class supporting deprecated @classmethod use."""
|
|
127
|
-
|
|
128
|
-
def __init__(self, wrapper, cache_clear):
|
|
129
|
-
super().__init__(deprecated=True)
|
|
130
|
-
self.__wrapper = wrapper
|
|
131
|
-
self.__cache_clear = cache_clear
|
|
132
|
-
|
|
133
|
-
# called for @classmethod with Python >= 3.13
|
|
134
|
-
def __call__(self, *args, **kwargs):
|
|
135
|
-
_warn_classmethod(stacklevel=3)
|
|
136
|
-
return self.__wrapper(*args, **kwargs)
|
|
137
|
-
|
|
138
|
-
# backward-compatible @classmethod handling with Python >= 3.13
|
|
139
|
-
def cache_clear(self, objtype):
|
|
140
|
-
_warn_classmethod(stacklevel=3)
|
|
141
|
-
return self.__cache_clear(objtype)
|
|
142
|
-
|
|
143
|
-
|
|
144
53
|
# At least for now, the implementation prefers clarity and performance
|
|
145
54
|
# over ease of maintenance, thus providing separate descriptors for
|
|
146
55
|
# all valid combinations of decorator parameters lock, condition and
|
|
@@ -148,7 +57,7 @@ class _DeprecatedDescriptorBase(_DescriptorBase):
|
|
|
148
57
|
|
|
149
58
|
|
|
150
59
|
def _condition_info(method, cache, key, lock, cond, info):
|
|
151
|
-
class Descriptor(
|
|
60
|
+
class Descriptor(_MethodDescriptor):
|
|
152
61
|
class Wrapper(_WrapperBase):
|
|
153
62
|
def __init__(self, obj):
|
|
154
63
|
super().__init__(obj, method, cache, key, lock, cond)
|
|
@@ -196,7 +105,7 @@ def _condition_info(method, cache, key, lock, cond, info):
|
|
|
196
105
|
|
|
197
106
|
|
|
198
107
|
def _locked_info(method, cache, key, lock, info):
|
|
199
|
-
class Descriptor(
|
|
108
|
+
class Descriptor(_MethodDescriptor):
|
|
200
109
|
class Wrapper(_WrapperBase):
|
|
201
110
|
def __init__(self, obj):
|
|
202
111
|
super().__init__(obj, method, cache, key, lock)
|
|
@@ -236,7 +145,7 @@ def _locked_info(method, cache, key, lock, info):
|
|
|
236
145
|
|
|
237
146
|
|
|
238
147
|
def _unlocked_info(method, cache, key, info):
|
|
239
|
-
class Descriptor(
|
|
148
|
+
class Descriptor(_MethodDescriptor):
|
|
240
149
|
class Wrapper(_WrapperBase):
|
|
241
150
|
def __init__(self, obj):
|
|
242
151
|
super().__init__(obj, method, cache, key)
|
|
@@ -303,7 +212,7 @@ def _condition(method, cache, key, lock, cond):
|
|
|
303
212
|
p = pending.setdefault(self, set())
|
|
304
213
|
return wrapper(self, p, *args, **kwargs)
|
|
305
214
|
|
|
306
|
-
class Descriptor(
|
|
215
|
+
class Descriptor(_ClassMethodDescriptor):
|
|
307
216
|
class Wrapper(_WrapperBase):
|
|
308
217
|
def __init__(self, obj):
|
|
309
218
|
super().__init__(obj, method, cache, key, lock, cond)
|
|
@@ -343,7 +252,7 @@ def _locked(method, cache, key, lock):
|
|
|
343
252
|
with lock(self):
|
|
344
253
|
c.clear()
|
|
345
254
|
|
|
346
|
-
class Descriptor(
|
|
255
|
+
class Descriptor(_ClassMethodDescriptor):
|
|
347
256
|
class Wrapper(_WrapperBase):
|
|
348
257
|
def __init__(self, obj):
|
|
349
258
|
super().__init__(obj, method, cache, key, lock)
|
|
@@ -377,7 +286,7 @@ def _unlocked(method, cache, key):
|
|
|
377
286
|
c = cache(self)
|
|
378
287
|
c.clear()
|
|
379
288
|
|
|
380
|
-
class Descriptor(
|
|
289
|
+
class Descriptor(_ClassMethodDescriptor):
|
|
381
290
|
class Wrapper(_WrapperBase):
|
|
382
291
|
def __init__(self, obj):
|
|
383
292
|
super().__init__(obj, method, cache, key)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Descriptor helpers."""
|
|
2
|
+
|
|
3
|
+
__all__ = ()
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _warn_classmethod(stacklevel):
|
|
10
|
+
warnings.warn(
|
|
11
|
+
"decorating class methods with @cachedmethod is deprecated",
|
|
12
|
+
DeprecationWarning,
|
|
13
|
+
stacklevel=stacklevel,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _warn_instance_dict(msg, stacklevel):
|
|
18
|
+
warnings.warn(
|
|
19
|
+
msg,
|
|
20
|
+
DeprecationWarning,
|
|
21
|
+
stacklevel=stacklevel,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _MethodDescriptor:
|
|
26
|
+
"""Descriptor class implementing the basic descriptor protocol."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, deprecated=False):
|
|
29
|
+
self.__attrname = None
|
|
30
|
+
self.__deprecated = deprecated
|
|
31
|
+
self.__unbound = None
|
|
32
|
+
|
|
33
|
+
def __set_name__(self, owner, name):
|
|
34
|
+
if self.__attrname is None:
|
|
35
|
+
self.__attrname = name
|
|
36
|
+
elif name != self.__attrname:
|
|
37
|
+
raise TypeError(
|
|
38
|
+
"Cannot assign the same @cachedmethod to two different names "
|
|
39
|
+
f"({self.__attrname!r} and {name!r})."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def __get__(self, obj, objtype=None):
|
|
43
|
+
if obj is None:
|
|
44
|
+
# objtype is not used for instance lookups, so it need not be bound here
|
|
45
|
+
return self.__get_unbound()
|
|
46
|
+
elif self.__attrname is not None:
|
|
47
|
+
# replace descriptor instance with wrapper in instance dict
|
|
48
|
+
wrapper = self.Wrapper(obj) # type: ignore
|
|
49
|
+
try:
|
|
50
|
+
descriptor = next(
|
|
51
|
+
(
|
|
52
|
+
cls.__dict__[self.__attrname]
|
|
53
|
+
for cls in type(obj).__mro__
|
|
54
|
+
if self.__attrname in cls.__dict__
|
|
55
|
+
),
|
|
56
|
+
None,
|
|
57
|
+
)
|
|
58
|
+
if descriptor is self:
|
|
59
|
+
# In case of a race condition where another thread already replaced
|
|
60
|
+
# the descriptor, prefer the initial wrapper.
|
|
61
|
+
wrapper = obj.__dict__.setdefault(self.__attrname, wrapper)
|
|
62
|
+
else:
|
|
63
|
+
# A super() lookup must neither reuse the override's wrapper
|
|
64
|
+
# nor replace it, so preserve state separately for each descriptor.
|
|
65
|
+
wrappers = obj.__dict__.setdefault("__cachetools_cachedmethods", {})
|
|
66
|
+
wrapper = wrappers.setdefault(self, wrapper)
|
|
67
|
+
except AttributeError:
|
|
68
|
+
# not all objects have __dict__ (e.g. class defines slots)
|
|
69
|
+
msg = (
|
|
70
|
+
f"No '__dict__' attribute on {type(obj).__name__!r} "
|
|
71
|
+
f"instance to cache {self.__attrname!r} property."
|
|
72
|
+
)
|
|
73
|
+
if self.__deprecated:
|
|
74
|
+
_warn_instance_dict(msg, 3)
|
|
75
|
+
else:
|
|
76
|
+
raise TypeError(msg) from None
|
|
77
|
+
except TypeError:
|
|
78
|
+
msg = (
|
|
79
|
+
f"The '__dict__' attribute on {type(obj).__name__!r} "
|
|
80
|
+
f"instance does not support item assignment for "
|
|
81
|
+
f"caching {self.__attrname!r} property."
|
|
82
|
+
)
|
|
83
|
+
if self.__deprecated:
|
|
84
|
+
_warn_instance_dict(msg, 3)
|
|
85
|
+
else:
|
|
86
|
+
raise TypeError(msg) from None
|
|
87
|
+
elif self.__deprecated:
|
|
88
|
+
# deprecated @classmethod, warning already raised elsewhere
|
|
89
|
+
wrapper = self.Wrapper(obj) # type: ignore
|
|
90
|
+
else:
|
|
91
|
+
msg = "Cannot use @cachedmethod instance without calling __set_name__ on it"
|
|
92
|
+
raise TypeError(msg) from None
|
|
93
|
+
return wrapper
|
|
94
|
+
|
|
95
|
+
def __get_unbound(self):
|
|
96
|
+
# Created once and shared by all class accesses; objtype is not
|
|
97
|
+
# used for instance lookups, so it need not be bound here.
|
|
98
|
+
if self.__unbound is None:
|
|
99
|
+
descriptor = self
|
|
100
|
+
|
|
101
|
+
# As in the previous function wrapper, the receiver is named self.
|
|
102
|
+
@functools.wraps(self.__wrapped__) # type: ignore
|
|
103
|
+
def unbound(self, *args, **kwargs):
|
|
104
|
+
return descriptor.__get__(self)(*args, **kwargs)
|
|
105
|
+
|
|
106
|
+
self.__unbound = unbound # type: ignore
|
|
107
|
+
return self.__unbound
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class _ClassMethodDescriptor(_MethodDescriptor):
|
|
111
|
+
"""Descriptor base class supporting deprecated @classmethod use."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, wrapper, cache_clear):
|
|
114
|
+
super().__init__(deprecated=True)
|
|
115
|
+
self.__wrapper = wrapper
|
|
116
|
+
self.__cache_clear = cache_clear
|
|
117
|
+
|
|
118
|
+
# called for @classmethod with Python >= 3.13
|
|
119
|
+
def __call__(self, *args, **kwargs):
|
|
120
|
+
_warn_classmethod(stacklevel=3)
|
|
121
|
+
return self.__wrapper(*args, **kwargs)
|
|
122
|
+
|
|
123
|
+
# backward-compatible @classmethod handling with Python >= 3.13
|
|
124
|
+
def cache_clear(self, objtype):
|
|
125
|
+
_warn_classmethod(stacklevel=3)
|
|
126
|
+
return self.__cache_clear(objtype)
|
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
from collections.abc import Callable, Sequence
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
Concatenate,
|
|
5
|
+
Final,
|
|
6
|
+
Generic,
|
|
7
|
+
ParamSpec,
|
|
8
|
+
TypeVar,
|
|
9
|
+
overload,
|
|
10
|
+
type_check_only,
|
|
11
|
+
)
|
|
3
12
|
|
|
4
13
|
from . import _CacheInfo
|
|
5
14
|
|
|
6
15
|
__all__: Final = ("fifo_cache", "lfu_cache", "lru_cache", "rr_cache", "ttl_cache")
|
|
7
16
|
|
|
8
17
|
_P = ParamSpec("_P")
|
|
9
|
-
|
|
18
|
+
_Q = ParamSpec("_Q")
|
|
10
19
|
_R = TypeVar("_R")
|
|
20
|
+
_S = TypeVar("_S")
|
|
21
|
+
_T = TypeVar("_T")
|
|
11
22
|
|
|
12
23
|
@type_check_only
|
|
13
24
|
class _cachetools_cache_wrapper(Generic[_P, _R]):
|
|
14
25
|
__wrapped__: Callable[_P, _R]
|
|
15
26
|
__name__: str
|
|
27
|
+
__qualname__: str
|
|
16
28
|
__doc__: str | None
|
|
29
|
+
@overload # class access, e.g. Cls.method
|
|
30
|
+
def __get__(
|
|
31
|
+
self, obj: None, objtype: type | None = None
|
|
32
|
+
) -> _cachetools_cache_wrapper[_P, _R]: ...
|
|
33
|
+
@overload # instance access binds the first parameter
|
|
34
|
+
def __get__(
|
|
35
|
+
self: _cachetools_cache_wrapper[Concatenate[_S, _Q], _R],
|
|
36
|
+
obj: _S,
|
|
37
|
+
objtype: type | None = None,
|
|
38
|
+
) -> _cachetools_cache_wrapper[_Q, _R]: ...
|
|
39
|
+
@overload # fallback, e.g. when wrapped by @staticmethod or @classmethod
|
|
40
|
+
def __get__(
|
|
41
|
+
self, obj: object, objtype: type | None = None
|
|
42
|
+
) -> _cachetools_cache_wrapper[_P, _R]: ...
|
|
17
43
|
def __call__(self, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
|
18
44
|
def cache_info(self) -> _CacheInfo: ...
|
|
19
45
|
def cache_clear(self) -> None: ...
|
|
@@ -5,7 +5,7 @@ from typing import Any, Protocol
|
|
|
5
5
|
import cachetools
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class
|
|
8
|
+
class SimplifiedTestCaseProtocol(Protocol):
|
|
9
9
|
def assertEqual(self, first: Any, second: Any, msg: Any = None) -> None: ...
|
|
10
10
|
def assertNotEqual(self, first: Any, second: Any, msg: Any = None) -> None: ...
|
|
11
11
|
def assertTrue(self, expr: Any, msg: Any = None) -> None: ...
|
|
@@ -29,7 +29,7 @@ class _TestCaseProtocol(Protocol):
|
|
|
29
29
|
) -> Any: ...
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class CacheTestMixin(
|
|
32
|
+
class CacheTestMixin(SimplifiedTestCaseProtocol):
|
|
33
33
|
Cache: type[cachetools.Cache]
|
|
34
34
|
|
|
35
35
|
def test_defaults(self):
|
|
@@ -4,10 +4,10 @@ import warnings
|
|
|
4
4
|
import cachetools
|
|
5
5
|
import cachetools.keys
|
|
6
6
|
|
|
7
|
-
from . import CountedCondition, CountedLock,
|
|
7
|
+
from . import CountedCondition, CountedLock, SimplifiedTestCaseProtocol
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class DecoratorTestMixin(
|
|
10
|
+
class DecoratorTestMixin(SimplifiedTestCaseProtocol):
|
|
11
11
|
def cache(self, minsize):
|
|
12
12
|
raise NotImplementedError
|
|
13
13
|
|
|
@@ -154,6 +154,8 @@ class DecoratorTestMixin(_TestCaseProtocol):
|
|
|
154
154
|
wrapper = cachetools.cached(cache)(self.func)
|
|
155
155
|
|
|
156
156
|
self.assertEqual(wrapper.__wrapped__, self.func)
|
|
157
|
+
self.assertEqual(wrapper.__name__, "func")
|
|
158
|
+
self.assertEqual(wrapper.__qualname__, "DecoratorTestMixin.func")
|
|
157
159
|
|
|
158
160
|
self.assertEqual(len(cache), 0)
|
|
159
161
|
self.assertEqual(wrapper.__wrapped__(0), 0)
|
|
@@ -316,6 +318,21 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
|
|
|
316
318
|
self.assertEqual(wrapper.cache_info(), (0, 0, 2, 0))
|
|
317
319
|
self.assertEqual(lock.count, 13)
|
|
318
320
|
|
|
321
|
+
def test_stacked_decorators(self):
|
|
322
|
+
inner_cache = self.cache(2)
|
|
323
|
+
outer_cache = self.cache(2)
|
|
324
|
+
inner_wrapper = cachetools.cached(inner_cache, info=True)(self.func)
|
|
325
|
+
outer_wrapper = cachetools.cached(outer_cache, info=True)(inner_wrapper)
|
|
326
|
+
self.assertIs(outer_wrapper.__wrapped__, inner_wrapper)
|
|
327
|
+
self.assertIs(outer_wrapper.cache, outer_cache)
|
|
328
|
+
self.assertEqual(outer_wrapper(0), 0)
|
|
329
|
+
self.assertEqual(outer_wrapper(0), 0)
|
|
330
|
+
self.assertEqual(outer_wrapper.cache_info(), (1, 1, 2, 1))
|
|
331
|
+
self.assertEqual(inner_wrapper.cache_info(), (0, 1, 2, 1))
|
|
332
|
+
outer_wrapper.cache_clear()
|
|
333
|
+
self.assertEqual(len(outer_cache), 0)
|
|
334
|
+
self.assertEqual(len(inner_cache), 1)
|
|
335
|
+
|
|
319
336
|
def test_zero_size_cache_decorator(self):
|
|
320
337
|
cache = self.cache(0)
|
|
321
338
|
wrapper = cachetools.cached(cache)(self.func)
|
|
@@ -380,6 +397,47 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
|
|
|
380
397
|
self.assertEqual(wrapper.cache_info(), (0, 1, 0, 0))
|
|
381
398
|
self.assertEqual(lock.count, 5)
|
|
382
399
|
|
|
400
|
+
def test_decorator_method(self):
|
|
401
|
+
"""Though it is not encouraged, like functools.lru_cache, the
|
|
402
|
+
@cached decorator may be used on methods, too.
|
|
403
|
+
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
cache = self.cache(2)
|
|
407
|
+
|
|
408
|
+
class Cached:
|
|
409
|
+
@cachetools.cached(cache, key=cachetools.keys.methodkey)
|
|
410
|
+
def get(self, value: int) -> int:
|
|
411
|
+
return value
|
|
412
|
+
|
|
413
|
+
@cachetools.cached(self.cache(2), key=cachetools.keys.methodkey, info=True)
|
|
414
|
+
def get_info(self, value):
|
|
415
|
+
return value
|
|
416
|
+
|
|
417
|
+
@staticmethod
|
|
418
|
+
@cachetools.cached(self.cache(2))
|
|
419
|
+
def get_static(value: int) -> int:
|
|
420
|
+
return value
|
|
421
|
+
|
|
422
|
+
cached = Cached()
|
|
423
|
+
self.assertEqual(cached.get(1), 1)
|
|
424
|
+
self.assertEqual(Cached.get(cached, 1), 1)
|
|
425
|
+
self.assertEqual(len(cache), 1)
|
|
426
|
+
cached.get.cache_clear()
|
|
427
|
+
self.assertEqual(len(cache), 0)
|
|
428
|
+
|
|
429
|
+
self.assertEqual(cached.get_info(1), 1)
|
|
430
|
+
self.assertEqual(cached.get_info(1), 1)
|
|
431
|
+
self.assertEqual(Cached.get_info(cached, 1), 1)
|
|
432
|
+
self.assertEqual(cached.get_info.cache_info(), (2, 1, 2, 1))
|
|
433
|
+
self.assertEqual(Cached.get_info.cache_info(), (2, 1, 2, 1))
|
|
434
|
+
cached.get_info.cache_clear()
|
|
435
|
+
self.assertEqual(cached.get_info.cache_info(), (0, 0, 2, 0))
|
|
436
|
+
self.assertEqual(Cached.get_info.cache_info(), (0, 0, 2, 0))
|
|
437
|
+
|
|
438
|
+
self.assertEqual(cached.get_static(1), 1)
|
|
439
|
+
self.assertEqual(Cached.get_static(1), 1)
|
|
440
|
+
|
|
383
441
|
|
|
384
442
|
class DictWrapperTest(unittest.TestCase, DecoratorTestMixin):
|
|
385
443
|
def cache(self, minsize):
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import unittest
|
|
2
|
-
import unittest.mock
|
|
3
2
|
import warnings
|
|
4
3
|
|
|
5
4
|
from cachetools import Cache, cachedmethod, keys
|
|
6
5
|
|
|
7
|
-
from . import CountedCondition, CountedLock,
|
|
6
|
+
from . import CountedCondition, CountedLock, SimplifiedTestCaseProtocol
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class Cached:
|
|
@@ -77,7 +76,7 @@ class Unhashable(Cached):
|
|
|
77
76
|
raise TypeError("unhashable type")
|
|
78
77
|
|
|
79
78
|
|
|
80
|
-
class MethodDecoratorTestMixin(
|
|
79
|
+
class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
|
|
81
80
|
def cache(self, _minsize, **_kwargs):
|
|
82
81
|
raise NotImplementedError
|
|
83
82
|
|
|
@@ -357,6 +356,7 @@ class MethodDecoratorTestMixin(_TestCaseProtocol):
|
|
|
357
356
|
self.assertEqual(len(cache), 0)
|
|
358
357
|
self.assertEqual(cached.get.__wrapped__(cached, 0), 0)
|
|
359
358
|
self.assertEqual(cached.get.__name__, "get")
|
|
359
|
+
self.assertEqual(cached.get.__qualname__, "Cached.get")
|
|
360
360
|
self.assertEqual((cached.get.__doc__ or "").strip(), "docstring")
|
|
361
361
|
self.assertEqual(len(cache), 0)
|
|
362
362
|
self.assertEqual(cached.get(0), 1)
|
|
@@ -632,6 +632,69 @@ class CacheMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
|
|
|
632
632
|
self.assertEqual(cached.get(2), 2)
|
|
633
633
|
self.assertNotIn(2, cache.values())
|
|
634
634
|
|
|
635
|
+
def test_undecorated_override(self):
|
|
636
|
+
class Child(Cached):
|
|
637
|
+
def get_info(self, value): # type: ignore
|
|
638
|
+
return super().get_info(value) + 1
|
|
639
|
+
|
|
640
|
+
cache = self.cache(2)
|
|
641
|
+
cached = Child(cache)
|
|
642
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
643
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
644
|
+
self.assertEqual(super(Child, cached).get_info(0), 0)
|
|
645
|
+
self.assertEqual(cached.get_info(1), 2)
|
|
646
|
+
self.assertEqual(super(Child, cached).get_info(1), 1)
|
|
647
|
+
self.assertNotIn("get_info", vars(cached))
|
|
648
|
+
self.assertEqual(super(Child, cached).get_info.cache_info(), (3, 2, 2, 2))
|
|
649
|
+
|
|
650
|
+
def test_decorated_override(self):
|
|
651
|
+
class Child(Cached):
|
|
652
|
+
def __init__(self, base_cache, child_cache):
|
|
653
|
+
super().__init__(base_cache)
|
|
654
|
+
self.child_cache = child_cache
|
|
655
|
+
|
|
656
|
+
@cachedmethod(lambda self: self.child_cache, info=True)
|
|
657
|
+
def get_info(self, value):
|
|
658
|
+
return super().get_info(value) + 1
|
|
659
|
+
|
|
660
|
+
cached = Child(self.cache(2), self.cache(2))
|
|
661
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
662
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
663
|
+
self.assertEqual(super(Child, cached).get_info(0), 0)
|
|
664
|
+
self.assertEqual(cached.get_info(1), 2)
|
|
665
|
+
self.assertEqual(super(Child, cached).get_info(1), 1)
|
|
666
|
+
self.assertIn("get_info", vars(cached))
|
|
667
|
+
self.assertEqual(cached.get_info.cache_info(), (1, 2, 2, 2))
|
|
668
|
+
self.assertEqual(super(Child, cached).get_info.cache_info(), (2, 2, 2, 2))
|
|
669
|
+
cached.get_info.cache_clear()
|
|
670
|
+
self.assertEqual(cached.get_info.cache_info(), (0, 0, 2, 0))
|
|
671
|
+
self.assertEqual(super(Child, cached).get_info.cache_info(), (2, 2, 2, 2))
|
|
672
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
673
|
+
self.assertEqual(cached.get_info.cache_info(), (0, 1, 2, 1))
|
|
674
|
+
self.assertEqual(super(Child, cached).get_info.cache_info(), (3, 2, 2, 2))
|
|
675
|
+
super(Child, cached).get_info.cache_clear()
|
|
676
|
+
self.assertEqual(cached.get_info.cache_info(), (0, 1, 2, 1))
|
|
677
|
+
self.assertEqual(super(Child, cached).get_info.cache_info(), (0, 0, 2, 0))
|
|
678
|
+
|
|
679
|
+
def test_multilevel_override(self):
|
|
680
|
+
class Child(Cached):
|
|
681
|
+
@cachedmethod(lambda self: self.cache, key=keys.typedmethodkey)
|
|
682
|
+
def get_typed(self, value):
|
|
683
|
+
return super().get_typed(float(value)) + 1
|
|
684
|
+
|
|
685
|
+
class GrandChild(Child):
|
|
686
|
+
@cachedmethod(lambda self: self.cache, key=keys.typedmethodkey, info=True)
|
|
687
|
+
def get_typed(self, value):
|
|
688
|
+
return super().get_typed(int(value)) + 1
|
|
689
|
+
|
|
690
|
+
cache = self.cache(5) # shared cache, but different key types
|
|
691
|
+
cached = GrandChild(cache)
|
|
692
|
+
self.assertEqual(cached.get_typed("0"), 2)
|
|
693
|
+
self.assertEqual(cached.get_typed("0"), 2)
|
|
694
|
+
self.assertEqual(cached.get_typed.cache_info(), (1, 1, 5, 3))
|
|
695
|
+
self.assertEqual(super(GrandChild, cached).get_typed(0), 1)
|
|
696
|
+
self.assertEqual(super(Child, cached).get_typed(0.0), 0)
|
|
697
|
+
|
|
635
698
|
|
|
636
699
|
class DictMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
|
|
637
700
|
def cache(self, minsize, **_kwargs):
|
|
@@ -683,8 +746,53 @@ class NoneMethodTest(unittest.TestCase):
|
|
|
683
746
|
wrapper.cache_info()
|
|
684
747
|
|
|
685
748
|
|
|
686
|
-
class
|
|
749
|
+
class UnboundMethodTest(unittest.TestCase):
|
|
750
|
+
def test_class_call(self):
|
|
751
|
+
cached = Cached(Cache(2))
|
|
752
|
+
self.assertEqual(Cached.get(cached, 1), 0)
|
|
753
|
+
self.assertEqual(cached.get(1), 0)
|
|
754
|
+
self.assertEqual(Cached.get(cached, 2), 1)
|
|
755
|
+
self.assertEqual(cached.get(2), 1)
|
|
756
|
+
self.assertEqual(cached.count, 2)
|
|
757
|
+
other = Cached(Cache(2), count=10)
|
|
758
|
+
self.assertEqual(Cached.get(other, 1), 10)
|
|
759
|
+
self.assertEqual(cached.get(1), 0)
|
|
760
|
+
|
|
761
|
+
def test_class_call_info(self):
|
|
762
|
+
cached = Cached(Cache(2))
|
|
763
|
+
self.assertEqual(Cached.get_info(cached, 1), 0)
|
|
764
|
+
self.assertEqual(cached.get_info(1), 0)
|
|
765
|
+
self.assertEqual(Cached.get_info(cached, 2), 1)
|
|
766
|
+
self.assertEqual(cached.get_info(2), 1)
|
|
767
|
+
self.assertEqual(cached.count, 2)
|
|
768
|
+
self.assertEqual(cached.get_info.cache_info(), (2, 2, 2, 2))
|
|
769
|
+
other = Cached(Cache(2), count=10)
|
|
770
|
+
self.assertEqual(Cached.get_info(other, 1), 10)
|
|
771
|
+
self.assertEqual(cached.get_info(1), 0)
|
|
772
|
+
|
|
773
|
+
def test_base_class_call(self):
|
|
774
|
+
cached = Unhashable(Cache(2))
|
|
775
|
+
self.assertEqual(Cached.get(cached, 1), 0)
|
|
776
|
+
self.assertEqual(cached.get(1), 0)
|
|
777
|
+
self.assertEqual(cached.count, 1)
|
|
778
|
+
|
|
779
|
+
def test_class_access_identity(self):
|
|
780
|
+
self.assertIs(Cached.get, Cached.get)
|
|
781
|
+
self.assertIs(Cached.get_info, Cached.get_info)
|
|
782
|
+
self.assertIs(Unhashable.get, Cached.get)
|
|
783
|
+
self.assertIsNot(Cached.get, Cached.get_info)
|
|
784
|
+
|
|
785
|
+
def test_wrapped_method(self):
|
|
786
|
+
cached = Cached(Cache(2))
|
|
787
|
+
self.assertIs(Cached.get.__wrapped__, cached.get.__wrapped__)
|
|
788
|
+
self.assertEqual(Cached.get.__name__, "get")
|
|
789
|
+
self.assertEqual(Cached.get.__qualname__, "Cached.get")
|
|
790
|
+
self.assertEqual(Cached.get.__wrapped__(cached, 1), 0)
|
|
791
|
+
self.assertEqual(Cached.get.__wrapped__(cached, 1), 1)
|
|
792
|
+
self.assertEqual(len(cached.cache), 0)
|
|
793
|
+
|
|
687
794
|
def test_autospec_no_warnings(self):
|
|
795
|
+
import unittest.mock
|
|
688
796
|
|
|
689
797
|
with warnings.catch_warnings(record=True) as w:
|
|
690
798
|
warnings.simplefilter("always")
|
|
@@ -4,10 +4,10 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
import cachetools.func
|
|
6
6
|
|
|
7
|
-
from . import
|
|
7
|
+
from . import SimplifiedTestCaseProtocol
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class DecoratorTestMixin(
|
|
10
|
+
class DecoratorTestMixin(SimplifiedTestCaseProtocol):
|
|
11
11
|
DECORATOR: Callable[..., Any]
|
|
12
12
|
|
|
13
13
|
def decorator(self, maxsize, **kwargs):
|
|
@@ -109,10 +109,77 @@ class DecoratorTestMixin(_TestCaseProtocol):
|
|
|
109
109
|
# Then do a call which will cause a deadlock with a non-reentrant lock.
|
|
110
110
|
self.assertEqual(cached(RecursiveEquals(True)), RecursiveEquals(True))
|
|
111
111
|
|
|
112
|
+
def test_decorator_method(self):
|
|
113
|
+
"""Though it is not encouraged, like functools.lru_cache, the
|
|
114
|
+
cachetools.func decorators may be used on methods, too.
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
decorator = self.decorator(maxsize=2)
|
|
119
|
+
|
|
120
|
+
class Cached:
|
|
121
|
+
@decorator
|
|
122
|
+
def get(self, n):
|
|
123
|
+
return n
|
|
124
|
+
|
|
125
|
+
cached = Cached()
|
|
126
|
+
self.assertEqual(cached.get(1), 1)
|
|
127
|
+
self.assertEqual(cached.get(1), 1)
|
|
128
|
+
self.assertEqual(Cached.get(cached, 1), 1)
|
|
129
|
+
self.assertEqual(cached.get.cache_info(), (2, 1, 2, 1))
|
|
130
|
+
self.assertEqual(Cached.get.cache_info(), (2, 1, 2, 1))
|
|
131
|
+
cached.get.cache_clear()
|
|
132
|
+
self.assertEqual(cached.get.cache_info(), (0, 0, 2, 0))
|
|
133
|
+
self.assertEqual(Cached.get.cache_info(), (0, 0, 2, 0))
|
|
134
|
+
|
|
135
|
+
def test_stacked_decorators(self):
|
|
136
|
+
inner_wrapper = self.decorator(maxsize=2)(lambda n: n)
|
|
137
|
+
outer_wrapper = self.decorator(maxsize=3, typed=True)(inner_wrapper)
|
|
138
|
+
self.assertIs(outer_wrapper.__wrapped__, inner_wrapper)
|
|
139
|
+
self.assertEqual(
|
|
140
|
+
inner_wrapper.cache_parameters(), {"maxsize": 2, "typed": False}
|
|
141
|
+
)
|
|
142
|
+
self.assertEqual(
|
|
143
|
+
outer_wrapper.cache_parameters(), {"maxsize": 3, "typed": True}
|
|
144
|
+
)
|
|
145
|
+
self.assertEqual(outer_wrapper(1), 1)
|
|
146
|
+
self.assertEqual(outer_wrapper(1), 1)
|
|
147
|
+
self.assertEqual(outer_wrapper.cache_info(), (1, 1, 3, 1))
|
|
148
|
+
self.assertEqual(inner_wrapper.cache_info(), (0, 1, 2, 1))
|
|
149
|
+
outer_wrapper.cache_clear()
|
|
150
|
+
self.assertEqual(outer_wrapper.cache_info(), (0, 0, 3, 0))
|
|
151
|
+
self.assertEqual(inner_wrapper.cache_info(), (0, 1, 2, 1))
|
|
152
|
+
|
|
112
153
|
|
|
113
154
|
class FIFODecoratorTest(unittest.TestCase, DecoratorTestMixin):
|
|
114
155
|
DECORATOR = staticmethod(cachetools.func.fifo_cache)
|
|
115
156
|
|
|
157
|
+
def test_method_typed(self):
|
|
158
|
+
"""Type-checked by pyright via the stub __get__ overloads."""
|
|
159
|
+
|
|
160
|
+
class Cached:
|
|
161
|
+
@cachetools.func.fifo_cache(maxsize=2)
|
|
162
|
+
def get(self, n: int) -> int:
|
|
163
|
+
return n
|
|
164
|
+
|
|
165
|
+
@cachetools.func.fifo_cache
|
|
166
|
+
def get_bare(self, n: int) -> int:
|
|
167
|
+
return n
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
@cachetools.func.fifo_cache
|
|
171
|
+
def get_static(n: int) -> int:
|
|
172
|
+
return n
|
|
173
|
+
|
|
174
|
+
cached = Cached()
|
|
175
|
+
result: int = cached.get(1)
|
|
176
|
+
self.assertEqual(result, 1)
|
|
177
|
+
self.assertEqual(Cached.get(cached, 1), 1)
|
|
178
|
+
self.assertEqual(cached.get.cache_info(), (1, 1, 2, 1))
|
|
179
|
+
self.assertEqual(cached.get_bare(1), 1)
|
|
180
|
+
self.assertEqual(cached.get_static(1), 1)
|
|
181
|
+
self.assertEqual(Cached.get_static(1), 1)
|
|
182
|
+
|
|
116
183
|
|
|
117
184
|
class LFUDecoratorTest(unittest.TestCase, DecoratorTestMixin):
|
|
118
185
|
DECORATOR = staticmethod(cachetools.func.lfu_cache)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from threading import Barrier, Condition, Lock, Thread
|
|
3
|
+
|
|
4
|
+
from cachetools import LRUCache, cached, cachedmethod
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ThreadingTest(unittest.TestCase):
|
|
8
|
+
NTHREADS = 10
|
|
9
|
+
|
|
10
|
+
TIMEOUT = 10
|
|
11
|
+
|
|
12
|
+
def test_cached_lock(self):
|
|
13
|
+
for info in (False, True):
|
|
14
|
+
with self.subTest(info=info):
|
|
15
|
+
barrier = Barrier(self.NTHREADS, timeout=self.TIMEOUT)
|
|
16
|
+
cache = LRUCache(1)
|
|
17
|
+
|
|
18
|
+
@cached(cache, lock=Lock(), info=info)
|
|
19
|
+
def func(barrier=barrier):
|
|
20
|
+
result = object()
|
|
21
|
+
barrier.wait()
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
self.run_lock_test(func, cache, info)
|
|
25
|
+
|
|
26
|
+
def test_cachedmethod_lock(self):
|
|
27
|
+
for info in (False, True):
|
|
28
|
+
with self.subTest(info=info):
|
|
29
|
+
barrier = Barrier(self.NTHREADS, timeout=self.TIMEOUT)
|
|
30
|
+
cache = LRUCache(1)
|
|
31
|
+
lock = Lock()
|
|
32
|
+
|
|
33
|
+
class Cached:
|
|
34
|
+
@cachedmethod(lambda _: cache, lock=lambda _: lock, info=info)
|
|
35
|
+
def method(self, barrier=barrier):
|
|
36
|
+
result = object()
|
|
37
|
+
barrier.wait()
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
self.run_lock_test(Cached().method, cache, info)
|
|
41
|
+
|
|
42
|
+
def test_cached_cond(self):
|
|
43
|
+
for info in (False, True):
|
|
44
|
+
with self.subTest(info=info):
|
|
45
|
+
cache = LRUCache(1)
|
|
46
|
+
|
|
47
|
+
@cached(cache, condition=Condition(), info=info)
|
|
48
|
+
def func():
|
|
49
|
+
result = object()
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
self.run_cond_test(func, cache, info)
|
|
53
|
+
|
|
54
|
+
def test_cachedmethod_cond(self):
|
|
55
|
+
for info in (False, True):
|
|
56
|
+
with self.subTest(info=info):
|
|
57
|
+
cache = LRUCache(1)
|
|
58
|
+
cond = Condition()
|
|
59
|
+
|
|
60
|
+
class Cached:
|
|
61
|
+
@cachedmethod(lambda _: cache, condition=lambda _: cond, info=info)
|
|
62
|
+
def method(self):
|
|
63
|
+
result = object()
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
self.run_cond_test(Cached().method, cache, info)
|
|
67
|
+
|
|
68
|
+
def run_lock_test(self, func, cache, info):
|
|
69
|
+
self.run_threading_test(func, cache)
|
|
70
|
+
|
|
71
|
+
if info:
|
|
72
|
+
cache_info = func.cache_info()
|
|
73
|
+
self.assertEqual(cache_info.hits + cache_info.misses, self.NTHREADS)
|
|
74
|
+
self.assertGreaterEqual(cache_info.misses, 1)
|
|
75
|
+
|
|
76
|
+
def run_cond_test(self, func, cache, info):
|
|
77
|
+
self.run_threading_test(func, cache)
|
|
78
|
+
|
|
79
|
+
if info:
|
|
80
|
+
cache_info = func.cache_info()
|
|
81
|
+
self.assertEqual(cache_info.hits, self.NTHREADS - 1)
|
|
82
|
+
self.assertEqual(cache_info.misses, 1)
|
|
83
|
+
|
|
84
|
+
def run_threading_test(self, func, cache):
|
|
85
|
+
results = [None] * self.NTHREADS
|
|
86
|
+
|
|
87
|
+
def run(index):
|
|
88
|
+
results[index] = func()
|
|
89
|
+
|
|
90
|
+
threads = [Thread(target=run, args=[i]) for i in range(self.NTHREADS)]
|
|
91
|
+
for thread in threads:
|
|
92
|
+
thread.start()
|
|
93
|
+
for thread in threads:
|
|
94
|
+
thread.join(timeout=self.TIMEOUT)
|
|
95
|
+
self.assertFalse(thread.is_alive())
|
|
96
|
+
self.assertEqual(len(cache), 1)
|
|
97
|
+
|
|
98
|
+
# multiple threads may compute the value, but the first one
|
|
99
|
+
# should always win and all concurrent invocations should
|
|
100
|
+
# return the same result
|
|
101
|
+
cached_result = next(iter(cache.values()))
|
|
102
|
+
for result in results:
|
|
103
|
+
self.assertIs(result, cached_result)
|
|
@@ -218,6 +218,22 @@ class TTLCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
218
218
|
self.assertEqual([(1, 1)], list(items))
|
|
219
219
|
self.assertEqual(0, len(cache))
|
|
220
220
|
|
|
221
|
+
def test_ttl_datetime_overflow(self):
|
|
222
|
+
from datetime import datetime, timedelta
|
|
223
|
+
|
|
224
|
+
cache = TTLCache[str, int, datetime](
|
|
225
|
+
maxsize=1, ttl=timedelta(days=1), timer=lambda: datetime.max
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
with self.assertRaises(OverflowError):
|
|
229
|
+
cache["overflow"] = 1
|
|
230
|
+
|
|
231
|
+
self.assertNotIn("overflow", cache)
|
|
232
|
+
self.assertEqual(0, len(cache))
|
|
233
|
+
self.assertEqual(0, cache.currsize)
|
|
234
|
+
self.assertEqual([], list(cache))
|
|
235
|
+
self.assertEqual([], cache.expire())
|
|
236
|
+
|
|
221
237
|
def test_ttl_clear(self):
|
|
222
238
|
cache = TTLCache[int, int, int](maxsize=2, ttl=2, timer=Timer())
|
|
223
239
|
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
import time
|
|
3
|
-
import unittest
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from cachetools import LRUCache, cached, cachedmethod
|
|
7
|
-
|
|
8
|
-
count = 0
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@cached(cache=LRUCache(1), condition=threading.Condition(), info=True)
|
|
12
|
-
def func():
|
|
13
|
-
global count
|
|
14
|
-
time.sleep(1.0)
|
|
15
|
-
count += 1
|
|
16
|
-
return count
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@cached(cache=LRUCache(1), lock=threading.Lock(), info=True)
|
|
20
|
-
def locked_func():
|
|
21
|
-
global count
|
|
22
|
-
time.sleep(1.0)
|
|
23
|
-
count += 1
|
|
24
|
-
return 42
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class ThreadingTest(unittest.TestCase):
|
|
28
|
-
NTHREADS = 10
|
|
29
|
-
|
|
30
|
-
TIMEOUT = 10
|
|
31
|
-
|
|
32
|
-
cache: LRUCache[Any, int] = LRUCache(1)
|
|
33
|
-
|
|
34
|
-
cond = threading.Condition()
|
|
35
|
-
|
|
36
|
-
lock = threading.Lock()
|
|
37
|
-
|
|
38
|
-
count = 0
|
|
39
|
-
|
|
40
|
-
@cachedmethod(
|
|
41
|
-
cache=lambda self: self.cache, condition=lambda self: self.cond, info=True
|
|
42
|
-
)
|
|
43
|
-
def method(self):
|
|
44
|
-
time.sleep(1.0)
|
|
45
|
-
self.count += 1
|
|
46
|
-
return 42
|
|
47
|
-
|
|
48
|
-
@cachedmethod(cache=lambda self: self.cache, lock=lambda self: self.lock, info=True)
|
|
49
|
-
def locked_method(self):
|
|
50
|
-
time.sleep(1.0)
|
|
51
|
-
self.count += 1
|
|
52
|
-
return 42
|
|
53
|
-
|
|
54
|
-
def test_cached_stampede(self):
|
|
55
|
-
global count
|
|
56
|
-
count = 0
|
|
57
|
-
threads = [threading.Thread(target=func) for i in range(self.NTHREADS)]
|
|
58
|
-
for t in threads:
|
|
59
|
-
t.start()
|
|
60
|
-
for t in threads:
|
|
61
|
-
t.join(timeout=self.TIMEOUT)
|
|
62
|
-
self.assertFalse(t.is_alive())
|
|
63
|
-
|
|
64
|
-
self.assertEqual(count, 1)
|
|
65
|
-
|
|
66
|
-
info = func.cache_info()
|
|
67
|
-
self.assertEqual(info.hits, self.NTHREADS - 1)
|
|
68
|
-
self.assertEqual(info.misses, 1)
|
|
69
|
-
|
|
70
|
-
def test_cachedmethod_stampede(self):
|
|
71
|
-
self.cache = LRUCache(1)
|
|
72
|
-
self.count = 0
|
|
73
|
-
threads = [threading.Thread(target=self.method) for i in range(self.NTHREADS)]
|
|
74
|
-
for t in threads:
|
|
75
|
-
t.start()
|
|
76
|
-
for t in threads:
|
|
77
|
-
t.join(timeout=self.TIMEOUT)
|
|
78
|
-
self.assertFalse(t.is_alive())
|
|
79
|
-
|
|
80
|
-
self.assertEqual(self.count, 1)
|
|
81
|
-
|
|
82
|
-
info = self.method.cache_info()
|
|
83
|
-
self.assertEqual(info.hits, self.NTHREADS - 1)
|
|
84
|
-
self.assertEqual(info.misses, 1)
|
|
85
|
-
|
|
86
|
-
def test_cached_locked(self):
|
|
87
|
-
global count
|
|
88
|
-
count = 0
|
|
89
|
-
threads = [threading.Thread(target=locked_func) for i in range(self.NTHREADS)]
|
|
90
|
-
for t in threads:
|
|
91
|
-
t.start()
|
|
92
|
-
for t in threads:
|
|
93
|
-
t.join(timeout=self.TIMEOUT)
|
|
94
|
-
self.assertFalse(t.is_alive())
|
|
95
|
-
|
|
96
|
-
# Without condition/stampede prevention, func is called by multiple
|
|
97
|
-
# threads, but all return the same cached value (42).
|
|
98
|
-
self.assertGreaterEqual(count, 1)
|
|
99
|
-
|
|
100
|
-
info = locked_func.cache_info()
|
|
101
|
-
self.assertEqual(info.hits + info.misses, self.NTHREADS)
|
|
102
|
-
self.assertGreaterEqual(info.misses, 1)
|
|
103
|
-
|
|
104
|
-
def test_cachedmethod_locked(self):
|
|
105
|
-
self.cache = LRUCache(1)
|
|
106
|
-
self.count = 0
|
|
107
|
-
threads = [
|
|
108
|
-
threading.Thread(target=self.locked_method) for i in range(self.NTHREADS)
|
|
109
|
-
]
|
|
110
|
-
for t in threads:
|
|
111
|
-
t.start()
|
|
112
|
-
for t in threads:
|
|
113
|
-
t.join(timeout=self.TIMEOUT)
|
|
114
|
-
self.assertFalse(t.is_alive())
|
|
115
|
-
|
|
116
|
-
# Multiple threads may compute the value, but setdefault ensures
|
|
117
|
-
# the first cached result wins and all return 42.
|
|
118
|
-
self.assertGreaterEqual(self.count, 1)
|
|
119
|
-
|
|
120
|
-
info = self.locked_method.cache_info()
|
|
121
|
-
self.assertEqual(info.hits + info.misses, self.NTHREADS)
|
|
122
|
-
self.assertGreaterEqual(info.misses, 1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|