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.
Files changed (38) hide show
  1. {cachetools-7.2.7 → cachetools-7.3.0}/CHANGELOG.rst +21 -0
  2. {cachetools-7.2.7/src/cachetools.egg-info → cachetools-7.3.0}/PKG-INFO +1 -1
  3. {cachetools-7.2.7 → cachetools-7.3.0}/docs/conf.py +0 -1
  4. {cachetools-7.2.7 → cachetools-7.3.0}/docs/index.rst +22 -3
  5. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/__init__.py +7 -7
  6. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/__init__.pyi +14 -13
  7. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/_cachedmethod.py +10 -10
  8. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/_descriptor.py +1 -1
  9. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/keys.py +2 -2
  10. {cachetools-7.2.7 → cachetools-7.3.0/src/cachetools.egg-info}/PKG-INFO +1 -1
  11. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_cached.py +15 -0
  12. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_cachedmethod.py +8 -0
  13. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_classmethod.py +18 -0
  14. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_func.py +65 -0
  15. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_keys.py +10 -0
  16. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_lfu.py +41 -13
  17. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_threading.py +86 -10
  18. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_tlru.py +22 -2
  19. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_ttl.py +17 -2
  20. {cachetools-7.2.7 → cachetools-7.3.0}/LICENSE +0 -0
  21. {cachetools-7.2.7 → cachetools-7.3.0}/MANIFEST.in +0 -0
  22. {cachetools-7.2.7 → cachetools-7.3.0}/README.rst +0 -0
  23. {cachetools-7.2.7 → cachetools-7.3.0}/pyproject.toml +0 -0
  24. {cachetools-7.2.7 → cachetools-7.3.0}/setup.cfg +0 -0
  25. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/_cached.py +0 -0
  26. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/func.py +0 -0
  27. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/func.pyi +0 -0
  28. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/keys.pyi +0 -0
  29. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools/py.typed +0 -0
  30. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools.egg-info/SOURCES.txt +0 -0
  31. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools.egg-info/dependency_links.txt +0 -0
  32. {cachetools-7.2.7 → cachetools-7.3.0}/src/cachetools.egg-info/top_level.txt +0 -0
  33. {cachetools-7.2.7 → cachetools-7.3.0}/tests/__init__.py +0 -0
  34. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_cache.py +0 -0
  35. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_fifo.py +0 -0
  36. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_lru.py +0 -0
  37. {cachetools-7.2.7 → cachetools-7.3.0}/tests/test_rr.py +0 -0
  38. {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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.7
3
+ Version: 7.3.0
4
4
  Summary: Extensible memoizing collections and decorators
5
5
  Author-email: Thomas Kemmer <tkemmer@computer.org>
6
6
  Maintainer-email: Thomas Kemmer <tkemmer@computer.org>
@@ -13,6 +13,5 @@ extensions = [
13
13
  "sphinx.ext.todo",
14
14
  ]
15
15
  exclude_patterns = ["_build"]
16
- master_doc = "index"
17
16
  html_theme = "classic"
18
17
  intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
@@ -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
- Updating a cache item via :meth:`LFUCache.__setitem__` also counts
98
- as a use of the item, i.e. it will increment the item's use count.
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.2.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 = set()
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.add(key)
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.remove(key)
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 an arbitrary element
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.add(key)
287
- link.keys.remove(key)
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
- def setdefault(self, key: _KT, default: _VT | None = None) -> _VT: ...
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: TTLCache[_KT2, _VT2, float],
102
+ self,
100
103
  maxsize: float,
101
104
  ttl: float,
102
105
  *,
103
- getsizeof: Callable[[_VT2], float] | None = None,
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, # FIXME: must be "addable" to _TT
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: TLRUCache[_KT2, _VT2, float],
123
+ self,
121
124
  maxsize: float,
122
- ttu: Callable[[_KT2, _VT2, float], float],
125
+ ttu: Callable[[_KT, _VT, float], float],
123
126
  *,
124
- getsizeof: Callable[[_VT2], float] | None = None,
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.7
3
+ Version: 7.3.0
4
4
  Summary: Extensible memoizing collections and decorators
5
5
  Author-email: Thomas Kemmer <tkemmer@computer.org>
6
6
  Maintainer-email: Thomas Kemmer <tkemmer@computer.org>
@@ -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.assertTrue(2 in cache or 3 in cache)
22
- self.assertTrue(2 not in cache or 3 not in cache)
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.assertEqual(cache[4], 4)
27
- self.assertEqual(cache[1], 1)
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
- self.assertTrue(2 in cache or 3 in cache or 4 in cache)
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