cachetools 7.2.2__tar.gz → 7.2.4__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.2 → cachetools-7.2.4}/CHANGELOG.rst +18 -0
- {cachetools-7.2.2/src/cachetools.egg-info → cachetools-7.2.4}/PKG-INFO +1 -1
- {cachetools-7.2.2 → cachetools-7.2.4}/pyproject.toml +1 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/__init__.py +1 -1
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/__init__.pyi +37 -1
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/_cached.py +13 -2
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/_descriptor.py +22 -7
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/func.pyi +28 -2
- {cachetools-7.2.2 → cachetools-7.2.4/src/cachetools.egg-info}/PKG-INFO +1 -1
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/__init__.py +40 -36
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_cached.py +44 -7
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_cachedmethod.py +29 -4
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_func.py +44 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_tlru.py +13 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_ttl.py +11 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tox.ini +1 -1
- {cachetools-7.2.2 → cachetools-7.2.4}/LICENSE +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/MANIFEST.in +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/README.rst +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/docs/conf.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/docs/index.rst +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/setup.cfg +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/_cachedmethod.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/func.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/keys.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/keys.pyi +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/py.typed +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools.egg-info/SOURCES.txt +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools.egg-info/dependency_links.txt +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools.egg-info/top_level.txt +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_cache.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_classmethod.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_fifo.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_keys.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_lfu.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_lru.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_rr.py +0 -0
- {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_threading.py +0 -0
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
v7.2.4 (2026-10-19)
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
- Restore autospec calls to ``@cachedmethod`` decorated methods.
|
|
5
|
+
|
|
6
|
+
- Improve branch coverage of unit tests.
|
|
7
|
+
|
|
8
|
+
- Minor type stub improvements.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
v7.2.3 (2026-10-17)
|
|
12
|
+
===================
|
|
13
|
+
|
|
14
|
+
- Fix ``@cached`` decorator stacking behavior.
|
|
15
|
+
|
|
16
|
+
- Minor type stub improvements.
|
|
17
|
+
|
|
18
|
+
|
|
1
19
|
v7.2.2 (2026-10-15)
|
|
2
20
|
===================
|
|
3
21
|
|
|
@@ -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")
|
|
@@ -141,7 +143,8 @@ class _CacheInfo(NamedTuple):
|
|
|
141
143
|
|
|
142
144
|
@type_check_only
|
|
143
145
|
class _AbstractCondition(AbstractContextManager[Any], Protocol):
|
|
144
|
-
# implementation and unit tests do not use plain wait()
|
|
146
|
+
# implementation and unit tests currently do not use plain wait()
|
|
147
|
+
# and notify(); these are reserved for future use
|
|
145
148
|
def wait(self, timeout: float | None = None) -> bool: ...
|
|
146
149
|
def wait_for(
|
|
147
150
|
self, predicate: Callable[[], _T], timeout: float | None = None
|
|
@@ -153,16 +156,47 @@ class _AbstractCondition(AbstractContextManager[Any], Protocol):
|
|
|
153
156
|
class _cached_wrapper(Generic[_P, _R]):
|
|
154
157
|
__wrapped__: Callable[_P, _R]
|
|
155
158
|
__name__: str
|
|
159
|
+
__qualname__: str
|
|
156
160
|
__doc__: str | None
|
|
157
161
|
cache: MutableMapping[Any, Any] | None
|
|
158
162
|
cache_key: Callable[..., Any] = ...
|
|
159
163
|
cache_lock: AbstractContextManager[Any] | None = None
|
|
160
164
|
cache_condition: _AbstractCondition | None = None
|
|
165
|
+
@overload # class access, e.g. Cls.method
|
|
166
|
+
def __get__(
|
|
167
|
+
self, obj: None, objtype: type | None = None
|
|
168
|
+
) -> _cached_wrapper[_P, _R]: ...
|
|
169
|
+
@overload # instance access binds the first parameter
|
|
170
|
+
def __get__(
|
|
171
|
+
self: _cached_wrapper[Concatenate[_S, _Q], _R],
|
|
172
|
+
obj: _S,
|
|
173
|
+
objtype: type | None = None,
|
|
174
|
+
) -> _cached_wrapper[_Q, _R]: ...
|
|
175
|
+
@overload # fallback, e.g. when wrapped by @staticmethod or @classmethod
|
|
176
|
+
def __get__(
|
|
177
|
+
self, obj: object, objtype: type | None = None
|
|
178
|
+
) -> _cached_wrapper[_P, _R]: ...
|
|
161
179
|
def __call__(self, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
|
162
180
|
def cache_clear(self) -> None: ...
|
|
163
181
|
|
|
164
182
|
@type_check_only
|
|
165
183
|
class _cached_wrapper_info(_cached_wrapper[_P, _R]):
|
|
184
|
+
@overload
|
|
185
|
+
def __get__(
|
|
186
|
+
self, obj: None, objtype: type | None = None
|
|
187
|
+
) -> _cached_wrapper_info[_P, _R]: ...
|
|
188
|
+
@overload
|
|
189
|
+
def __get__(
|
|
190
|
+
self: _cached_wrapper_info[Concatenate[_S, _Q], _R],
|
|
191
|
+
obj: _S,
|
|
192
|
+
objtype: type | None = None,
|
|
193
|
+
) -> _cached_wrapper_info[_Q, _R]: ...
|
|
194
|
+
# pyright cannot match the self-typed base overload; each return type is
|
|
195
|
+
# the _cached_wrapper_info subtype of the corresponding base return type
|
|
196
|
+
@overload
|
|
197
|
+
def __get__( # pyright: ignore[reportIncompatibleMethodOverride]
|
|
198
|
+
self, obj: object, objtype: type | None = None
|
|
199
|
+
) -> _cached_wrapper_info[_P, _R]: ...
|
|
166
200
|
def cache_info(self) -> _CacheInfo: ...
|
|
167
201
|
|
|
168
202
|
@overload
|
|
@@ -195,6 +229,7 @@ def cached(
|
|
|
195
229
|
class _cachedmethod_unbound(Generic[_P, _R]):
|
|
196
230
|
__wrapped__: Callable[Concatenate[Any, _P], _R]
|
|
197
231
|
__name__: str
|
|
232
|
+
__qualname__: str
|
|
198
233
|
__doc__: str | None
|
|
199
234
|
@overload
|
|
200
235
|
def __call__(self, obj: Any, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
|
@@ -206,6 +241,7 @@ class _cachedmethod_unbound(Generic[_P, _R]):
|
|
|
206
241
|
class _cachedmethod_wrapper(Generic[_P, _R]):
|
|
207
242
|
__wrapped__: Callable[Concatenate[Any, _P], _R]
|
|
208
243
|
__name__: str
|
|
244
|
+
__qualname__: str
|
|
209
245
|
__doc__: str | None
|
|
210
246
|
cache: MutableMapping[Any, Any]
|
|
211
247
|
cache_key: Callable[..., Any] = ...
|
|
@@ -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
|
|
@@ -96,15 +96,30 @@ class _MethodDescriptor:
|
|
|
96
96
|
# Created once and shared by all class accesses; objtype is not
|
|
97
97
|
# used for instance lookups, so it need not be bound here.
|
|
98
98
|
if self.__unbound is None:
|
|
99
|
-
|
|
99
|
+
self.__unbound = _Unbound(self)
|
|
100
|
+
return self.__unbound
|
|
100
101
|
|
|
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
102
|
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
class _Unbound:
|
|
104
|
+
"""Callable returned on class access.
|
|
105
|
+
|
|
106
|
+
Deliberately not a function: unittest.mock.create_autospec() only strips
|
|
107
|
+
the receiver from plain functions found in the class dict, so a function
|
|
108
|
+
here would leave the receiver in an autospec'd instance's signature.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, descriptor):
|
|
112
|
+
functools.update_wrapper(self, descriptor.__wrapped__)
|
|
113
|
+
self.__descriptor = descriptor
|
|
114
|
+
|
|
115
|
+
# Receiver is taken from *args so autospec'd instances accept any call,
|
|
116
|
+
# as they did before class access returned a callable.
|
|
117
|
+
def __call__(self, /, *args, **kwargs):
|
|
118
|
+
if not args:
|
|
119
|
+
msg = "Cannot call @cachedmethod through the class without an instance"
|
|
120
|
+
raise TypeError(msg)
|
|
121
|
+
obj, *args = args
|
|
122
|
+
return self.__descriptor.__get__(obj)(*args, **kwargs)
|
|
108
123
|
|
|
109
124
|
|
|
110
125
|
class _ClassMethodDescriptor(_MethodDescriptor):
|
|
@@ -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,6 +5,39 @@ from typing import Any, Protocol
|
|
|
5
5
|
import cachetools
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
class CountedLock:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.count = 0
|
|
11
|
+
|
|
12
|
+
def __enter__(self):
|
|
13
|
+
self.count += 1
|
|
14
|
+
|
|
15
|
+
def __exit__(self, *exc):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CountedCondition(CountedLock):
|
|
20
|
+
def __init__(self):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.wait_count = 0
|
|
23
|
+
self.notify_count = 0
|
|
24
|
+
|
|
25
|
+
def wait(self, timeout=None):
|
|
26
|
+
self.wait_count += 1
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
def wait_for(self, predicate, timeout=None):
|
|
30
|
+
res = predicate()
|
|
31
|
+
self.wait_count += 1
|
|
32
|
+
return res
|
|
33
|
+
|
|
34
|
+
def notify(self, n=1):
|
|
35
|
+
self.notify_count += 1
|
|
36
|
+
|
|
37
|
+
def notify_all(self):
|
|
38
|
+
self.notify_count += 1
|
|
39
|
+
|
|
40
|
+
|
|
8
41
|
class SimplifiedTestCaseProtocol(Protocol):
|
|
9
42
|
def assertEqual(self, first: Any, second: Any, msg: Any = None) -> None: ...
|
|
10
43
|
def assertNotEqual(self, first: Any, second: Any, msg: Any = None) -> None: ...
|
|
@@ -323,6 +356,13 @@ class CacheTestMixin(SimplifiedTestCaseProtocol):
|
|
|
323
356
|
self.assertEqual(3, cache[3])
|
|
324
357
|
self.assertEqual(7, cache[4])
|
|
325
358
|
|
|
359
|
+
# growing an existing entry may have to evict other entries
|
|
360
|
+
cache[4] = 9
|
|
361
|
+
self.assertEqual(1, len(cache))
|
|
362
|
+
self.assertEqual(9, cache.currsize)
|
|
363
|
+
self.assertNotIn(3, cache)
|
|
364
|
+
self.assertEqual(9, cache[4])
|
|
365
|
+
|
|
326
366
|
def test_getsizeof_replace_same_size(self):
|
|
327
367
|
cache = self.Cache(maxsize=10, getsizeof=lambda x: x)
|
|
328
368
|
|
|
@@ -453,39 +493,3 @@ class CacheTestMixin(SimplifiedTestCaseProtocol):
|
|
|
453
493
|
cache = pickle.loads(pickle.dumps(source))
|
|
454
494
|
self.assertEqual(n, len(cache))
|
|
455
495
|
self.assertEqual(source, cache)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
class CountedLock:
|
|
459
|
-
def __init__(self):
|
|
460
|
-
self.count = 0
|
|
461
|
-
|
|
462
|
-
def __enter__(self):
|
|
463
|
-
self.count += 1
|
|
464
|
-
|
|
465
|
-
def __exit__(self, *exc):
|
|
466
|
-
pass
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
class CountedCondition(CountedLock):
|
|
470
|
-
def __init__(self):
|
|
471
|
-
super().__init__()
|
|
472
|
-
self.wait_count = 0
|
|
473
|
-
self.notify_count = 0
|
|
474
|
-
|
|
475
|
-
# only wait_for() and notify_all() are currently used, but wait()
|
|
476
|
-
# and notify() are also required according to the docs...
|
|
477
|
-
|
|
478
|
-
def wait(self, timeout=None):
|
|
479
|
-
self.wait_count += 1
|
|
480
|
-
return True
|
|
481
|
-
|
|
482
|
-
def wait_for(self, predicate, timeout=None):
|
|
483
|
-
res = predicate()
|
|
484
|
-
self.wait_count += 1
|
|
485
|
-
return res
|
|
486
|
-
|
|
487
|
-
def notify(self, n=1):
|
|
488
|
-
self.notify_count += 1
|
|
489
|
-
|
|
490
|
-
def notify_all(self):
|
|
491
|
-
self.notify_count += 1
|
|
@@ -154,6 +154,8 @@ class DecoratorTestMixin(SimplifiedTestCaseProtocol):
|
|
|
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)
|
|
@@ -386,20 +403,40 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
|
|
|
386
403
|
|
|
387
404
|
"""
|
|
388
405
|
|
|
406
|
+
cache = self.cache(2)
|
|
407
|
+
|
|
389
408
|
class Cached:
|
|
409
|
+
@cachetools.cached(cache, key=cachetools.keys.methodkey)
|
|
410
|
+
def get(self, value: int) -> int:
|
|
411
|
+
return value
|
|
412
|
+
|
|
390
413
|
@cachetools.cached(self.cache(2), key=cachetools.keys.methodkey, info=True)
|
|
391
|
-
def
|
|
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:
|
|
392
420
|
return value
|
|
393
421
|
|
|
394
422
|
cached = Cached()
|
|
395
|
-
self.assertEqual(cached.get(1), 1)
|
|
396
|
-
self.assertEqual(cached.get(1), 1) # type: ignore
|
|
423
|
+
self.assertEqual(cached.get(1), 1)
|
|
397
424
|
self.assertEqual(Cached.get(cached, 1), 1)
|
|
398
|
-
self.assertEqual(
|
|
399
|
-
self.assertEqual(Cached.get.cache_info(), (2, 1, 2, 1))
|
|
425
|
+
self.assertEqual(len(cache), 1)
|
|
400
426
|
cached.get.cache_clear()
|
|
401
|
-
self.assertEqual(
|
|
402
|
-
|
|
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)
|
|
403
440
|
|
|
404
441
|
|
|
405
442
|
class DictWrapperTest(unittest.TestCase, DecoratorTestMixin):
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
# TODO: type stub overloads for unbound methods?
|
|
2
|
-
# pyright: reportCallIssue=false
|
|
3
|
-
|
|
4
1
|
import unittest
|
|
5
2
|
import warnings
|
|
6
3
|
|
|
@@ -359,6 +356,7 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
|
|
|
359
356
|
self.assertEqual(len(cache), 0)
|
|
360
357
|
self.assertEqual(cached.get.__wrapped__(cached, 0), 0)
|
|
361
358
|
self.assertEqual(cached.get.__name__, "get")
|
|
359
|
+
self.assertEqual(cached.get.__qualname__, "Cached.get")
|
|
362
360
|
self.assertEqual((cached.get.__doc__ or "").strip(), "docstring")
|
|
363
361
|
self.assertEqual(len(cache), 0)
|
|
364
362
|
self.assertEqual(cached.get(0), 1)
|
|
@@ -502,6 +500,17 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
|
|
|
502
500
|
|
|
503
501
|
bar = foo
|
|
504
502
|
|
|
503
|
+
def test_decorator_same_name(self):
|
|
504
|
+
# assigning to a second owner under the same name is accepted
|
|
505
|
+
class Other(Cached):
|
|
506
|
+
get = Cached.__dict__["get"]
|
|
507
|
+
|
|
508
|
+
other = Other(self.cache(2))
|
|
509
|
+
|
|
510
|
+
self.assertEqual(other.get(0), 0)
|
|
511
|
+
self.assertEqual(other.get(0), 0)
|
|
512
|
+
self.assertEqual(other.get(1), 1)
|
|
513
|
+
|
|
505
514
|
def test_decorator_no_set_name(self):
|
|
506
515
|
class Cached:
|
|
507
516
|
def foo(_self):
|
|
@@ -636,7 +645,7 @@ class CacheMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
|
|
|
636
645
|
|
|
637
646
|
def test_undecorated_override(self):
|
|
638
647
|
class Child(Cached):
|
|
639
|
-
def get_info(self, value): # type: ignore
|
|
648
|
+
def get_info(self, value): # type: ignore
|
|
640
649
|
return super().get_info(value) + 1
|
|
641
650
|
|
|
642
651
|
cache = self.cache(2)
|
|
@@ -778,6 +787,10 @@ class UnboundMethodTest(unittest.TestCase):
|
|
|
778
787
|
self.assertEqual(cached.get(1), 0)
|
|
779
788
|
self.assertEqual(cached.count, 1)
|
|
780
789
|
|
|
790
|
+
def test_class_call_without_instance(self):
|
|
791
|
+
with self.assertRaises(TypeError):
|
|
792
|
+
Cached.get() # type: ignore
|
|
793
|
+
|
|
781
794
|
def test_class_access_identity(self):
|
|
782
795
|
self.assertIs(Cached.get, Cached.get)
|
|
783
796
|
self.assertIs(Cached.get_info, Cached.get_info)
|
|
@@ -787,6 +800,8 @@ class UnboundMethodTest(unittest.TestCase):
|
|
|
787
800
|
def test_wrapped_method(self):
|
|
788
801
|
cached = Cached(Cache(2))
|
|
789
802
|
self.assertIs(Cached.get.__wrapped__, cached.get.__wrapped__)
|
|
803
|
+
self.assertEqual(Cached.get.__name__, "get")
|
|
804
|
+
self.assertEqual(Cached.get.__qualname__, "Cached.get")
|
|
790
805
|
self.assertEqual(Cached.get.__wrapped__(cached, 1), 0)
|
|
791
806
|
self.assertEqual(Cached.get.__wrapped__(cached, 1), 1)
|
|
792
807
|
self.assertEqual(len(cached.cache), 0)
|
|
@@ -798,3 +813,13 @@ class UnboundMethodTest(unittest.TestCase):
|
|
|
798
813
|
warnings.simplefilter("always")
|
|
799
814
|
unittest.mock.create_autospec(Cached, instance=True)
|
|
800
815
|
self.assertEqual(len(w), 0)
|
|
816
|
+
|
|
817
|
+
def test_autospec_call(self):
|
|
818
|
+
import unittest.mock
|
|
819
|
+
|
|
820
|
+
mock = unittest.mock.create_autospec(Cached, instance=True)
|
|
821
|
+
mock.get.return_value = 42
|
|
822
|
+
self.assertEqual(mock.get(1), 42)
|
|
823
|
+
mock.get.assert_called_with(1)
|
|
824
|
+
self.assertEqual(mock.get(value=1), 42)
|
|
825
|
+
mock.get.assert_called_with(value=1)
|
|
@@ -132,10 +132,54 @@ class DecoratorTestMixin(SimplifiedTestCaseProtocol):
|
|
|
132
132
|
self.assertEqual(cached.get.cache_info(), (0, 0, 2, 0))
|
|
133
133
|
self.assertEqual(Cached.get.cache_info(), (0, 0, 2, 0))
|
|
134
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
|
+
|
|
135
153
|
|
|
136
154
|
class FIFODecoratorTest(unittest.TestCase, DecoratorTestMixin):
|
|
137
155
|
DECORATOR = staticmethod(cachetools.func.fifo_cache)
|
|
138
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
|
+
|
|
139
183
|
|
|
140
184
|
class LFUDecoratorTest(unittest.TestCase, DecoratorTestMixin):
|
|
141
185
|
DECORATOR = staticmethod(cachetools.func.lfu_cache)
|
|
@@ -217,6 +217,19 @@ class TLRUCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
217
217
|
self.assertNotIn(2, cache)
|
|
218
218
|
self.assertNotIn(3, cache)
|
|
219
219
|
|
|
220
|
+
def test_ttu_iter_expired(self):
|
|
221
|
+
cache = TLRUCache[int, int, int](
|
|
222
|
+
maxsize=2, ttu=lambda _k, _v, t: t + 2, timer=Timer()
|
|
223
|
+
)
|
|
224
|
+
cache[1] = 1
|
|
225
|
+
cache.timer.tick()
|
|
226
|
+
cache[2] = 2
|
|
227
|
+
cache.timer.tick()
|
|
228
|
+
# a bare for loop skips expired items without removing them
|
|
229
|
+
self.assertEqual([2], [key for key in cache])
|
|
230
|
+
self.assertEqual({(1, 1)}, set(cache.expire()))
|
|
231
|
+
self.assertEqual([2], [key for key in cache])
|
|
232
|
+
|
|
220
233
|
def test_ttu_expired(self):
|
|
221
234
|
cache = TLRUCache[int, None, int](
|
|
222
235
|
maxsize=1, ttu=lambda k, _, t: t + k, timer=Timer()
|
|
@@ -177,6 +177,17 @@ class TTLCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
177
177
|
self.assertNotIn(2, cache)
|
|
178
178
|
self.assertNotIn(3, cache)
|
|
179
179
|
|
|
180
|
+
def test_ttl_iter_expired(self):
|
|
181
|
+
cache = TTLCache[int, int, int](maxsize=2, ttl=2, timer=Timer())
|
|
182
|
+
cache[1] = 1
|
|
183
|
+
cache.timer.tick()
|
|
184
|
+
cache[2] = 2
|
|
185
|
+
cache.timer.tick()
|
|
186
|
+
# a bare for loop skips expired items without removing them
|
|
187
|
+
self.assertEqual([2], [key for key in cache])
|
|
188
|
+
self.assertEqual({(1, 1)}, set(cache.expire()))
|
|
189
|
+
self.assertEqual([2], [key for key in cache])
|
|
190
|
+
|
|
180
191
|
def test_ttl_atomic(self):
|
|
181
192
|
cache = TTLCache[int, int, int](maxsize=1, ttl=2, timer=Timer(auto=True))
|
|
182
193
|
cache[1] = 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
|
|
File without changes
|