cachetools 7.2.0__tar.gz → 7.2.2__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.0 → cachetools-7.2.2}/CHANGELOG.rst +26 -0
- {cachetools-7.2.0/src/cachetools.egg-info → cachetools-7.2.2}/PKG-INFO +1 -1
- {cachetools-7.2.0 → cachetools-7.2.2}/docs/index.rst +3 -2
- {cachetools-7.2.0 → cachetools-7.2.2}/pyproject.toml +0 -3
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools/__init__.py +8 -8
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools/__init__.pyi +21 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools/_cached.py +2 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools/_cachedmethod.py +9 -98
- cachetools-7.2.2/src/cachetools/_descriptor.py +126 -0
- {cachetools-7.2.0 → cachetools-7.2.2/src/cachetools.egg-info}/PKG-INFO +1 -1
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools.egg-info/SOURCES.txt +1 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/__init__.py +2 -2
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_cached.py +23 -2
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_cachedmethod.py +112 -4
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_func.py +25 -2
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_rr.py +15 -0
- cachetools-7.2.2/tests/test_threading.py +103 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_ttl.py +16 -0
- cachetools-7.2.0/tests/test_threading.py +0 -122
- {cachetools-7.2.0 → cachetools-7.2.2}/LICENSE +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/MANIFEST.in +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/README.rst +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/docs/conf.py +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/setup.cfg +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools/func.py +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools/func.pyi +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools/keys.py +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools/keys.pyi +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools/py.typed +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools.egg-info/dependency_links.txt +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/src/cachetools.egg-info/top_level.txt +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_cache.py +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_classmethod.py +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_fifo.py +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_keys.py +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_lfu.py +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_lru.py +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/tests/test_tlru.py +0 -0
- {cachetools-7.2.0 → cachetools-7.2.2}/tox.ini +0 -0
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
v7.2.2 (2026-10-15)
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
- Fix ``@cachedmethod`` for subclasses that override a decorated
|
|
5
|
+
method and call the base class implementation via ``super()``.
|
|
6
|
+
|
|
7
|
+
- Restore class access to instance methods decorated with
|
|
8
|
+
``@cachedmethod``.
|
|
9
|
+
|
|
10
|
+
- Improve ``TTLCache`` robustness when calculating expiration time
|
|
11
|
+
fails.
|
|
12
|
+
|
|
13
|
+
- Various unit test improvements.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
v7.2.1 (2026-10-05)
|
|
17
|
+
===================
|
|
18
|
+
|
|
19
|
+
- Improve error handling for ``RRCache.popitem()`` when the cache is
|
|
20
|
+
empty.
|
|
21
|
+
|
|
22
|
+
- Minor style and documentation improvements.
|
|
23
|
+
|
|
24
|
+
- Update CI environment.
|
|
25
|
+
|
|
26
|
+
|
|
1
27
|
v7.2.0 (2026-09-16)
|
|
2
28
|
===================
|
|
3
29
|
|
|
@@ -756,8 +756,9 @@ this module provides several memoizing function decorators with a
|
|
|
756
756
|
similar API. All these decorators wrap a function with a memoizing
|
|
757
757
|
callable that saves up to the `maxsize` most recent calls, using
|
|
758
758
|
different caching strategies. If `maxsize` is set to :const:`None`,
|
|
759
|
-
the
|
|
760
|
-
|
|
759
|
+
the size limit is disabled and the cache can grow without bound.
|
|
760
|
+
For :func:`ttl_cache`, the time-to-live still applies, so cached
|
|
761
|
+
results expire after `ttl` even without a size limit.
|
|
761
762
|
|
|
762
763
|
If the optional argument `typed` is set to :const:`True`, function
|
|
763
764
|
arguments of different types will be cached separately. For example,
|
|
@@ -50,9 +50,6 @@ version = {attr = "cachetools.__version__"}
|
|
|
50
50
|
|
|
51
51
|
[tool.pyright]
|
|
52
52
|
typeCheckingMode = "standard"
|
|
53
|
-
reportFunctionMemberAccess = "information"
|
|
54
|
-
reportOptionalContextManager = "warning"
|
|
55
|
-
reportOptionalMemberAccess = "warning"
|
|
56
53
|
|
|
57
54
|
[tool.ruff.lint]
|
|
58
55
|
ignore = ["DTZ005", "UP031"]
|
|
@@ -12,7 +12,7 @@ __all__ = (
|
|
|
12
12
|
"cachedmethod",
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
__version__ = "7.2.
|
|
15
|
+
__version__ = "7.2.2"
|
|
16
16
|
|
|
17
17
|
import collections
|
|
18
18
|
import collections.abc
|
|
@@ -52,7 +52,7 @@ class Cache(collections.abc.MutableMapping):
|
|
|
52
52
|
def __init__(self, maxsize, getsizeof=None):
|
|
53
53
|
if maxsize < 0:
|
|
54
54
|
raise ValueError("maxsize must be non-negative")
|
|
55
|
-
if getsizeof:
|
|
55
|
+
if getsizeof is not None:
|
|
56
56
|
self.getsizeof = getsizeof
|
|
57
57
|
if self.getsizeof is not Cache.getsizeof:
|
|
58
58
|
self.__size = {}
|
|
@@ -357,12 +357,11 @@ class RRCache(Cache):
|
|
|
357
357
|
|
|
358
358
|
def popitem(self):
|
|
359
359
|
"""Remove and return a random `(key, value)` pair."""
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
except IndexError:
|
|
360
|
+
keys = self.__keys
|
|
361
|
+
if not keys:
|
|
363
362
|
raise KeyError("%s is empty" % type(self).__name__) from None
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
key = self.__choice(keys)
|
|
364
|
+
return (key, self.pop(key))
|
|
366
365
|
|
|
367
366
|
def clear(self):
|
|
368
367
|
Cache.clear(self)
|
|
@@ -496,6 +495,7 @@ class TTLCache(_TimedCache):
|
|
|
496
495
|
|
|
497
496
|
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
|
|
498
497
|
with self.timer as time:
|
|
498
|
+
expires = time + self.__ttl
|
|
499
499
|
self.expire(time)
|
|
500
500
|
cache_setitem(self, key, value)
|
|
501
501
|
try:
|
|
@@ -504,7 +504,7 @@ class TTLCache(_TimedCache):
|
|
|
504
504
|
self.__links[key] = link = TTLCache._Link(key)
|
|
505
505
|
else:
|
|
506
506
|
link.unlink()
|
|
507
|
-
link.expires =
|
|
507
|
+
link.expires = expires
|
|
508
508
|
link.next = root = self.__root
|
|
509
509
|
link.prev = prev = root.prev
|
|
510
510
|
prev.next = root.prev = link
|
|
@@ -191,6 +191,17 @@ def cached(
|
|
|
191
191
|
info: Literal[True],
|
|
192
192
|
) -> Callable[[Callable[_P, _R]], _cached_wrapper_info[_P, _R]]: ...
|
|
193
193
|
|
|
194
|
+
@type_check_only
|
|
195
|
+
class _cachedmethod_unbound(Generic[_P, _R]):
|
|
196
|
+
__wrapped__: Callable[Concatenate[Any, _P], _R]
|
|
197
|
+
__name__: str
|
|
198
|
+
__doc__: str | None
|
|
199
|
+
@overload
|
|
200
|
+
def __call__(self, obj: Any, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
|
201
|
+
# deprecated @classmethod use
|
|
202
|
+
@overload
|
|
203
|
+
def __call__(self, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
|
204
|
+
|
|
194
205
|
@type_check_only
|
|
195
206
|
class _cachedmethod_wrapper(Generic[_P, _R]):
|
|
196
207
|
__wrapped__: Callable[Concatenate[Any, _P], _R]
|
|
@@ -201,6 +212,11 @@ class _cachedmethod_wrapper(Generic[_P, _R]):
|
|
|
201
212
|
cache_lock: AbstractContextManager[Any] | None = None
|
|
202
213
|
cache_condition: _AbstractCondition | None = None
|
|
203
214
|
def __set_name__(self, owner: type, name: str) -> None: ...
|
|
215
|
+
@overload
|
|
216
|
+
def __get__(
|
|
217
|
+
self, obj: None, objtype: type | None = None
|
|
218
|
+
) -> _cachedmethod_unbound[_P, _R]: ...
|
|
219
|
+
@overload
|
|
204
220
|
def __get__(
|
|
205
221
|
self, obj: Any, objtype: type | None = None
|
|
206
222
|
) -> _cachedmethod_wrapper[_P, _R]: ...
|
|
@@ -209,6 +225,11 @@ class _cachedmethod_wrapper(Generic[_P, _R]):
|
|
|
209
225
|
|
|
210
226
|
@type_check_only
|
|
211
227
|
class _cachedmethod_wrapper_info(_cachedmethod_wrapper[_P, _R]):
|
|
228
|
+
@overload
|
|
229
|
+
def __get__(
|
|
230
|
+
self, obj: None, objtype: type | None = None
|
|
231
|
+
) -> _cachedmethod_unbound[_P, _R]: ...
|
|
232
|
+
@overload
|
|
212
233
|
def __get__(
|
|
213
234
|
self, obj: Any, objtype: type | None = None
|
|
214
235
|
) -> _cachedmethod_wrapper_info[_P, _R]: ...
|
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
"""Method decorator helpers."""
|
|
2
2
|
|
|
3
|
+
# pyright: reportOptionalContextManager=false, reportOptionalMemberAccess=false
|
|
4
|
+
|
|
3
5
|
__all__ = ()
|
|
4
6
|
|
|
5
7
|
import functools
|
|
6
|
-
import warnings
|
|
7
8
|
import weakref
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
def _warn_classmethod(stacklevel):
|
|
11
|
-
warnings.warn(
|
|
12
|
-
"decorating class methods with @cachedmethod is deprecated",
|
|
13
|
-
DeprecationWarning,
|
|
14
|
-
stacklevel=stacklevel,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _warn_instance_dict(msg, stacklevel):
|
|
19
|
-
warnings.warn(
|
|
20
|
-
msg,
|
|
21
|
-
DeprecationWarning,
|
|
22
|
-
stacklevel=stacklevel,
|
|
23
|
-
)
|
|
10
|
+
from ._descriptor import _ClassMethodDescriptor, _MethodDescriptor, _warn_classmethod
|
|
24
11
|
|
|
25
12
|
|
|
26
13
|
def _none(_):
|
|
@@ -63,82 +50,6 @@ class _WrapperBase:
|
|
|
63
50
|
return self.__cond(self._obj)
|
|
64
51
|
|
|
65
52
|
|
|
66
|
-
class _DescriptorBase:
|
|
67
|
-
"""Descriptor base class implementing the basic descriptor protocol."""
|
|
68
|
-
|
|
69
|
-
def __init__(self, deprecated=False):
|
|
70
|
-
self.__attrname = None
|
|
71
|
-
self.__deprecated = deprecated
|
|
72
|
-
|
|
73
|
-
def __set_name__(self, owner, name):
|
|
74
|
-
if self.__attrname is None:
|
|
75
|
-
self.__attrname = name
|
|
76
|
-
elif name != self.__attrname:
|
|
77
|
-
raise TypeError(
|
|
78
|
-
"Cannot assign the same @cachedmethod to two different names "
|
|
79
|
-
f"({self.__attrname!r} and {name!r})."
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
def __get__(self, obj, objtype=None):
|
|
83
|
-
wrapper = self.Wrapper(obj) # type: ignore
|
|
84
|
-
if obj is None:
|
|
85
|
-
# Return the wrapper itself without modification when accessed
|
|
86
|
-
# through the class to support class-level introspection, such
|
|
87
|
-
# as for mocking with autospec=True in unittest.mock.
|
|
88
|
-
pass
|
|
89
|
-
elif self.__attrname is not None:
|
|
90
|
-
# replace descriptor instance with wrapper in instance dict
|
|
91
|
-
try:
|
|
92
|
-
# In case of a race condition where another thread already replaced
|
|
93
|
-
# the descriptor, prefer the initial wrapper.
|
|
94
|
-
wrapper = obj.__dict__.setdefault(self.__attrname, wrapper)
|
|
95
|
-
except AttributeError:
|
|
96
|
-
# not all objects have __dict__ (e.g. class defines slots)
|
|
97
|
-
msg = (
|
|
98
|
-
f"No '__dict__' attribute on {type(obj).__name__!r} "
|
|
99
|
-
f"instance to cache {self.__attrname!r} property."
|
|
100
|
-
)
|
|
101
|
-
if self.__deprecated:
|
|
102
|
-
_warn_instance_dict(msg, 3)
|
|
103
|
-
else:
|
|
104
|
-
raise TypeError(msg) from None
|
|
105
|
-
except TypeError:
|
|
106
|
-
msg = (
|
|
107
|
-
f"The '__dict__' attribute on {type(obj).__name__!r} "
|
|
108
|
-
f"instance does not support item assignment for "
|
|
109
|
-
f"caching {self.__attrname!r} property."
|
|
110
|
-
)
|
|
111
|
-
if self.__deprecated:
|
|
112
|
-
_warn_instance_dict(msg, 3)
|
|
113
|
-
else:
|
|
114
|
-
raise TypeError(msg) from None
|
|
115
|
-
elif self.__deprecated:
|
|
116
|
-
pass # deprecated @classmethod, warning already raised elsewhere
|
|
117
|
-
else:
|
|
118
|
-
msg = "Cannot use @cachedmethod instance without calling __set_name__ on it"
|
|
119
|
-
raise TypeError(msg) from None
|
|
120
|
-
return wrapper
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
class _DeprecatedDescriptorBase(_DescriptorBase):
|
|
124
|
-
"""Descriptor base class supporting deprecated @classmethod use."""
|
|
125
|
-
|
|
126
|
-
def __init__(self, wrapper, cache_clear):
|
|
127
|
-
super().__init__(deprecated=True)
|
|
128
|
-
self.__wrapper = wrapper
|
|
129
|
-
self.__cache_clear = cache_clear
|
|
130
|
-
|
|
131
|
-
# called for @classmethod with Python >= 3.13
|
|
132
|
-
def __call__(self, *args, **kwargs):
|
|
133
|
-
_warn_classmethod(stacklevel=3)
|
|
134
|
-
return self.__wrapper(*args, **kwargs)
|
|
135
|
-
|
|
136
|
-
# backward-compatible @classmethod handling with Python >= 3.13
|
|
137
|
-
def cache_clear(self, objtype):
|
|
138
|
-
_warn_classmethod(stacklevel=3)
|
|
139
|
-
return self.__cache_clear(objtype)
|
|
140
|
-
|
|
141
|
-
|
|
142
53
|
# At least for now, the implementation prefers clarity and performance
|
|
143
54
|
# over ease of maintenance, thus providing separate descriptors for
|
|
144
55
|
# all valid combinations of decorator parameters lock, condition and
|
|
@@ -146,7 +57,7 @@ class _DeprecatedDescriptorBase(_DescriptorBase):
|
|
|
146
57
|
|
|
147
58
|
|
|
148
59
|
def _condition_info(method, cache, key, lock, cond, info):
|
|
149
|
-
class Descriptor(
|
|
60
|
+
class Descriptor(_MethodDescriptor):
|
|
150
61
|
class Wrapper(_WrapperBase):
|
|
151
62
|
def __init__(self, obj):
|
|
152
63
|
super().__init__(obj, method, cache, key, lock, cond)
|
|
@@ -194,7 +105,7 @@ def _condition_info(method, cache, key, lock, cond, info):
|
|
|
194
105
|
|
|
195
106
|
|
|
196
107
|
def _locked_info(method, cache, key, lock, info):
|
|
197
|
-
class Descriptor(
|
|
108
|
+
class Descriptor(_MethodDescriptor):
|
|
198
109
|
class Wrapper(_WrapperBase):
|
|
199
110
|
def __init__(self, obj):
|
|
200
111
|
super().__init__(obj, method, cache, key, lock)
|
|
@@ -234,7 +145,7 @@ def _locked_info(method, cache, key, lock, info):
|
|
|
234
145
|
|
|
235
146
|
|
|
236
147
|
def _unlocked_info(method, cache, key, info):
|
|
237
|
-
class Descriptor(
|
|
148
|
+
class Descriptor(_MethodDescriptor):
|
|
238
149
|
class Wrapper(_WrapperBase):
|
|
239
150
|
def __init__(self, obj):
|
|
240
151
|
super().__init__(obj, method, cache, key)
|
|
@@ -301,7 +212,7 @@ def _condition(method, cache, key, lock, cond):
|
|
|
301
212
|
p = pending.setdefault(self, set())
|
|
302
213
|
return wrapper(self, p, *args, **kwargs)
|
|
303
214
|
|
|
304
|
-
class Descriptor(
|
|
215
|
+
class Descriptor(_ClassMethodDescriptor):
|
|
305
216
|
class Wrapper(_WrapperBase):
|
|
306
217
|
def __init__(self, obj):
|
|
307
218
|
super().__init__(obj, method, cache, key, lock, cond)
|
|
@@ -341,7 +252,7 @@ def _locked(method, cache, key, lock):
|
|
|
341
252
|
with lock(self):
|
|
342
253
|
c.clear()
|
|
343
254
|
|
|
344
|
-
class Descriptor(
|
|
255
|
+
class Descriptor(_ClassMethodDescriptor):
|
|
345
256
|
class Wrapper(_WrapperBase):
|
|
346
257
|
def __init__(self, obj):
|
|
347
258
|
super().__init__(obj, method, cache, key, lock)
|
|
@@ -375,7 +286,7 @@ def _unlocked(method, cache, key):
|
|
|
375
286
|
c = cache(self)
|
|
376
287
|
c.clear()
|
|
377
288
|
|
|
378
|
-
class Descriptor(
|
|
289
|
+
class Descriptor(_ClassMethodDescriptor):
|
|
379
290
|
class Wrapper(_WrapperBase):
|
|
380
291
|
def __init__(self, obj):
|
|
381
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)
|
|
@@ -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
|
|
|
@@ -380,6 +380,27 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
|
|
|
380
380
|
self.assertEqual(wrapper.cache_info(), (0, 1, 0, 0))
|
|
381
381
|
self.assertEqual(lock.count, 5)
|
|
382
382
|
|
|
383
|
+
def test_decorator_method(self):
|
|
384
|
+
"""Though it is not encouraged, like functools.lru_cache, the
|
|
385
|
+
@cached decorator may be used on methods, too.
|
|
386
|
+
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
class Cached:
|
|
390
|
+
@cachetools.cached(self.cache(2), key=cachetools.keys.methodkey, info=True)
|
|
391
|
+
def get(self, value):
|
|
392
|
+
return value
|
|
393
|
+
|
|
394
|
+
cached = Cached()
|
|
395
|
+
self.assertEqual(cached.get(1), 1) # type: ignore
|
|
396
|
+
self.assertEqual(cached.get(1), 1) # type: ignore
|
|
397
|
+
self.assertEqual(Cached.get(cached, 1), 1)
|
|
398
|
+
self.assertEqual(cached.get.cache_info(), (2, 1, 2, 1))
|
|
399
|
+
self.assertEqual(Cached.get.cache_info(), (2, 1, 2, 1))
|
|
400
|
+
cached.get.cache_clear()
|
|
401
|
+
self.assertEqual(cached.get.cache_info(), (0, 0, 2, 0))
|
|
402
|
+
self.assertEqual(Cached.get.cache_info(), (0, 0, 2, 0))
|
|
403
|
+
|
|
383
404
|
|
|
384
405
|
class DictWrapperTest(unittest.TestCase, DecoratorTestMixin):
|
|
385
406
|
def cache(self, minsize):
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
# TODO: type stub overloads for unbound methods?
|
|
2
|
+
# pyright: reportCallIssue=false
|
|
3
|
+
|
|
1
4
|
import unittest
|
|
2
|
-
import unittest.mock
|
|
3
5
|
import warnings
|
|
4
6
|
|
|
5
7
|
from cachetools import Cache, cachedmethod, keys
|
|
6
8
|
|
|
7
|
-
from . import CountedCondition, CountedLock,
|
|
9
|
+
from . import CountedCondition, CountedLock, SimplifiedTestCaseProtocol
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class Cached:
|
|
@@ -77,7 +79,7 @@ class Unhashable(Cached):
|
|
|
77
79
|
raise TypeError("unhashable type")
|
|
78
80
|
|
|
79
81
|
|
|
80
|
-
class MethodDecoratorTestMixin(
|
|
82
|
+
class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
|
|
81
83
|
def cache(self, _minsize, **_kwargs):
|
|
82
84
|
raise NotImplementedError
|
|
83
85
|
|
|
@@ -632,6 +634,69 @@ class CacheMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
|
|
|
632
634
|
self.assertEqual(cached.get(2), 2)
|
|
633
635
|
self.assertNotIn(2, cache.values())
|
|
634
636
|
|
|
637
|
+
def test_undecorated_override(self):
|
|
638
|
+
class Child(Cached):
|
|
639
|
+
def get_info(self, value): # type: ignore[reportIncompatibleVariableOverride]
|
|
640
|
+
return super().get_info(value) + 1
|
|
641
|
+
|
|
642
|
+
cache = self.cache(2)
|
|
643
|
+
cached = Child(cache)
|
|
644
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
645
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
646
|
+
self.assertEqual(super(Child, cached).get_info(0), 0)
|
|
647
|
+
self.assertEqual(cached.get_info(1), 2)
|
|
648
|
+
self.assertEqual(super(Child, cached).get_info(1), 1)
|
|
649
|
+
self.assertNotIn("get_info", vars(cached))
|
|
650
|
+
self.assertEqual(super(Child, cached).get_info.cache_info(), (3, 2, 2, 2))
|
|
651
|
+
|
|
652
|
+
def test_decorated_override(self):
|
|
653
|
+
class Child(Cached):
|
|
654
|
+
def __init__(self, base_cache, child_cache):
|
|
655
|
+
super().__init__(base_cache)
|
|
656
|
+
self.child_cache = child_cache
|
|
657
|
+
|
|
658
|
+
@cachedmethod(lambda self: self.child_cache, info=True)
|
|
659
|
+
def get_info(self, value):
|
|
660
|
+
return super().get_info(value) + 1
|
|
661
|
+
|
|
662
|
+
cached = Child(self.cache(2), self.cache(2))
|
|
663
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
664
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
665
|
+
self.assertEqual(super(Child, cached).get_info(0), 0)
|
|
666
|
+
self.assertEqual(cached.get_info(1), 2)
|
|
667
|
+
self.assertEqual(super(Child, cached).get_info(1), 1)
|
|
668
|
+
self.assertIn("get_info", vars(cached))
|
|
669
|
+
self.assertEqual(cached.get_info.cache_info(), (1, 2, 2, 2))
|
|
670
|
+
self.assertEqual(super(Child, cached).get_info.cache_info(), (2, 2, 2, 2))
|
|
671
|
+
cached.get_info.cache_clear()
|
|
672
|
+
self.assertEqual(cached.get_info.cache_info(), (0, 0, 2, 0))
|
|
673
|
+
self.assertEqual(super(Child, cached).get_info.cache_info(), (2, 2, 2, 2))
|
|
674
|
+
self.assertEqual(cached.get_info(0), 1)
|
|
675
|
+
self.assertEqual(cached.get_info.cache_info(), (0, 1, 2, 1))
|
|
676
|
+
self.assertEqual(super(Child, cached).get_info.cache_info(), (3, 2, 2, 2))
|
|
677
|
+
super(Child, cached).get_info.cache_clear()
|
|
678
|
+
self.assertEqual(cached.get_info.cache_info(), (0, 1, 2, 1))
|
|
679
|
+
self.assertEqual(super(Child, cached).get_info.cache_info(), (0, 0, 2, 0))
|
|
680
|
+
|
|
681
|
+
def test_multilevel_override(self):
|
|
682
|
+
class Child(Cached):
|
|
683
|
+
@cachedmethod(lambda self: self.cache, key=keys.typedmethodkey)
|
|
684
|
+
def get_typed(self, value):
|
|
685
|
+
return super().get_typed(float(value)) + 1
|
|
686
|
+
|
|
687
|
+
class GrandChild(Child):
|
|
688
|
+
@cachedmethod(lambda self: self.cache, key=keys.typedmethodkey, info=True)
|
|
689
|
+
def get_typed(self, value):
|
|
690
|
+
return super().get_typed(int(value)) + 1
|
|
691
|
+
|
|
692
|
+
cache = self.cache(5) # shared cache, but different key types
|
|
693
|
+
cached = GrandChild(cache)
|
|
694
|
+
self.assertEqual(cached.get_typed("0"), 2)
|
|
695
|
+
self.assertEqual(cached.get_typed("0"), 2)
|
|
696
|
+
self.assertEqual(cached.get_typed.cache_info(), (1, 1, 5, 3))
|
|
697
|
+
self.assertEqual(super(GrandChild, cached).get_typed(0), 1)
|
|
698
|
+
self.assertEqual(super(Child, cached).get_typed(0.0), 0)
|
|
699
|
+
|
|
635
700
|
|
|
636
701
|
class DictMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
|
|
637
702
|
def cache(self, minsize, **_kwargs):
|
|
@@ -683,8 +748,51 @@ class NoneMethodTest(unittest.TestCase):
|
|
|
683
748
|
wrapper.cache_info()
|
|
684
749
|
|
|
685
750
|
|
|
686
|
-
class
|
|
751
|
+
class UnboundMethodTest(unittest.TestCase):
|
|
752
|
+
def test_class_call(self):
|
|
753
|
+
cached = Cached(Cache(2))
|
|
754
|
+
self.assertEqual(Cached.get(cached, 1), 0)
|
|
755
|
+
self.assertEqual(cached.get(1), 0)
|
|
756
|
+
self.assertEqual(Cached.get(cached, 2), 1)
|
|
757
|
+
self.assertEqual(cached.get(2), 1)
|
|
758
|
+
self.assertEqual(cached.count, 2)
|
|
759
|
+
other = Cached(Cache(2), count=10)
|
|
760
|
+
self.assertEqual(Cached.get(other, 1), 10)
|
|
761
|
+
self.assertEqual(cached.get(1), 0)
|
|
762
|
+
|
|
763
|
+
def test_class_call_info(self):
|
|
764
|
+
cached = Cached(Cache(2))
|
|
765
|
+
self.assertEqual(Cached.get_info(cached, 1), 0)
|
|
766
|
+
self.assertEqual(cached.get_info(1), 0)
|
|
767
|
+
self.assertEqual(Cached.get_info(cached, 2), 1)
|
|
768
|
+
self.assertEqual(cached.get_info(2), 1)
|
|
769
|
+
self.assertEqual(cached.count, 2)
|
|
770
|
+
self.assertEqual(cached.get_info.cache_info(), (2, 2, 2, 2))
|
|
771
|
+
other = Cached(Cache(2), count=10)
|
|
772
|
+
self.assertEqual(Cached.get_info(other, 1), 10)
|
|
773
|
+
self.assertEqual(cached.get_info(1), 0)
|
|
774
|
+
|
|
775
|
+
def test_base_class_call(self):
|
|
776
|
+
cached = Unhashable(Cache(2))
|
|
777
|
+
self.assertEqual(Cached.get(cached, 1), 0)
|
|
778
|
+
self.assertEqual(cached.get(1), 0)
|
|
779
|
+
self.assertEqual(cached.count, 1)
|
|
780
|
+
|
|
781
|
+
def test_class_access_identity(self):
|
|
782
|
+
self.assertIs(Cached.get, Cached.get)
|
|
783
|
+
self.assertIs(Cached.get_info, Cached.get_info)
|
|
784
|
+
self.assertIs(Unhashable.get, Cached.get)
|
|
785
|
+
self.assertIsNot(Cached.get, Cached.get_info)
|
|
786
|
+
|
|
787
|
+
def test_wrapped_method(self):
|
|
788
|
+
cached = Cached(Cache(2))
|
|
789
|
+
self.assertIs(Cached.get.__wrapped__, cached.get.__wrapped__)
|
|
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,6 +109,29 @@ 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
|
+
|
|
112
135
|
|
|
113
136
|
class FIFODecoratorTest(unittest.TestCase, DecoratorTestMixin):
|
|
114
137
|
DECORATOR = staticmethod(cachetools.func.fifo_cache)
|
|
@@ -86,3 +86,18 @@ class RRCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
86
86
|
def test_rr_default_choice(self):
|
|
87
87
|
cache = RRCache[int, int](maxsize=2)
|
|
88
88
|
self.assertIs(cache.choice, random.choice)
|
|
89
|
+
|
|
90
|
+
def test_rr_popitem_empty_custom_choice(self):
|
|
91
|
+
# a custom `choice` function only has to handle non-empty
|
|
92
|
+
# sequences, so an empty cache has to be detected beforehand
|
|
93
|
+
for choice in (min, max, lambda seq: next(iter(seq))):
|
|
94
|
+
cache = RRCache[int, int](maxsize=2, choice=choice)
|
|
95
|
+
|
|
96
|
+
with self.assertRaises(KeyError):
|
|
97
|
+
cache.popitem()
|
|
98
|
+
|
|
99
|
+
cache[1] = 1
|
|
100
|
+
self.assertEqual((1, 1), cache.popitem())
|
|
101
|
+
|
|
102
|
+
with self.assertRaises(KeyError):
|
|
103
|
+
cache.popitem()
|
|
@@ -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
|