cachetools 7.2.7__tar.gz → 7.2.8__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.2.8}/CHANGELOG.rst +9 -0
  2. {cachetools-7.2.7/src/cachetools.egg-info → cachetools-7.2.8}/PKG-INFO +1 -1
  3. {cachetools-7.2.7 → cachetools-7.2.8}/docs/index.rst +12 -0
  4. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools/__init__.py +1 -1
  5. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools/__init__.pyi +7 -9
  6. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools/_cachedmethod.py +10 -10
  7. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools/keys.py +2 -2
  8. {cachetools-7.2.7 → cachetools-7.2.8/src/cachetools.egg-info}/PKG-INFO +1 -1
  9. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_keys.py +10 -0
  10. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_threading.py +86 -10
  11. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_tlru.py +20 -0
  12. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_ttl.py +15 -0
  13. {cachetools-7.2.7 → cachetools-7.2.8}/LICENSE +0 -0
  14. {cachetools-7.2.7 → cachetools-7.2.8}/MANIFEST.in +0 -0
  15. {cachetools-7.2.7 → cachetools-7.2.8}/README.rst +0 -0
  16. {cachetools-7.2.7 → cachetools-7.2.8}/docs/conf.py +0 -0
  17. {cachetools-7.2.7 → cachetools-7.2.8}/pyproject.toml +0 -0
  18. {cachetools-7.2.7 → cachetools-7.2.8}/setup.cfg +0 -0
  19. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools/_cached.py +0 -0
  20. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools/_descriptor.py +0 -0
  21. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools/func.py +0 -0
  22. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools/func.pyi +0 -0
  23. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools/keys.pyi +0 -0
  24. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools/py.typed +0 -0
  25. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools.egg-info/SOURCES.txt +0 -0
  26. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools.egg-info/dependency_links.txt +0 -0
  27. {cachetools-7.2.7 → cachetools-7.2.8}/src/cachetools.egg-info/top_level.txt +0 -0
  28. {cachetools-7.2.7 → cachetools-7.2.8}/tests/__init__.py +0 -0
  29. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_cache.py +0 -0
  30. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_cached.py +0 -0
  31. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_cachedmethod.py +0 -0
  32. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_classmethod.py +0 -0
  33. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_fifo.py +0 -0
  34. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_func.py +0 -0
  35. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_lfu.py +0 -0
  36. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_lru.py +0 -0
  37. {cachetools-7.2.7 → cachetools-7.2.8}/tests/test_rr.py +0 -0
  38. {cachetools-7.2.7 → cachetools-7.2.8}/tox.ini +0 -0
@@ -1,3 +1,12 @@
1
+ v7.2.8 (2026-10-31)
2
+ ===================
3
+
4
+ - Fix type stubs for ``TTLCache`` and ``TLRUCache`` with default
5
+ ``timer``.
6
+
7
+ - Minor documentation and unit test improvements.
8
+
9
+
1
10
  v7.2.7 (2026-10-28)
2
11
  ===================
3
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.7
3
+ Version: 7.2.8
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>
@@ -356,6 +356,16 @@ often called with the same arguments:
356
356
  implement the `context manager`_ protocol, and will also be used to
357
357
  guard access to the cache.
358
358
 
359
+ .. note::
360
+
361
+ Even though `lock` and `condition` can be passed separately,
362
+ the condition variable must be associated with the lock that
363
+ guards the cache. In practice, when using
364
+ :class:`threading.Lock` and :class:`threading.Condition`, the
365
+ condition variable must be created with the corresponding lock,
366
+ e.g. ``condition=threading.Condition(lock)``; an unrelated
367
+ :class:`threading.Condition` will raise :exc:`RuntimeError`.
368
+
359
369
  The decorator's `cache`, `key`, `lock` and `condition` parameters
360
370
  are also available as :attr:`cache`, :attr:`cache_key`,
361
371
  :attr:`cache_lock` and :attr:`cache_condition` attributes of the
@@ -548,6 +558,8 @@ often called with the same arguments:
548
558
  `lock(self)` will only guard access to the cache itself. It is
549
559
  the user's responsibility to handle concurrent calls to the
550
560
  underlying wrapped method in a multithreaded environment.
561
+ Likewise, a condition variable returned by `condition(self)`
562
+ must be associated with a lock returned by `lock(self)`.
551
563
 
552
564
  Similarly to the :func:`cached` function decorator, the `cache`,
553
565
  `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.2.8"
16
16
 
17
17
  import collections
18
18
  import collections.abc
@@ -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__(
@@ -94,13 +92,13 @@ class _TimedCache(Cache[_KT, _VT], Generic[_KT, _VT, _TT]):
94
92
  def timer(self) -> _Timer[_TT]: ...
95
93
 
96
94
  class TTLCache(_TimedCache[_KT, _VT, _TT]):
97
- @overload
95
+ @overload # without timer, _TT takes its default
98
96
  def __init__(
99
- self: TTLCache[_KT2, _VT2, float],
97
+ self,
100
98
  maxsize: float,
101
99
  ttl: float,
102
100
  *,
103
- getsizeof: Callable[[_VT2], float] | None = None,
101
+ getsizeof: Callable[[_VT], float] | None = None,
104
102
  ) -> None: ...
105
103
  @overload
106
104
  def __init__(
@@ -115,13 +113,13 @@ class TTLCache(_TimedCache[_KT, _VT, _TT]):
115
113
  def expire(self, time: _TT | None = None) -> list[tuple[_KT, _VT]]: ...
116
114
 
117
115
  class TLRUCache(_TimedCache[_KT, _VT, _TT]):
118
- @overload
116
+ @overload # without timer, _TT takes its default
119
117
  def __init__(
120
- self: TLRUCache[_KT2, _VT2, float],
118
+ self,
121
119
  maxsize: float,
122
- ttu: Callable[[_KT2, _VT2, float], float],
120
+ ttu: Callable[[_KT, _VT, float], float],
123
121
  *,
124
- getsizeof: Callable[[_VT2], float] | None = None,
122
+ getsizeof: Callable[[_VT], float] | None = None,
125
123
  ) -> None: ...
126
124
  @overload
127
125
  def __init__(
@@ -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
@@ -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.2.8
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>
@@ -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)
@@ -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
 
@@ -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)
@@ -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