cachetools 7.2.7__tar.gz → 7.3.0__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.7 → cachetools-7.3.0}/CHANGELOG.rst +21 -0
- {cachetools-7.2.7/src/cachetools.egg-info → cachetools-7.3.0}/PKG-INFO +1 -1
- {cachetools-7.2.7 → cachetools-7.3.0}/docs/conf.py +0 -1
- {cachetools-7.2.7 → cachetools-7.3.0}/docs/index.rst +22 -3
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/__init__.py +7 -7
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/__init__.pyi +14 -13
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/_cachedmethod.py +10 -10
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/_descriptor.py +1 -1
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/keys.py +2 -2
- {cachetools-7.2.7 → cachetools-7.3.0/src/cachetools.egg-info}/PKG-INFO +1 -1
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_cached.py +15 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_cachedmethod.py +8 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_classmethod.py +18 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_func.py +65 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_keys.py +10 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_lfu.py +41 -13
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_threading.py +86 -10
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_tlru.py +22 -2
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_ttl.py +17 -2
- {cachetools-7.2.7 → cachetools-7.3.0}/LICENSE +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/MANIFEST.in +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/README.rst +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/pyproject.toml +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/setup.cfg +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/_cached.py +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/func.py +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/func.pyi +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/keys.pyi +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/py.typed +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools.egg-info/SOURCES.txt +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools.egg-info/dependency_links.txt +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools.egg-info/top_level.txt +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/__init__.py +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_cache.py +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_fifo.py +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_lru.py +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_rr.py +0 -0
- {cachetools-7.2.7 → cachetools-7.3.0}/tox.ini +0 -0
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
v7.3.0 (2026-11-05)
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
- Use ``OrderedDict`` for defined and faster ``LFUCache`` eviction.
|
|
5
|
+
|
|
6
|
+
- Various unit test improvements.
|
|
7
|
+
|
|
8
|
+
- Minor type stub improvements.
|
|
9
|
+
|
|
10
|
+
- Minor documentation improvements.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
v7.2.8 (2026-10-31)
|
|
14
|
+
===================
|
|
15
|
+
|
|
16
|
+
- Fix type stubs for ``TTLCache`` and ``TLRUCache`` with default
|
|
17
|
+
``timer``.
|
|
18
|
+
|
|
19
|
+
- Minor documentation and unit test improvements.
|
|
20
|
+
|
|
21
|
+
|
|
1
22
|
v7.2.7 (2026-10-28)
|
|
2
23
|
===================
|
|
3
24
|
|
|
@@ -92,10 +92,17 @@ inserted into the cache.
|
|
|
92
92
|
:members: popitem
|
|
93
93
|
|
|
94
94
|
This class counts how often an item is retrieved, and discards the
|
|
95
|
-
items used least often to make space when necessary.
|
|
95
|
+
items used least often to make space when necessary. In case of a
|
|
96
|
+
tie, the least recently used of these items is discarded.
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
as a use of the item
|
|
98
|
+
Inserting or updating a cache item via :meth:`LFUCache.__setitem__`
|
|
99
|
+
also counts as a use of the item; an update will increment the
|
|
100
|
+
item's use count.
|
|
101
|
+
|
|
102
|
+
.. versionchanged:: 7.3
|
|
103
|
+
|
|
104
|
+
Switched to discarding the least recently used item in case of
|
|
105
|
+
a tie. Previously, an arbitrary item was discarded.
|
|
99
106
|
|
|
100
107
|
.. autoclass:: LRUCache(maxsize, getsizeof=None)
|
|
101
108
|
:members: popitem
|
|
@@ -356,6 +363,16 @@ often called with the same arguments:
|
|
|
356
363
|
implement the `context manager`_ protocol, and will also be used to
|
|
357
364
|
guard access to the cache.
|
|
358
365
|
|
|
366
|
+
.. note::
|
|
367
|
+
|
|
368
|
+
Even though `lock` and `condition` can be passed separately,
|
|
369
|
+
the condition variable must be associated with the lock that
|
|
370
|
+
guards the cache. In practice, when using
|
|
371
|
+
:class:`threading.Lock` and :class:`threading.Condition`, the
|
|
372
|
+
condition variable must be created with the corresponding lock,
|
|
373
|
+
e.g. ``condition=threading.Condition(lock)``; an unrelated
|
|
374
|
+
:class:`threading.Condition` will raise :exc:`RuntimeError`.
|
|
375
|
+
|
|
359
376
|
The decorator's `cache`, `key`, `lock` and `condition` parameters
|
|
360
377
|
are also available as :attr:`cache`, :attr:`cache_key`,
|
|
361
378
|
:attr:`cache_lock` and :attr:`cache_condition` attributes of the
|
|
@@ -548,6 +565,8 @@ often called with the same arguments:
|
|
|
548
565
|
`lock(self)` will only guard access to the cache itself. It is
|
|
549
566
|
the user's responsibility to handle concurrent calls to the
|
|
550
567
|
underlying wrapped method in a multithreaded environment.
|
|
568
|
+
Likewise, a condition variable returned by `condition(self)`
|
|
569
|
+
must be associated with a lock returned by `lock(self)`.
|
|
551
570
|
|
|
552
571
|
Similarly to the :func:`cached` function decorator, the `cache`,
|
|
553
572
|
`key`, `lock` and `condition` parameters are also available as
|
|
@@ -12,7 +12,7 @@ __all__ = (
|
|
|
12
12
|
"cachedmethod",
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
__version__ = "7.
|
|
15
|
+
__version__ = "7.3.0"
|
|
16
16
|
|
|
17
17
|
import collections
|
|
18
18
|
import collections.abc
|
|
@@ -212,7 +212,7 @@ class LFUCache(Cache):
|
|
|
212
212
|
|
|
213
213
|
def __init__(self, count):
|
|
214
214
|
self.count = count
|
|
215
|
-
self.keys =
|
|
215
|
+
self.keys = collections.OrderedDict()
|
|
216
216
|
|
|
217
217
|
def unlink(self):
|
|
218
218
|
next = self.next
|
|
@@ -244,13 +244,13 @@ class LFUCache(Cache):
|
|
|
244
244
|
link.next = root.next
|
|
245
245
|
root.next = link.next.prev = link
|
|
246
246
|
link.prev = root
|
|
247
|
-
link.keys
|
|
247
|
+
link.keys[key] = None
|
|
248
248
|
self.__links[key] = link
|
|
249
249
|
|
|
250
250
|
def __delitem__(self, key, cache_delitem=Cache.__delitem__):
|
|
251
251
|
cache_delitem(self, key)
|
|
252
252
|
link = self.__links.pop(key)
|
|
253
|
-
link.keys
|
|
253
|
+
del link.keys[key]
|
|
254
254
|
if not link.keys:
|
|
255
255
|
link.unlink()
|
|
256
256
|
|
|
@@ -260,7 +260,7 @@ class LFUCache(Cache):
|
|
|
260
260
|
curr = root.next
|
|
261
261
|
if curr is root:
|
|
262
262
|
raise KeyError("%s is empty" % type(self).__name__) from None
|
|
263
|
-
key = next(iter(curr.keys)) # remove
|
|
263
|
+
key = next(iter(curr.keys)) # remove the "oldest" element
|
|
264
264
|
value = Cache.__getitem__(self, key)
|
|
265
265
|
del self[key]
|
|
266
266
|
return (key, value)
|
|
@@ -283,8 +283,8 @@ class LFUCache(Cache):
|
|
|
283
283
|
curr.next = link.next
|
|
284
284
|
link.next = curr.next.prev = curr
|
|
285
285
|
curr.prev = link
|
|
286
|
-
curr.keys
|
|
287
|
-
link.keys
|
|
286
|
+
curr.keys[key] = None
|
|
287
|
+
del link.keys[key]
|
|
288
288
|
if not link.keys:
|
|
289
289
|
link.unlink()
|
|
290
290
|
self.__links[key] = curr
|
|
@@ -36,8 +36,6 @@ _T = TypeVar("_T")
|
|
|
36
36
|
_KT = TypeVar("_KT")
|
|
37
37
|
_VT = TypeVar("_VT")
|
|
38
38
|
_TT = TypeVar("_TT", default=float)
|
|
39
|
-
_KT2 = TypeVar("_KT2")
|
|
40
|
-
_VT2 = TypeVar("_VT2")
|
|
41
39
|
|
|
42
40
|
class Cache(MutableMapping[_KT, _VT]):
|
|
43
41
|
def __init__(
|
|
@@ -53,7 +51,12 @@ class Cache(MutableMapping[_KT, _VT]):
|
|
|
53
51
|
def pop(self, key: _KT) -> _VT: ...
|
|
54
52
|
@overload
|
|
55
53
|
def pop(self, key: _KT, default: _VT | _T) -> _VT | _T: ...
|
|
56
|
-
|
|
54
|
+
@overload
|
|
55
|
+
def setdefault(
|
|
56
|
+
self: Cache[_KT, _T | None], key: _KT, default: None = None
|
|
57
|
+
) -> _T | None: ...
|
|
58
|
+
@overload
|
|
59
|
+
def setdefault(self, key: _KT, default: _VT) -> _VT: ...
|
|
57
60
|
@property
|
|
58
61
|
def maxsize(self) -> float: ...
|
|
59
62
|
@property
|
|
@@ -94,19 +97,19 @@ class _TimedCache(Cache[_KT, _VT], Generic[_KT, _VT, _TT]):
|
|
|
94
97
|
def timer(self) -> _Timer[_TT]: ...
|
|
95
98
|
|
|
96
99
|
class TTLCache(_TimedCache[_KT, _VT, _TT]):
|
|
97
|
-
@overload
|
|
100
|
+
@overload # without timer, _TT takes its default
|
|
98
101
|
def __init__(
|
|
99
|
-
self
|
|
102
|
+
self,
|
|
100
103
|
maxsize: float,
|
|
101
104
|
ttl: float,
|
|
102
105
|
*,
|
|
103
|
-
getsizeof: Callable[[
|
|
106
|
+
getsizeof: Callable[[_VT], float] | None = None,
|
|
104
107
|
) -> None: ...
|
|
105
108
|
@overload
|
|
106
109
|
def __init__(
|
|
107
110
|
self,
|
|
108
111
|
maxsize: float,
|
|
109
|
-
ttl: Any, #
|
|
112
|
+
ttl: Any, # must be "addable" to _TT
|
|
110
113
|
timer: Callable[[], _TT],
|
|
111
114
|
getsizeof: Callable[[_VT], float] | None = None,
|
|
112
115
|
) -> None: ...
|
|
@@ -115,13 +118,13 @@ class TTLCache(_TimedCache[_KT, _VT, _TT]):
|
|
|
115
118
|
def expire(self, time: _TT | None = None) -> list[tuple[_KT, _VT]]: ...
|
|
116
119
|
|
|
117
120
|
class TLRUCache(_TimedCache[_KT, _VT, _TT]):
|
|
118
|
-
@overload
|
|
121
|
+
@overload # without timer, _TT takes its default
|
|
119
122
|
def __init__(
|
|
120
|
-
self
|
|
123
|
+
self,
|
|
121
124
|
maxsize: float,
|
|
122
|
-
ttu: Callable[[
|
|
125
|
+
ttu: Callable[[_KT, _VT, float], float],
|
|
123
126
|
*,
|
|
124
|
-
getsizeof: Callable[[
|
|
127
|
+
getsizeof: Callable[[_VT], float] | None = None,
|
|
125
128
|
) -> None: ...
|
|
126
129
|
@overload
|
|
127
130
|
def __init__(
|
|
@@ -143,8 +146,6 @@ class _CacheInfo(NamedTuple):
|
|
|
143
146
|
|
|
144
147
|
@type_check_only
|
|
145
148
|
class _AbstractCondition(AbstractContextManager[Any], Protocol):
|
|
146
|
-
# implementation and unit tests currently do not use plain wait()
|
|
147
|
-
# and notify(); these are reserved for future use
|
|
148
149
|
def wait(self, timeout: float | None = None) -> bool: ...
|
|
149
150
|
def wait_for(
|
|
150
151
|
self, predicate: Callable[[], _T], timeout: float | None = None
|
|
@@ -35,7 +35,7 @@ class _WrapperBase:
|
|
|
35
35
|
# wrappers
|
|
36
36
|
return (self.__descriptor.__get__, (self._obj,))
|
|
37
37
|
|
|
38
|
-
def __call__(self, *args, **kwargs):
|
|
38
|
+
def __call__(self, /, *args, **kwargs):
|
|
39
39
|
raise NotImplementedError() # pragma: no cover
|
|
40
40
|
|
|
41
41
|
def cache_clear(self):
|
|
@@ -72,7 +72,7 @@ def _condition_info(method, cache, key, lock, cond, info):
|
|
|
72
72
|
self.__hits = self.__misses = 0
|
|
73
73
|
self.__pending = set()
|
|
74
74
|
|
|
75
|
-
def __call__(self, *args, **kwargs):
|
|
75
|
+
def __call__(self, /, *args, **kwargs):
|
|
76
76
|
cache = self.cache
|
|
77
77
|
lock = self.cache_lock
|
|
78
78
|
cond = self.cache_condition
|
|
@@ -121,7 +121,7 @@ def _locked_info(method, cache, key, lock, info):
|
|
|
121
121
|
super().__init__(obj, descriptor, method, cache, key, lock)
|
|
122
122
|
self.__hits = self.__misses = 0
|
|
123
123
|
|
|
124
|
-
def __call__(self, *args, **kwargs):
|
|
124
|
+
def __call__(self, /, *args, **kwargs):
|
|
125
125
|
cache = self.cache
|
|
126
126
|
lock = self.cache_lock
|
|
127
127
|
key = self.cache_key(*args, **kwargs)
|
|
@@ -161,7 +161,7 @@ def _unlocked_info(method, cache, key, info):
|
|
|
161
161
|
super().__init__(obj, descriptor, method, cache, key)
|
|
162
162
|
self.__hits = self.__misses = 0
|
|
163
163
|
|
|
164
|
-
def __call__(self, *args, **kwargs):
|
|
164
|
+
def __call__(self, /, *args, **kwargs):
|
|
165
165
|
cache = self.cache
|
|
166
166
|
key = self.cache_key(*args, **kwargs)
|
|
167
167
|
try:
|
|
@@ -220,7 +220,7 @@ def _condition(method, cache, key, lock, cond):
|
|
|
220
220
|
with lock(self):
|
|
221
221
|
c.clear()
|
|
222
222
|
|
|
223
|
-
def classmethod_wrapper(self, *args, **kwargs):
|
|
223
|
+
def classmethod_wrapper(self, /, *args, **kwargs):
|
|
224
224
|
p = pending.setdefault(self, set())
|
|
225
225
|
return wrapper(self, p, *args, **kwargs)
|
|
226
226
|
|
|
@@ -230,7 +230,7 @@ def _condition(method, cache, key, lock, cond):
|
|
|
230
230
|
super().__init__(obj, descriptor, method, cache, key, lock, cond)
|
|
231
231
|
self.__pending = set()
|
|
232
232
|
|
|
233
|
-
def __call__(self, *args, **kwargs):
|
|
233
|
+
def __call__(self, /, *args, **kwargs):
|
|
234
234
|
return wrapper(self._obj, self.__pending, *args, **kwargs)
|
|
235
235
|
|
|
236
236
|
# objtype: backward-compatible @classmethod handling with Python < 3.13
|
|
@@ -241,7 +241,7 @@ def _condition(method, cache, key, lock, cond):
|
|
|
241
241
|
|
|
242
242
|
|
|
243
243
|
def _locked(method, cache, key, lock):
|
|
244
|
-
def wrapper(self, *args, **kwargs):
|
|
244
|
+
def wrapper(self, /, *args, **kwargs):
|
|
245
245
|
c = cache(self)
|
|
246
246
|
k = key(self, *args, **kwargs)
|
|
247
247
|
with lock(self):
|
|
@@ -269,7 +269,7 @@ def _locked(method, cache, key, lock):
|
|
|
269
269
|
def __init__(self, obj, descriptor):
|
|
270
270
|
super().__init__(obj, descriptor, method, cache, key, lock)
|
|
271
271
|
|
|
272
|
-
def __call__(self, *args, **kwargs):
|
|
272
|
+
def __call__(self, /, *args, **kwargs):
|
|
273
273
|
return wrapper(self._obj, *args, **kwargs)
|
|
274
274
|
|
|
275
275
|
# objtype: backward-compatible @classmethod handling with Python < 3.13
|
|
@@ -280,7 +280,7 @@ def _locked(method, cache, key, lock):
|
|
|
280
280
|
|
|
281
281
|
|
|
282
282
|
def _unlocked(method, cache, key):
|
|
283
|
-
def wrapper(self, *args, **kwargs):
|
|
283
|
+
def wrapper(self, /, *args, **kwargs):
|
|
284
284
|
c = cache(self)
|
|
285
285
|
k = key(self, *args, **kwargs)
|
|
286
286
|
try:
|
|
@@ -303,7 +303,7 @@ def _unlocked(method, cache, key):
|
|
|
303
303
|
def __init__(self, obj, descriptor):
|
|
304
304
|
super().__init__(obj, descriptor, method, cache, key)
|
|
305
305
|
|
|
306
|
-
def __call__(self, *args, **kwargs):
|
|
306
|
+
def __call__(self, /, *args, **kwargs):
|
|
307
307
|
return wrapper(self._obj, *args, **kwargs)
|
|
308
308
|
|
|
309
309
|
# objtype: backward-compatible @classmethod handling with Python < 3.13
|
|
@@ -178,7 +178,7 @@ class _ClassMethodDescriptor(_MethodDescriptor):
|
|
|
178
178
|
self.__cache_clear = cache_clear
|
|
179
179
|
|
|
180
180
|
# called for @classmethod with Python >= 3.13
|
|
181
|
-
def __call__(self, *args, **kwargs):
|
|
181
|
+
def __call__(self, /, *args, **kwargs):
|
|
182
182
|
_warn_classmethod(stacklevel=3)
|
|
183
183
|
return self.__wrapper(*args, **kwargs)
|
|
184
184
|
|
|
@@ -54,7 +54,7 @@ def hashkey(*args, **kwargs):
|
|
|
54
54
|
return _HashedTuple(args)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def methodkey(self, *args, **kwargs):
|
|
57
|
+
def methodkey(self, /, *args, **kwargs):
|
|
58
58
|
"""Return a cache key for use with cached methods."""
|
|
59
59
|
return hashkey(*args, **kwargs)
|
|
60
60
|
|
|
@@ -72,6 +72,6 @@ def typedkey(*args, **kwargs):
|
|
|
72
72
|
return key
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def typedmethodkey(self, *args, **kwargs):
|
|
75
|
+
def typedmethodkey(self, /, *args, **kwargs):
|
|
76
76
|
"""Return a typed cache key for use with cached methods."""
|
|
77
77
|
return typedkey(*args, **kwargs)
|
|
@@ -438,6 +438,21 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
|
|
|
438
438
|
self.assertEqual(cached.get_static(1), 1)
|
|
439
439
|
self.assertEqual(Cached.get_static(1), 1)
|
|
440
440
|
|
|
441
|
+
def test_autospec(self):
|
|
442
|
+
import unittest.mock
|
|
443
|
+
|
|
444
|
+
class Cached:
|
|
445
|
+
@cachetools.cached(self.cache(2))
|
|
446
|
+
def get(self, value):
|
|
447
|
+
return value
|
|
448
|
+
|
|
449
|
+
mock = unittest.mock.create_autospec(Cached, instance=True)
|
|
450
|
+
mock.get.return_value = 42
|
|
451
|
+
self.assertEqual(mock.get(1), 42)
|
|
452
|
+
mock.get.assert_called_with(1)
|
|
453
|
+
self.assertEqual(mock.get(value=1), 42)
|
|
454
|
+
mock.get.assert_called_with(value=1)
|
|
455
|
+
|
|
441
456
|
|
|
442
457
|
class DictWrapperTest(unittest.TestCase, DecoratorTestMixin):
|
|
443
458
|
def cache(self, minsize):
|
|
@@ -982,6 +982,10 @@ class NoneMethodTest(unittest.TestCase):
|
|
|
982
982
|
|
|
983
983
|
class UnboundMethodTest(unittest.TestCase):
|
|
984
984
|
def test_class_call(self):
|
|
985
|
+
for attr in ("cache", "cache_key", "cache_lock", "cache_condition"):
|
|
986
|
+
self.assertFalse(hasattr(Cached.get, attr))
|
|
987
|
+
for attr in ("cache_clear", "cache_info"):
|
|
988
|
+
self.assertFalse(hasattr(Cached.get, attr))
|
|
985
989
|
cached = Cached(Cache(2))
|
|
986
990
|
self.assertEqual(Cached.get(cached, 1), 0)
|
|
987
991
|
self.assertEqual(cached.get(1), 0)
|
|
@@ -993,6 +997,10 @@ class UnboundMethodTest(unittest.TestCase):
|
|
|
993
997
|
self.assertEqual(cached.get(1), 0)
|
|
994
998
|
|
|
995
999
|
def test_class_call_info(self):
|
|
1000
|
+
for attr in ("cache", "cache_key", "cache_lock", "cache_condition"):
|
|
1001
|
+
self.assertFalse(hasattr(Cached.get_info, attr))
|
|
1002
|
+
for attr in ("cache_clear", "cache_info"):
|
|
1003
|
+
self.assertFalse(hasattr(Cached.get_info, attr))
|
|
996
1004
|
cached = Cached(Cache(2))
|
|
997
1005
|
self.assertEqual(Cached.get_info(cached, 1), 0)
|
|
998
1006
|
self.assertEqual(cached.get_info(1), 0)
|
|
@@ -42,6 +42,12 @@ class Cached:
|
|
|
42
42
|
cls.count += 1
|
|
43
43
|
return cls.count
|
|
44
44
|
|
|
45
|
+
@classmethod
|
|
46
|
+
@cachedmethod(lambda cls: cls.cache)
|
|
47
|
+
def get_self(cls, self):
|
|
48
|
+
cls.count += 1
|
|
49
|
+
return cls.count
|
|
50
|
+
|
|
45
51
|
|
|
46
52
|
class CachedClassMethodTest(unittest.TestCase):
|
|
47
53
|
def test(self):
|
|
@@ -192,3 +198,15 @@ class CachedClassMethodTest(unittest.TestCase):
|
|
|
192
198
|
self.assertEqual(len(Cached.cache), 1)
|
|
193
199
|
Cached.get_condition.cache_clear(cached) # type: ignore
|
|
194
200
|
self.assertEqual(len(Cached.cache), 0)
|
|
201
|
+
|
|
202
|
+
def test_self_keyword(self):
|
|
203
|
+
# https://github.com/tkem/cachetools/issues/451
|
|
204
|
+
Cached.cache = LRUCache(2)
|
|
205
|
+
Cached.count = 0
|
|
206
|
+
cached = Cached()
|
|
207
|
+
|
|
208
|
+
with warnings.catch_warnings():
|
|
209
|
+
warnings.simplefilter("ignore")
|
|
210
|
+
self.assertEqual(Cached.get_self(self=0), 1)
|
|
211
|
+
self.assertEqual(cached.get_self(self=0), 1)
|
|
212
|
+
self.assertEqual(Cached.get_self(self=1), 2)
|
|
@@ -192,10 +192,75 @@ class LRUDecoratorTest(unittest.TestCase, DecoratorTestMixin):
|
|
|
192
192
|
class RRDecoratorTest(unittest.TestCase, DecoratorTestMixin):
|
|
193
193
|
DECORATOR = staticmethod(cachetools.func.rr_cache)
|
|
194
194
|
|
|
195
|
+
def test_rr_choice(self):
|
|
196
|
+
cached = cachetools.func.rr_cache(maxsize=2, choice=min)(lambda n: n)
|
|
197
|
+
self.assertEqual(cached.cache_parameters(), {"maxsize": 2, "typed": False})
|
|
198
|
+
self.assertEqual(cached(1), 1)
|
|
199
|
+
self.assertEqual(cached(2), 2)
|
|
200
|
+
self.assertEqual(cached.cache_info(), (0, 2, 2, 2))
|
|
201
|
+
# the smallest key, (1,), is evicted
|
|
202
|
+
self.assertEqual(cached(3), 3)
|
|
203
|
+
self.assertEqual(cached.cache_info(), (0, 3, 2, 2))
|
|
204
|
+
self.assertEqual(cached(2), 2)
|
|
205
|
+
self.assertEqual(cached(3), 3)
|
|
206
|
+
self.assertEqual(cached.cache_info(), (2, 3, 2, 2))
|
|
207
|
+
# now (2,) is the smallest key
|
|
208
|
+
self.assertEqual(cached(1), 1)
|
|
209
|
+
self.assertEqual(cached.cache_info(), (2, 4, 2, 2))
|
|
210
|
+
self.assertEqual(cached(3), 3)
|
|
211
|
+
self.assertEqual(cached.cache_info(), (3, 4, 2, 2))
|
|
212
|
+
self.assertEqual(cached(2), 2)
|
|
213
|
+
self.assertEqual(cached.cache_info(), (3, 5, 2, 2))
|
|
214
|
+
|
|
195
215
|
|
|
196
216
|
class TTLDecoratorTest(unittest.TestCase, DecoratorTestMixin):
|
|
197
217
|
DECORATOR = staticmethod(cachetools.func.ttl_cache)
|
|
198
218
|
|
|
219
|
+
class Timer:
|
|
220
|
+
def __init__(self):
|
|
221
|
+
self.time = 0
|
|
222
|
+
|
|
223
|
+
def __call__(self):
|
|
224
|
+
return self.time
|
|
225
|
+
|
|
226
|
+
def tick(self):
|
|
227
|
+
self.time += 1
|
|
228
|
+
|
|
229
|
+
def test_ttl_expire(self):
|
|
230
|
+
timer = self.Timer()
|
|
231
|
+
cached = cachetools.func.ttl_cache(maxsize=2, ttl=1, timer=timer)(lambda n: n)
|
|
232
|
+
self.assertEqual(cached.cache_parameters(), {"maxsize": 2, "typed": False})
|
|
233
|
+
self.assertEqual(cached(1), 1)
|
|
234
|
+
self.assertEqual(cached.cache_info(), (0, 1, 2, 1))
|
|
235
|
+
self.assertEqual(cached(1), 1)
|
|
236
|
+
self.assertEqual(cached.cache_info(), (1, 1, 2, 1))
|
|
237
|
+
timer.tick()
|
|
238
|
+
self.assertEqual(cached.cache_info(), (1, 1, 2, 0))
|
|
239
|
+
self.assertEqual(cached(1), 1)
|
|
240
|
+
self.assertEqual(cached.cache_info(), (1, 2, 2, 1))
|
|
241
|
+
self.assertEqual(cached(2), 2)
|
|
242
|
+
self.assertEqual(cached.cache_info(), (1, 3, 2, 2))
|
|
243
|
+
timer.tick()
|
|
244
|
+
self.assertEqual(cached(1), 1)
|
|
245
|
+
self.assertEqual(cached.cache_info(), (1, 4, 2, 1))
|
|
246
|
+
|
|
247
|
+
def test_ttl_expire_unbound(self):
|
|
248
|
+
timer = self.Timer()
|
|
249
|
+
cached = cachetools.func.ttl_cache(maxsize=None, ttl=1, timer=timer)(
|
|
250
|
+
lambda n: n
|
|
251
|
+
)
|
|
252
|
+
self.assertEqual(cached.cache_parameters(), {"maxsize": None, "typed": False})
|
|
253
|
+
self.assertEqual(cached.cache_info(), (0, 0, None, 0))
|
|
254
|
+
for n in range(256):
|
|
255
|
+
self.assertEqual(cached(n), n)
|
|
256
|
+
self.assertEqual(cached.cache_info(), (0, 256, None, 256))
|
|
257
|
+
self.assertEqual(cached(0), 0)
|
|
258
|
+
self.assertEqual(cached.cache_info(), (1, 256, None, 256))
|
|
259
|
+
timer.tick()
|
|
260
|
+
self.assertEqual(cached.cache_info(), (1, 256, None, 0))
|
|
261
|
+
self.assertEqual(cached(0), 0)
|
|
262
|
+
self.assertEqual(cached.cache_info(), (1, 257, None, 1))
|
|
263
|
+
|
|
199
264
|
def test_ttl_datetime(self):
|
|
200
265
|
from datetime import datetime, timedelta
|
|
201
266
|
|
|
@@ -38,6 +38,9 @@ class CacheKeysTest(unittest.TestCase):
|
|
|
38
38
|
# untyped keys compare equal
|
|
39
39
|
self.assertEqual(key("x", 1, 2, 3), key("y", 1.0, 2.0, 3.0))
|
|
40
40
|
self.assertEqual(hash(key("x", 1, 2, 3)), hash(key("y", 1.0, 2.0, 3.0)))
|
|
41
|
+
# "self" is positional-only, so it may also be a keyword argument
|
|
42
|
+
self.assertEqual(key("x", self=1), key("y", self=1))
|
|
43
|
+
self.assertNotEqual(key("x", self=1), key("x"))
|
|
41
44
|
|
|
42
45
|
def test_typedkey(self, key=cachetools.keys.typedkey):
|
|
43
46
|
self.assertEqual(key(), key())
|
|
@@ -54,6 +57,8 @@ class CacheKeysTest(unittest.TestCase):
|
|
|
54
57
|
hash(key({}))
|
|
55
58
|
# typed keys compare unequal
|
|
56
59
|
self.assertNotEqual(key(1, 2, 3), key(1.0, 2.0, 3.0))
|
|
60
|
+
self.assertNotEqual(key(1, 2, 3, x=0), key(1, 2, 3, x=0.0))
|
|
61
|
+
self.assertNotEqual(key(1, 2, 3, x=0), key(1, 2, 3, x=False))
|
|
57
62
|
|
|
58
63
|
def test_typedmethodkey(self, key=cachetools.keys.typedmethodkey):
|
|
59
64
|
# similar to typedkey(), but ignores its first positional argument
|
|
@@ -71,6 +76,11 @@ class CacheKeysTest(unittest.TestCase):
|
|
|
71
76
|
hash(key("x", {}))
|
|
72
77
|
# typed keys compare unequal
|
|
73
78
|
self.assertNotEqual(key("x", 1, 2, 3), key("x", 1.0, 2.0, 3.0))
|
|
79
|
+
self.assertNotEqual(key("x", 1, 2, 3, x=0), key("x", 1, 2, 3, x=0.0))
|
|
80
|
+
self.assertNotEqual(key("x", 1, 2, 3, x=0), key("x", 1, 2, 3, x=False))
|
|
81
|
+
# "self" is positional-only, so it may also be a keyword argument
|
|
82
|
+
self.assertEqual(key("x", self=1), key("y", self=1))
|
|
83
|
+
self.assertNotEqual(key("x", self=1), key("x", self=1.0))
|
|
74
84
|
|
|
75
85
|
def test_addkeys(self, key=cachetools.keys.hashkey):
|
|
76
86
|
self.assertIsInstance(key(), tuple)
|
|
@@ -18,39 +18,67 @@ class LFUCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
18
18
|
|
|
19
19
|
self.assertEqual(len(cache), 2)
|
|
20
20
|
self.assertEqual(cache[1], 1)
|
|
21
|
-
self.
|
|
22
|
-
self.
|
|
21
|
+
self.assertNotIn(2, cache)
|
|
22
|
+
self.assertEqual(cache[3], 3)
|
|
23
23
|
|
|
24
|
+
cache[3]
|
|
24
25
|
cache[4] = 4
|
|
25
26
|
self.assertEqual(len(cache), 2)
|
|
26
|
-
self.
|
|
27
|
-
self.assertEqual(cache[
|
|
28
|
-
|
|
29
|
-
cache[1]
|
|
30
|
-
self.assertEqual(len(cache), 2)
|
|
31
|
-
self.assertEqual(cache[1], 1)
|
|
27
|
+
self.assertNotIn(1, cache)
|
|
28
|
+
self.assertEqual(cache[3], 3)
|
|
32
29
|
self.assertEqual(cache[4], 4)
|
|
33
30
|
|
|
34
31
|
def test_lfu_tie(self):
|
|
35
32
|
cache = LFUCache[int, int](maxsize=3)
|
|
36
33
|
|
|
37
34
|
cache[1] = 1
|
|
38
|
-
cache[1]
|
|
39
35
|
cache[2] = 2
|
|
40
36
|
cache[3] = 3
|
|
41
|
-
cache[4] = 4
|
|
42
37
|
|
|
38
|
+
cache[3]
|
|
39
|
+
cache[2]
|
|
40
|
+
cache[1]
|
|
41
|
+
|
|
42
|
+
# do not use self.assertEqual(cache[1], 1) here, since that
|
|
43
|
+
# would mess up usage counts
|
|
43
44
|
self.assertEqual(len(cache), 3)
|
|
44
45
|
self.assertIn(1, cache)
|
|
46
|
+
self.assertIn(2, cache)
|
|
47
|
+
self.assertIn(3, cache)
|
|
48
|
+
|
|
49
|
+
cache[4] = 4
|
|
50
|
+
self.assertEqual(len(cache), 3)
|
|
51
|
+
self.assertIn(1, cache)
|
|
52
|
+
self.assertIn(2, cache)
|
|
53
|
+
self.assertNotIn(3, cache)
|
|
45
54
|
self.assertIn(4, cache)
|
|
46
|
-
self.assertTrue(2 in cache or 3 in cache)
|
|
47
|
-
self.assertTrue(2 not in cache or 3 not in cache)
|
|
48
55
|
|
|
56
|
+
cache[4]
|
|
49
57
|
cache[5] = 5
|
|
50
58
|
self.assertEqual(len(cache), 3)
|
|
51
59
|
self.assertIn(1, cache)
|
|
60
|
+
self.assertNotIn(2, cache)
|
|
61
|
+
self.assertNotIn(3, cache)
|
|
62
|
+
self.assertIn(4, cache)
|
|
52
63
|
self.assertIn(5, cache)
|
|
53
|
-
|
|
64
|
+
|
|
65
|
+
def test_lfu_tie_insert_only(self):
|
|
66
|
+
cache = LFUCache[int, int](maxsize=3)
|
|
67
|
+
|
|
68
|
+
cache[1] = 1
|
|
69
|
+
cache[2] = 2
|
|
70
|
+
cache[3] = 3
|
|
71
|
+
|
|
72
|
+
# items never retrieved are tied at use count 1, so they are
|
|
73
|
+
# evicted in insertion order; the set-based implementation
|
|
74
|
+
# used up to v7.2 only fails this from key 17 on
|
|
75
|
+
for key in range(4, 20):
|
|
76
|
+
cache[key] = key
|
|
77
|
+
self.assertEqual(len(cache), 3)
|
|
78
|
+
self.assertNotIn(key - 3, cache)
|
|
79
|
+
self.assertIn(key - 2, cache)
|
|
80
|
+
self.assertIn(key - 1, cache)
|
|
81
|
+
self.assertIn(key, cache)
|
|
54
82
|
|
|
55
83
|
def test_lfu_getsizeof(self):
|
|
56
84
|
cache = LFUCache[int, int](maxsize=3, getsizeof=lambda x: x)
|
|
@@ -5,8 +5,8 @@ from cachetools import LRUCache, cached, cachedmethod
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class WaitCondition(Condition):
|
|
8
|
-
def __init__(self):
|
|
9
|
-
super().__init__()
|
|
8
|
+
def __init__(self, lock=None):
|
|
9
|
+
super().__init__(lock)
|
|
10
10
|
self.waiting = Event()
|
|
11
11
|
|
|
12
12
|
def wait(self, timeout=None):
|
|
@@ -93,6 +93,90 @@ class ThreadingTest(unittest.TestCase):
|
|
|
93
93
|
func = Cached().method
|
|
94
94
|
self.run_cond_wait_test(func, cond, started, release, calls)
|
|
95
95
|
|
|
96
|
+
def test_cached_lock_cond(self):
|
|
97
|
+
for info in (False, True):
|
|
98
|
+
with self.subTest(info=info):
|
|
99
|
+
cache = LRUCache(1)
|
|
100
|
+
lock = Lock()
|
|
101
|
+
cond = WaitCondition(lock)
|
|
102
|
+
started = Event()
|
|
103
|
+
release = Event()
|
|
104
|
+
calls = []
|
|
105
|
+
|
|
106
|
+
@cached(cache, lock=lock, condition=cond, info=info)
|
|
107
|
+
def func(calls=calls, started=started, release=release):
|
|
108
|
+
calls.append(None)
|
|
109
|
+
started.set()
|
|
110
|
+
release.wait(self.TIMEOUT)
|
|
111
|
+
return object()
|
|
112
|
+
|
|
113
|
+
self.run_cond_wait_test(func, cond, started, release, calls)
|
|
114
|
+
|
|
115
|
+
def test_cachedmethod_lock_cond(self):
|
|
116
|
+
for info in (False, True):
|
|
117
|
+
with self.subTest(info=info):
|
|
118
|
+
cache = LRUCache(1)
|
|
119
|
+
lock = Lock()
|
|
120
|
+
cond = WaitCondition(lock)
|
|
121
|
+
started = Event()
|
|
122
|
+
release = Event()
|
|
123
|
+
calls = []
|
|
124
|
+
|
|
125
|
+
class Cached:
|
|
126
|
+
@cachedmethod(
|
|
127
|
+
lambda _: cache,
|
|
128
|
+
lock=lambda _: lock,
|
|
129
|
+
condition=lambda _: cond,
|
|
130
|
+
info=info,
|
|
131
|
+
)
|
|
132
|
+
def method(
|
|
133
|
+
self,
|
|
134
|
+
calls=calls,
|
|
135
|
+
started=started,
|
|
136
|
+
release=release,
|
|
137
|
+
timeout=self.TIMEOUT,
|
|
138
|
+
):
|
|
139
|
+
calls.append(None)
|
|
140
|
+
started.set()
|
|
141
|
+
release.wait(timeout)
|
|
142
|
+
return object()
|
|
143
|
+
|
|
144
|
+
func = Cached().method
|
|
145
|
+
self.run_cond_wait_test(func, cond, started, release, calls)
|
|
146
|
+
|
|
147
|
+
def test_cached_unrelated_lock_cond(self):
|
|
148
|
+
# a condition variable not created with the lock fails on
|
|
149
|
+
# the first cache miss, even without concurrent calls
|
|
150
|
+
for info in (False, True):
|
|
151
|
+
with self.subTest(info=info):
|
|
152
|
+
|
|
153
|
+
@cached(LRUCache(1), lock=Lock(), condition=Condition(), info=info)
|
|
154
|
+
def func():
|
|
155
|
+
return object()
|
|
156
|
+
|
|
157
|
+
with self.assertRaises(RuntimeError):
|
|
158
|
+
func()
|
|
159
|
+
|
|
160
|
+
def test_cachedmethod_unrelated_lock_cond(self):
|
|
161
|
+
for info in (False, True):
|
|
162
|
+
with self.subTest(info=info):
|
|
163
|
+
cache = LRUCache(1)
|
|
164
|
+
lock = Lock()
|
|
165
|
+
cond = Condition()
|
|
166
|
+
|
|
167
|
+
class Cached:
|
|
168
|
+
@cachedmethod(
|
|
169
|
+
lambda _: cache,
|
|
170
|
+
lock=lambda _: lock,
|
|
171
|
+
condition=lambda _: cond,
|
|
172
|
+
info=info,
|
|
173
|
+
)
|
|
174
|
+
def method(self):
|
|
175
|
+
return object()
|
|
176
|
+
|
|
177
|
+
with self.assertRaises(RuntimeError):
|
|
178
|
+
Cached().method()
|
|
179
|
+
|
|
96
180
|
def run_cond_wait_test(self, func, cond, started, release, calls):
|
|
97
181
|
results = [None] * 2
|
|
98
182
|
|
|
@@ -122,14 +206,6 @@ class ThreadingTest(unittest.TestCase):
|
|
|
122
206
|
self.assertEqual(cache_info.hits + cache_info.misses, self.NTHREADS)
|
|
123
207
|
self.assertGreaterEqual(cache_info.misses, 1)
|
|
124
208
|
|
|
125
|
-
def run_cond_test(self, func, cache, info):
|
|
126
|
-
self.run_threading_test(func, cache)
|
|
127
|
-
|
|
128
|
-
if info:
|
|
129
|
-
cache_info = func.cache_info()
|
|
130
|
-
self.assertEqual(cache_info.hits, self.NTHREADS - 1)
|
|
131
|
-
self.assertEqual(cache_info.misses, 1)
|
|
132
|
-
|
|
133
209
|
def run_threading_test(self, func, cache):
|
|
134
210
|
results = [None] * self.NTHREADS
|
|
135
211
|
|
|
@@ -248,7 +248,7 @@ class TLRUCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
248
248
|
self.assertEqual(1, len(cache))
|
|
249
249
|
|
|
250
250
|
def test_ttu_atomic(self):
|
|
251
|
-
cache = TLRUCache[int, int, int](
|
|
251
|
+
cache = TLRUCache[int, int | None, int](
|
|
252
252
|
maxsize=1, ttu=lambda k, v, t: t + 2, timer=Timer(auto=True)
|
|
253
253
|
)
|
|
254
254
|
cache[1] = 1
|
|
@@ -258,7 +258,7 @@ class TLRUCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
258
258
|
cache[1] = 1
|
|
259
259
|
self.assertEqual(1, cache.pop(1))
|
|
260
260
|
cache[1] = 1
|
|
261
|
-
self.assertEqual(1, cache.setdefault(1))
|
|
261
|
+
self.assertEqual(1, cache.setdefault(1)) # implicit default=None
|
|
262
262
|
cache[1] = 1
|
|
263
263
|
cache.clear()
|
|
264
264
|
self.assertEqual(0, len(cache))
|
|
@@ -400,3 +400,23 @@ class TLRUCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
400
400
|
cache.timer.tick()
|
|
401
401
|
cache.timer.tick() # past TTL
|
|
402
402
|
self.assertNotIn(42, cache)
|
|
403
|
+
|
|
404
|
+
def test_tlru_default_timer_stubs(self):
|
|
405
|
+
# type stubs must infer key and value types with the default timer
|
|
406
|
+
def ttu(_key: str, value: int, now: float) -> float:
|
|
407
|
+
return now + value
|
|
408
|
+
|
|
409
|
+
cache: TLRUCache[str, int] = TLRUCache(maxsize=2, ttu=ttu)
|
|
410
|
+
|
|
411
|
+
cache["a"] = 3600
|
|
412
|
+
self.assertEqual(3600, cache["a"])
|
|
413
|
+
self.assertEqual(1, len(cache))
|
|
414
|
+
self.assertIn("a", cache)
|
|
415
|
+
|
|
416
|
+
sized: TLRUCache[str, str] = TLRUCache(
|
|
417
|
+
maxsize=10, ttu=lambda _k, _v, now: now + 3600, getsizeof=len
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
sized["abc"] = "abc"
|
|
421
|
+
self.assertEqual("abc", sized["abc"])
|
|
422
|
+
self.assertEqual(3, sized.currsize)
|
|
@@ -189,7 +189,7 @@ class TTLCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
189
189
|
self.assertEqual([2], [key for key in cache])
|
|
190
190
|
|
|
191
191
|
def test_ttl_atomic(self):
|
|
192
|
-
cache = TTLCache[int, int, int](maxsize=1, ttl=2, timer=Timer(auto=True))
|
|
192
|
+
cache = TTLCache[int, int | None, int](maxsize=1, ttl=2, timer=Timer(auto=True))
|
|
193
193
|
cache[1] = 1
|
|
194
194
|
self.assertEqual(1, cache[1])
|
|
195
195
|
cache[1] = 1
|
|
@@ -197,7 +197,7 @@ class TTLCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
197
197
|
cache[1] = 1
|
|
198
198
|
self.assertEqual(1, cache.pop(1))
|
|
199
199
|
cache[1] = 1
|
|
200
|
-
self.assertEqual(1, cache.setdefault(1))
|
|
200
|
+
self.assertEqual(1, cache.setdefault(1)) # implicit default=None
|
|
201
201
|
cache[1] = 1
|
|
202
202
|
cache.clear()
|
|
203
203
|
self.assertEqual(0, len(cache))
|
|
@@ -272,3 +272,18 @@ class TTLCacheTest(unittest.TestCase, CacheTestMixin):
|
|
|
272
272
|
cache.timer.tick()
|
|
273
273
|
cache.timer.tick() # past TTL
|
|
274
274
|
self.assertNotIn(42, cache)
|
|
275
|
+
|
|
276
|
+
def test_ttl_default_timer_stubs(self):
|
|
277
|
+
# type stubs must infer key and value types with the default timer
|
|
278
|
+
cache: TTLCache[str, int] = TTLCache(maxsize=2, ttl=math.inf)
|
|
279
|
+
|
|
280
|
+
cache["a"] = 1
|
|
281
|
+
self.assertEqual(1, cache["a"])
|
|
282
|
+
self.assertEqual(1, len(cache))
|
|
283
|
+
self.assertIn("a", cache)
|
|
284
|
+
|
|
285
|
+
sized: TTLCache[str, str] = TTLCache(maxsize=10, ttl=math.inf, getsizeof=len)
|
|
286
|
+
|
|
287
|
+
sized["abc"] = "abc"
|
|
288
|
+
self.assertEqual("abc", sized["abc"])
|
|
289
|
+
self.assertEqual(3, sized.currsize)
|
|
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
|