cachetools 7.2.1__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.1 → cachetools-7.2.2}/CHANGELOG.rst +15 -0
- {cachetools-7.2.1/src/cachetools.egg-info → cachetools-7.2.2}/PKG-INFO +1 -1
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/__init__.py +3 -2
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/__init__.pyi +21 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/_cachedmethod.py +7 -98
- cachetools-7.2.2/src/cachetools/_descriptor.py +126 -0
- {cachetools-7.2.1 → cachetools-7.2.2/src/cachetools.egg-info}/PKG-INFO +1 -1
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools.egg-info/SOURCES.txt +1 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/__init__.py +2 -2
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_cached.py +23 -2
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_cachedmethod.py +112 -4
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_func.py +25 -2
- cachetools-7.2.2/tests/test_threading.py +103 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_ttl.py +16 -0
- cachetools-7.2.1/tests/test_threading.py +0 -122
- {cachetools-7.2.1 → cachetools-7.2.2}/LICENSE +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/MANIFEST.in +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/README.rst +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/docs/conf.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/docs/index.rst +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/pyproject.toml +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/setup.cfg +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/_cached.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/func.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/func.pyi +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/keys.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/keys.pyi +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/py.typed +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools.egg-info/dependency_links.txt +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools.egg-info/top_level.txt +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_cache.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_classmethod.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_fifo.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_keys.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_lfu.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_lru.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_rr.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_tlru.py +0 -0
- {cachetools-7.2.1 → cachetools-7.2.2}/tox.ini +0 -0
|
@@ -1,3 +1,18 @@
|
|
|
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
|
+
|
|
1
16
|
v7.2.1 (2026-10-05)
|
|
2
17
|
===================
|
|
3
18
|
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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]: ...
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|