cachetools 7.2.2__tar.gz → 7.2.3__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.2 → cachetools-7.2.3}/CHANGELOG.rst +8 -0
  2. {cachetools-7.2.2/src/cachetools.egg-info → cachetools-7.2.3}/PKG-INFO +1 -1
  3. {cachetools-7.2.2 → cachetools-7.2.3}/pyproject.toml +1 -0
  4. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools/__init__.py +1 -1
  5. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools/__init__.pyi +35 -0
  6. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools/_cached.py +13 -2
  7. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools/func.pyi +28 -2
  8. {cachetools-7.2.2 → cachetools-7.2.3/src/cachetools.egg-info}/PKG-INFO +1 -1
  9. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_cached.py +44 -7
  10. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_cachedmethod.py +4 -4
  11. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_func.py +44 -0
  12. {cachetools-7.2.2 → cachetools-7.2.3}/LICENSE +0 -0
  13. {cachetools-7.2.2 → cachetools-7.2.3}/MANIFEST.in +0 -0
  14. {cachetools-7.2.2 → cachetools-7.2.3}/README.rst +0 -0
  15. {cachetools-7.2.2 → cachetools-7.2.3}/docs/conf.py +0 -0
  16. {cachetools-7.2.2 → cachetools-7.2.3}/docs/index.rst +0 -0
  17. {cachetools-7.2.2 → cachetools-7.2.3}/setup.cfg +0 -0
  18. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools/_cachedmethod.py +0 -0
  19. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools/_descriptor.py +0 -0
  20. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools/func.py +0 -0
  21. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools/keys.py +0 -0
  22. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools/keys.pyi +0 -0
  23. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools/py.typed +0 -0
  24. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools.egg-info/SOURCES.txt +0 -0
  25. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools.egg-info/dependency_links.txt +0 -0
  26. {cachetools-7.2.2 → cachetools-7.2.3}/src/cachetools.egg-info/top_level.txt +0 -0
  27. {cachetools-7.2.2 → cachetools-7.2.3}/tests/__init__.py +0 -0
  28. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_cache.py +0 -0
  29. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_classmethod.py +0 -0
  30. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_fifo.py +0 -0
  31. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_keys.py +0 -0
  32. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_lfu.py +0 -0
  33. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_lru.py +0 -0
  34. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_rr.py +0 -0
  35. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_threading.py +0 -0
  36. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_tlru.py +0 -0
  37. {cachetools-7.2.2 → cachetools-7.2.3}/tests/test_ttl.py +0 -0
  38. {cachetools-7.2.2 → cachetools-7.2.3}/tox.ini +0 -0
@@ -1,3 +1,11 @@
1
+ v7.2.3 (2026-10-17)
2
+ ===================
3
+
4
+ - Fix ``@cached`` decorator stacking behavior.
5
+
6
+ - Minor type stub improvements.
7
+
8
+
1
9
  v7.2.2 (2026-10-15)
2
10
  ===================
3
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.2
3
+ Version: 7.2.3
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>
@@ -50,6 +50,7 @@ version = {attr = "cachetools.__version__"}
50
50
 
51
51
  [tool.pyright]
52
52
  typeCheckingMode = "standard"
53
+ reportUnnecessaryTypeIgnoreComment = "error"
53
54
 
54
55
  [tool.ruff.lint]
55
56
  ignore = ["DTZ005", "UP031"]
@@ -12,7 +12,7 @@ __all__ = (
12
12
  "cachedmethod",
13
13
  )
14
14
 
15
- __version__ = "7.2.2"
15
+ __version__ = "7.2.3"
16
16
 
17
17
  import collections
18
18
  import collections.abc
@@ -29,7 +29,9 @@ __all__: Final = (
29
29
  __version__: str
30
30
 
31
31
  _P = ParamSpec("_P")
32
+ _Q = ParamSpec("_Q")
32
33
  _R = TypeVar("_R")
34
+ _S = TypeVar("_S")
33
35
  _T = TypeVar("_T")
34
36
  _KT = TypeVar("_KT")
35
37
  _VT = TypeVar("_VT")
@@ -153,16 +155,47 @@ class _AbstractCondition(AbstractContextManager[Any], Protocol):
153
155
  class _cached_wrapper(Generic[_P, _R]):
154
156
  __wrapped__: Callable[_P, _R]
155
157
  __name__: str
158
+ __qualname__: str
156
159
  __doc__: str | None
157
160
  cache: MutableMapping[Any, Any] | None
158
161
  cache_key: Callable[..., Any] = ...
159
162
  cache_lock: AbstractContextManager[Any] | None = None
160
163
  cache_condition: _AbstractCondition | None = None
164
+ @overload # class access, e.g. Cls.method
165
+ def __get__(
166
+ self, obj: None, objtype: type | None = None
167
+ ) -> _cached_wrapper[_P, _R]: ...
168
+ @overload # instance access binds the first parameter
169
+ def __get__(
170
+ self: _cached_wrapper[Concatenate[_S, _Q], _R],
171
+ obj: _S,
172
+ objtype: type | None = None,
173
+ ) -> _cached_wrapper[_Q, _R]: ...
174
+ @overload # fallback, e.g. when wrapped by @staticmethod or @classmethod
175
+ def __get__(
176
+ self, obj: object, objtype: type | None = None
177
+ ) -> _cached_wrapper[_P, _R]: ...
161
178
  def __call__(self, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
162
179
  def cache_clear(self) -> None: ...
163
180
 
164
181
  @type_check_only
165
182
  class _cached_wrapper_info(_cached_wrapper[_P, _R]):
183
+ @overload
184
+ def __get__(
185
+ self, obj: None, objtype: type | None = None
186
+ ) -> _cached_wrapper_info[_P, _R]: ...
187
+ @overload
188
+ def __get__(
189
+ self: _cached_wrapper_info[Concatenate[_S, _Q], _R],
190
+ obj: _S,
191
+ objtype: type | None = None,
192
+ ) -> _cached_wrapper_info[_Q, _R]: ...
193
+ # pyright cannot match the self-typed base overload; each return type is
194
+ # the _cached_wrapper_info subtype of the corresponding base return type
195
+ @overload
196
+ def __get__( # pyright: ignore[reportIncompatibleMethodOverride]
197
+ self, obj: object, objtype: type | None = None
198
+ ) -> _cached_wrapper_info[_P, _R]: ...
166
199
  def cache_info(self) -> _CacheInfo: ...
167
200
 
168
201
  @overload
@@ -195,6 +228,7 @@ def cached(
195
228
  class _cachedmethod_unbound(Generic[_P, _R]):
196
229
  __wrapped__: Callable[Concatenate[Any, _P], _R]
197
230
  __name__: str
231
+ __qualname__: str
198
232
  __doc__: str | None
199
233
  @overload
200
234
  def __call__(self, obj: Any, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
@@ -206,6 +240,7 @@ class _cachedmethod_unbound(Generic[_P, _R]):
206
240
  class _cachedmethod_wrapper(Generic[_P, _R]):
207
241
  __wrapped__: Callable[Concatenate[Any, _P], _R]
208
242
  __name__: str
243
+ __qualname__: str
209
244
  __doc__: str | None
210
245
  cache: MutableMapping[Any, Any]
211
246
  cache_key: Callable[..., Any] = ...
@@ -11,6 +11,10 @@ import warnings
11
11
  # over ease of maintenance, thus providing separate wrappers for
12
12
  # all valid combinations of decorator parameters lock, condition and
13
13
  # info.
14
+ #
15
+ # Each wrapper calls functools.update_wrapper() before setting its own
16
+ # attributes, so func.__dict__ cannot override wrapper state, e.g. when
17
+ # stacking decorators.
14
18
 
15
19
 
16
20
  def _condition_info(func, cache, key, lock, cond, info):
@@ -52,6 +56,7 @@ def _condition_info(func, cache, key, lock, cond, info):
52
56
  with lock:
53
57
  return info(hits, misses)
54
58
 
59
+ functools.update_wrapper(wrapper, func)
55
60
  wrapper.cache_clear = cache_clear
56
61
  wrapper.cache_info = cache_info
57
62
  return wrapper
@@ -90,6 +95,7 @@ def _locked_info(func, cache, key, lock, info):
90
95
  with lock:
91
96
  return info(hits, misses)
92
97
 
98
+ functools.update_wrapper(wrapper, func)
93
99
  wrapper.cache_clear = cache_clear
94
100
  wrapper.cache_info = cache_info
95
101
  return wrapper
@@ -122,6 +128,7 @@ def _unlocked_info(func, cache, key, info):
122
128
  def cache_info():
123
129
  return info(hits, misses)
124
130
 
131
+ functools.update_wrapper(wrapper, func)
125
132
  wrapper.cache_clear = cache_clear
126
133
  wrapper.cache_info = cache_info
127
134
  return wrapper
@@ -139,6 +146,7 @@ def _uncached_info(func, info):
139
146
  nonlocal misses
140
147
  misses = 0
141
148
 
149
+ functools.update_wrapper(wrapper, func)
142
150
  wrapper.cache_clear = cache_clear
143
151
  wrapper.cache_info = lambda: info(0, misses)
144
152
  return wrapper
@@ -173,6 +181,7 @@ def _condition(func, cache, key, lock, cond):
173
181
  with lock:
174
182
  cache.clear()
175
183
 
184
+ functools.update_wrapper(wrapper, func)
176
185
  wrapper.cache_clear = cache_clear
177
186
  return wrapper
178
187
 
@@ -199,6 +208,7 @@ def _locked(func, cache, key, lock):
199
208
  with lock:
200
209
  cache.clear()
201
210
 
211
+ functools.update_wrapper(wrapper, func)
202
212
  wrapper.cache_clear = cache_clear
203
213
  return wrapper
204
214
 
@@ -217,6 +227,7 @@ def _unlocked(func, cache, key):
217
227
  pass # value too large
218
228
  return v
219
229
 
230
+ functools.update_wrapper(wrapper, func)
220
231
  wrapper.cache_clear = lambda: cache.clear()
221
232
  return wrapper
222
233
 
@@ -225,6 +236,7 @@ def _uncached(func):
225
236
  def wrapper(*args, **kwargs):
226
237
  return func(*args, **kwargs)
227
238
 
239
+ functools.update_wrapper(wrapper, func)
228
240
  wrapper.cache_clear = lambda: None
229
241
  return wrapper
230
242
 
@@ -265,5 +277,4 @@ def _wrapper(func, cache, key, lock=None, cond=None, info=None):
265
277
  wrapper.cache_key = key
266
278
  wrapper.cache_lock = lock if lock is not None else cond
267
279
  wrapper.cache_condition = cond
268
-
269
- return functools.update_wrapper(wrapper, func)
280
+ return wrapper
@@ -1,19 +1,45 @@
1
1
  from collections.abc import Callable, Sequence
2
- from typing import Any, Final, Generic, ParamSpec, TypeVar, overload, type_check_only
2
+ from typing import (
3
+ Any,
4
+ Concatenate,
5
+ Final,
6
+ Generic,
7
+ ParamSpec,
8
+ TypeVar,
9
+ overload,
10
+ type_check_only,
11
+ )
3
12
 
4
13
  from . import _CacheInfo
5
14
 
6
15
  __all__: Final = ("fifo_cache", "lfu_cache", "lru_cache", "rr_cache", "ttl_cache")
7
16
 
8
17
  _P = ParamSpec("_P")
9
- _T = TypeVar("_T")
18
+ _Q = ParamSpec("_Q")
10
19
  _R = TypeVar("_R")
20
+ _S = TypeVar("_S")
21
+ _T = TypeVar("_T")
11
22
 
12
23
  @type_check_only
13
24
  class _cachetools_cache_wrapper(Generic[_P, _R]):
14
25
  __wrapped__: Callable[_P, _R]
15
26
  __name__: str
27
+ __qualname__: str
16
28
  __doc__: str | None
29
+ @overload # class access, e.g. Cls.method
30
+ def __get__(
31
+ self, obj: None, objtype: type | None = None
32
+ ) -> _cachetools_cache_wrapper[_P, _R]: ...
33
+ @overload # instance access binds the first parameter
34
+ def __get__(
35
+ self: _cachetools_cache_wrapper[Concatenate[_S, _Q], _R],
36
+ obj: _S,
37
+ objtype: type | None = None,
38
+ ) -> _cachetools_cache_wrapper[_Q, _R]: ...
39
+ @overload # fallback, e.g. when wrapped by @staticmethod or @classmethod
40
+ def __get__(
41
+ self, obj: object, objtype: type | None = None
42
+ ) -> _cachetools_cache_wrapper[_P, _R]: ...
17
43
  def __call__(self, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
18
44
  def cache_info(self) -> _CacheInfo: ...
19
45
  def cache_clear(self) -> None: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.2
3
+ Version: 7.2.3
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>
@@ -154,6 +154,8 @@ class DecoratorTestMixin(SimplifiedTestCaseProtocol):
154
154
  wrapper = cachetools.cached(cache)(self.func)
155
155
 
156
156
  self.assertEqual(wrapper.__wrapped__, self.func)
157
+ self.assertEqual(wrapper.__name__, "func")
158
+ self.assertEqual(wrapper.__qualname__, "DecoratorTestMixin.func")
157
159
 
158
160
  self.assertEqual(len(cache), 0)
159
161
  self.assertEqual(wrapper.__wrapped__(0), 0)
@@ -316,6 +318,21 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
316
318
  self.assertEqual(wrapper.cache_info(), (0, 0, 2, 0))
317
319
  self.assertEqual(lock.count, 13)
318
320
 
321
+ def test_stacked_decorators(self):
322
+ inner_cache = self.cache(2)
323
+ outer_cache = self.cache(2)
324
+ inner_wrapper = cachetools.cached(inner_cache, info=True)(self.func)
325
+ outer_wrapper = cachetools.cached(outer_cache, info=True)(inner_wrapper)
326
+ self.assertIs(outer_wrapper.__wrapped__, inner_wrapper)
327
+ self.assertIs(outer_wrapper.cache, outer_cache)
328
+ self.assertEqual(outer_wrapper(0), 0)
329
+ self.assertEqual(outer_wrapper(0), 0)
330
+ self.assertEqual(outer_wrapper.cache_info(), (1, 1, 2, 1))
331
+ self.assertEqual(inner_wrapper.cache_info(), (0, 1, 2, 1))
332
+ outer_wrapper.cache_clear()
333
+ self.assertEqual(len(outer_cache), 0)
334
+ self.assertEqual(len(inner_cache), 1)
335
+
319
336
  def test_zero_size_cache_decorator(self):
320
337
  cache = self.cache(0)
321
338
  wrapper = cachetools.cached(cache)(self.func)
@@ -386,20 +403,40 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
386
403
 
387
404
  """
388
405
 
406
+ cache = self.cache(2)
407
+
389
408
  class Cached:
409
+ @cachetools.cached(cache, key=cachetools.keys.methodkey)
410
+ def get(self, value: int) -> int:
411
+ return value
412
+
390
413
  @cachetools.cached(self.cache(2), key=cachetools.keys.methodkey, info=True)
391
- def get(self, value):
414
+ def get_info(self, value):
415
+ return value
416
+
417
+ @staticmethod
418
+ @cachetools.cached(self.cache(2))
419
+ def get_static(value: int) -> int:
392
420
  return value
393
421
 
394
422
  cached = Cached()
395
- self.assertEqual(cached.get(1), 1) # type: ignore
396
- self.assertEqual(cached.get(1), 1) # type: ignore
423
+ self.assertEqual(cached.get(1), 1)
397
424
  self.assertEqual(Cached.get(cached, 1), 1)
398
- self.assertEqual(cached.get.cache_info(), (2, 1, 2, 1))
399
- self.assertEqual(Cached.get.cache_info(), (2, 1, 2, 1))
425
+ self.assertEqual(len(cache), 1)
400
426
  cached.get.cache_clear()
401
- self.assertEqual(cached.get.cache_info(), (0, 0, 2, 0))
402
- self.assertEqual(Cached.get.cache_info(), (0, 0, 2, 0))
427
+ self.assertEqual(len(cache), 0)
428
+
429
+ self.assertEqual(cached.get_info(1), 1)
430
+ self.assertEqual(cached.get_info(1), 1)
431
+ self.assertEqual(Cached.get_info(cached, 1), 1)
432
+ self.assertEqual(cached.get_info.cache_info(), (2, 1, 2, 1))
433
+ self.assertEqual(Cached.get_info.cache_info(), (2, 1, 2, 1))
434
+ cached.get_info.cache_clear()
435
+ self.assertEqual(cached.get_info.cache_info(), (0, 0, 2, 0))
436
+ self.assertEqual(Cached.get_info.cache_info(), (0, 0, 2, 0))
437
+
438
+ self.assertEqual(cached.get_static(1), 1)
439
+ self.assertEqual(Cached.get_static(1), 1)
403
440
 
404
441
 
405
442
  class DictWrapperTest(unittest.TestCase, DecoratorTestMixin):
@@ -1,6 +1,3 @@
1
- # TODO: type stub overloads for unbound methods?
2
- # pyright: reportCallIssue=false
3
-
4
1
  import unittest
5
2
  import warnings
6
3
 
@@ -359,6 +356,7 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
359
356
  self.assertEqual(len(cache), 0)
360
357
  self.assertEqual(cached.get.__wrapped__(cached, 0), 0)
361
358
  self.assertEqual(cached.get.__name__, "get")
359
+ self.assertEqual(cached.get.__qualname__, "Cached.get")
362
360
  self.assertEqual((cached.get.__doc__ or "").strip(), "docstring")
363
361
  self.assertEqual(len(cache), 0)
364
362
  self.assertEqual(cached.get(0), 1)
@@ -636,7 +634,7 @@ class CacheMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
636
634
 
637
635
  def test_undecorated_override(self):
638
636
  class Child(Cached):
639
- def get_info(self, value): # type: ignore[reportIncompatibleVariableOverride]
637
+ def get_info(self, value): # type: ignore
640
638
  return super().get_info(value) + 1
641
639
 
642
640
  cache = self.cache(2)
@@ -787,6 +785,8 @@ class UnboundMethodTest(unittest.TestCase):
787
785
  def test_wrapped_method(self):
788
786
  cached = Cached(Cache(2))
789
787
  self.assertIs(Cached.get.__wrapped__, cached.get.__wrapped__)
788
+ self.assertEqual(Cached.get.__name__, "get")
789
+ self.assertEqual(Cached.get.__qualname__, "Cached.get")
790
790
  self.assertEqual(Cached.get.__wrapped__(cached, 1), 0)
791
791
  self.assertEqual(Cached.get.__wrapped__(cached, 1), 1)
792
792
  self.assertEqual(len(cached.cache), 0)
@@ -132,10 +132,54 @@ class DecoratorTestMixin(SimplifiedTestCaseProtocol):
132
132
  self.assertEqual(cached.get.cache_info(), (0, 0, 2, 0))
133
133
  self.assertEqual(Cached.get.cache_info(), (0, 0, 2, 0))
134
134
 
135
+ def test_stacked_decorators(self):
136
+ inner_wrapper = self.decorator(maxsize=2)(lambda n: n)
137
+ outer_wrapper = self.decorator(maxsize=3, typed=True)(inner_wrapper)
138
+ self.assertIs(outer_wrapper.__wrapped__, inner_wrapper)
139
+ self.assertEqual(
140
+ inner_wrapper.cache_parameters(), {"maxsize": 2, "typed": False}
141
+ )
142
+ self.assertEqual(
143
+ outer_wrapper.cache_parameters(), {"maxsize": 3, "typed": True}
144
+ )
145
+ self.assertEqual(outer_wrapper(1), 1)
146
+ self.assertEqual(outer_wrapper(1), 1)
147
+ self.assertEqual(outer_wrapper.cache_info(), (1, 1, 3, 1))
148
+ self.assertEqual(inner_wrapper.cache_info(), (0, 1, 2, 1))
149
+ outer_wrapper.cache_clear()
150
+ self.assertEqual(outer_wrapper.cache_info(), (0, 0, 3, 0))
151
+ self.assertEqual(inner_wrapper.cache_info(), (0, 1, 2, 1))
152
+
135
153
 
136
154
  class FIFODecoratorTest(unittest.TestCase, DecoratorTestMixin):
137
155
  DECORATOR = staticmethod(cachetools.func.fifo_cache)
138
156
 
157
+ def test_method_typed(self):
158
+ """Type-checked by pyright via the stub __get__ overloads."""
159
+
160
+ class Cached:
161
+ @cachetools.func.fifo_cache(maxsize=2)
162
+ def get(self, n: int) -> int:
163
+ return n
164
+
165
+ @cachetools.func.fifo_cache
166
+ def get_bare(self, n: int) -> int:
167
+ return n
168
+
169
+ @staticmethod
170
+ @cachetools.func.fifo_cache
171
+ def get_static(n: int) -> int:
172
+ return n
173
+
174
+ cached = Cached()
175
+ result: int = cached.get(1)
176
+ self.assertEqual(result, 1)
177
+ self.assertEqual(Cached.get(cached, 1), 1)
178
+ self.assertEqual(cached.get.cache_info(), (1, 1, 2, 1))
179
+ self.assertEqual(cached.get_bare(1), 1)
180
+ self.assertEqual(cached.get_static(1), 1)
181
+ self.assertEqual(Cached.get_static(1), 1)
182
+
139
183
 
140
184
  class LFUDecoratorTest(unittest.TestCase, DecoratorTestMixin):
141
185
  DECORATOR = staticmethod(cachetools.func.lfu_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
File without changes
File without changes
File without changes