cachetools 7.2.8__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.8 → cachetools-7.3.0}/CHANGELOG.rst +12 -0
  2. {cachetools-7.2.8/src/cachetools.egg-info → cachetools-7.3.0}/PKG-INFO +1 -1
  3. {cachetools-7.2.8 → cachetools-7.3.0}/docs/conf.py +0 -1
  4. {cachetools-7.2.8 → cachetools-7.3.0}/docs/index.rst +10 -3
  5. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools/__init__.py +7 -7
  6. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools/__init__.pyi +7 -4
  7. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools/_descriptor.py +1 -1
  8. {cachetools-7.2.8 → cachetools-7.3.0/src/cachetools.egg-info}/PKG-INFO +1 -1
  9. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_cached.py +15 -0
  10. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_cachedmethod.py +8 -0
  11. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_classmethod.py +18 -0
  12. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_func.py +65 -0
  13. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_lfu.py +41 -13
  14. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_tlru.py +2 -2
  15. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_ttl.py +2 -2
  16. {cachetools-7.2.8 → cachetools-7.3.0}/LICENSE +0 -0
  17. {cachetools-7.2.8 → cachetools-7.3.0}/MANIFEST.in +0 -0
  18. {cachetools-7.2.8 → cachetools-7.3.0}/README.rst +0 -0
  19. {cachetools-7.2.8 → cachetools-7.3.0}/pyproject.toml +0 -0
  20. {cachetools-7.2.8 → cachetools-7.3.0}/setup.cfg +0 -0
  21. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools/_cached.py +0 -0
  22. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools/_cachedmethod.py +0 -0
  23. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools/func.py +0 -0
  24. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools/func.pyi +0 -0
  25. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools/keys.py +0 -0
  26. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools/keys.pyi +0 -0
  27. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools/py.typed +0 -0
  28. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools.egg-info/SOURCES.txt +0 -0
  29. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools.egg-info/dependency_links.txt +0 -0
  30. {cachetools-7.2.8 → cachetools-7.3.0}/src/cachetools.egg-info/top_level.txt +0 -0
  31. {cachetools-7.2.8 → cachetools-7.3.0}/tests/__init__.py +0 -0
  32. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_cache.py +0 -0
  33. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_fifo.py +0 -0
  34. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_keys.py +0 -0
  35. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_lru.py +0 -0
  36. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_rr.py +0 -0
  37. {cachetools-7.2.8 → cachetools-7.3.0}/tests/test_threading.py +0 -0
  38. {cachetools-7.2.8 → cachetools-7.3.0}/tox.ini +0 -0
@@ -1,3 +1,15 @@
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
+
1
13
  v7.2.8 (2026-10-31)
2
14
  ===================
3
15
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.8
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
@@ -12,7 +12,7 @@ __all__ = (
12
12
  "cachedmethod",
13
13
  )
14
14
 
15
- __version__ = "7.2.8"
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
@@ -51,7 +51,12 @@ class Cache(MutableMapping[_KT, _VT]):
51
51
  def pop(self, key: _KT) -> _VT: ...
52
52
  @overload
53
53
  def pop(self, key: _KT, default: _VT | _T) -> _VT | _T: ...
54
- 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: ...
55
60
  @property
56
61
  def maxsize(self) -> float: ...
57
62
  @property
@@ -104,7 +109,7 @@ class TTLCache(_TimedCache[_KT, _VT, _TT]):
104
109
  def __init__(
105
110
  self,
106
111
  maxsize: float,
107
- ttl: Any, # FIXME: must be "addable" to _TT
112
+ ttl: Any, # must be "addable" to _TT
108
113
  timer: Callable[[], _TT],
109
114
  getsizeof: Callable[[_VT], float] | None = None,
110
115
  ) -> None: ...
@@ -141,8 +146,6 @@ class _CacheInfo(NamedTuple):
141
146
 
142
147
  @type_check_only
143
148
  class _AbstractCondition(AbstractContextManager[Any], Protocol):
144
- # implementation and unit tests currently do not use plain wait()
145
- # and notify(); these are reserved for future use
146
149
  def wait(self, timeout: float | None = None) -> bool: ...
147
150
  def wait_for(
148
151
  self, predicate: Callable[[], _T], timeout: float | None = None
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.8
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
 
@@ -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)
@@ -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))
@@ -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))
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