cachetools 7.2.2__tar.gz → 7.2.4__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.4}/CHANGELOG.rst +18 -0
  2. {cachetools-7.2.2/src/cachetools.egg-info → cachetools-7.2.4}/PKG-INFO +1 -1
  3. {cachetools-7.2.2 → cachetools-7.2.4}/pyproject.toml +1 -0
  4. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/__init__.py +1 -1
  5. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/__init__.pyi +37 -1
  6. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/_cached.py +13 -2
  7. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/_descriptor.py +22 -7
  8. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/func.pyi +28 -2
  9. {cachetools-7.2.2 → cachetools-7.2.4/src/cachetools.egg-info}/PKG-INFO +1 -1
  10. {cachetools-7.2.2 → cachetools-7.2.4}/tests/__init__.py +40 -36
  11. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_cached.py +44 -7
  12. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_cachedmethod.py +29 -4
  13. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_func.py +44 -0
  14. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_tlru.py +13 -0
  15. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_ttl.py +11 -0
  16. {cachetools-7.2.2 → cachetools-7.2.4}/tox.ini +1 -1
  17. {cachetools-7.2.2 → cachetools-7.2.4}/LICENSE +0 -0
  18. {cachetools-7.2.2 → cachetools-7.2.4}/MANIFEST.in +0 -0
  19. {cachetools-7.2.2 → cachetools-7.2.4}/README.rst +0 -0
  20. {cachetools-7.2.2 → cachetools-7.2.4}/docs/conf.py +0 -0
  21. {cachetools-7.2.2 → cachetools-7.2.4}/docs/index.rst +0 -0
  22. {cachetools-7.2.2 → cachetools-7.2.4}/setup.cfg +0 -0
  23. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/_cachedmethod.py +0 -0
  24. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/func.py +0 -0
  25. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/keys.py +0 -0
  26. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/keys.pyi +0 -0
  27. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools/py.typed +0 -0
  28. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools.egg-info/SOURCES.txt +0 -0
  29. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools.egg-info/dependency_links.txt +0 -0
  30. {cachetools-7.2.2 → cachetools-7.2.4}/src/cachetools.egg-info/top_level.txt +0 -0
  31. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_cache.py +0 -0
  32. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_classmethod.py +0 -0
  33. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_fifo.py +0 -0
  34. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_keys.py +0 -0
  35. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_lfu.py +0 -0
  36. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_lru.py +0 -0
  37. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_rr.py +0 -0
  38. {cachetools-7.2.2 → cachetools-7.2.4}/tests/test_threading.py +0 -0
@@ -1,3 +1,21 @@
1
+ v7.2.4 (2026-10-19)
2
+ ===================
3
+
4
+ - Restore autospec calls to ``@cachedmethod`` decorated methods.
5
+
6
+ - Improve branch coverage of unit tests.
7
+
8
+ - Minor type stub improvements.
9
+
10
+
11
+ v7.2.3 (2026-10-17)
12
+ ===================
13
+
14
+ - Fix ``@cached`` decorator stacking behavior.
15
+
16
+ - Minor type stub improvements.
17
+
18
+
1
19
  v7.2.2 (2026-10-15)
2
20
  ===================
3
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.2
3
+ Version: 7.2.4
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.4"
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")
@@ -141,7 +143,8 @@ class _CacheInfo(NamedTuple):
141
143
 
142
144
  @type_check_only
143
145
  class _AbstractCondition(AbstractContextManager[Any], Protocol):
144
- # implementation and unit tests do not use plain wait() and notify()
146
+ # implementation and unit tests currently do not use plain wait()
147
+ # and notify(); these are reserved for future use
145
148
  def wait(self, timeout: float | None = None) -> bool: ...
146
149
  def wait_for(
147
150
  self, predicate: Callable[[], _T], timeout: float | None = None
@@ -153,16 +156,47 @@ class _AbstractCondition(AbstractContextManager[Any], Protocol):
153
156
  class _cached_wrapper(Generic[_P, _R]):
154
157
  __wrapped__: Callable[_P, _R]
155
158
  __name__: str
159
+ __qualname__: str
156
160
  __doc__: str | None
157
161
  cache: MutableMapping[Any, Any] | None
158
162
  cache_key: Callable[..., Any] = ...
159
163
  cache_lock: AbstractContextManager[Any] | None = None
160
164
  cache_condition: _AbstractCondition | None = None
165
+ @overload # class access, e.g. Cls.method
166
+ def __get__(
167
+ self, obj: None, objtype: type | None = None
168
+ ) -> _cached_wrapper[_P, _R]: ...
169
+ @overload # instance access binds the first parameter
170
+ def __get__(
171
+ self: _cached_wrapper[Concatenate[_S, _Q], _R],
172
+ obj: _S,
173
+ objtype: type | None = None,
174
+ ) -> _cached_wrapper[_Q, _R]: ...
175
+ @overload # fallback, e.g. when wrapped by @staticmethod or @classmethod
176
+ def __get__(
177
+ self, obj: object, objtype: type | None = None
178
+ ) -> _cached_wrapper[_P, _R]: ...
161
179
  def __call__(self, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
162
180
  def cache_clear(self) -> None: ...
163
181
 
164
182
  @type_check_only
165
183
  class _cached_wrapper_info(_cached_wrapper[_P, _R]):
184
+ @overload
185
+ def __get__(
186
+ self, obj: None, objtype: type | None = None
187
+ ) -> _cached_wrapper_info[_P, _R]: ...
188
+ @overload
189
+ def __get__(
190
+ self: _cached_wrapper_info[Concatenate[_S, _Q], _R],
191
+ obj: _S,
192
+ objtype: type | None = None,
193
+ ) -> _cached_wrapper_info[_Q, _R]: ...
194
+ # pyright cannot match the self-typed base overload; each return type is
195
+ # the _cached_wrapper_info subtype of the corresponding base return type
196
+ @overload
197
+ def __get__( # pyright: ignore[reportIncompatibleMethodOverride]
198
+ self, obj: object, objtype: type | None = None
199
+ ) -> _cached_wrapper_info[_P, _R]: ...
166
200
  def cache_info(self) -> _CacheInfo: ...
167
201
 
168
202
  @overload
@@ -195,6 +229,7 @@ def cached(
195
229
  class _cachedmethod_unbound(Generic[_P, _R]):
196
230
  __wrapped__: Callable[Concatenate[Any, _P], _R]
197
231
  __name__: str
232
+ __qualname__: str
198
233
  __doc__: str | None
199
234
  @overload
200
235
  def __call__(self, obj: Any, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
@@ -206,6 +241,7 @@ class _cachedmethod_unbound(Generic[_P, _R]):
206
241
  class _cachedmethod_wrapper(Generic[_P, _R]):
207
242
  __wrapped__: Callable[Concatenate[Any, _P], _R]
208
243
  __name__: str
244
+ __qualname__: str
209
245
  __doc__: str | None
210
246
  cache: MutableMapping[Any, Any]
211
247
  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
@@ -96,15 +96,30 @@ class _MethodDescriptor:
96
96
  # Created once and shared by all class accesses; objtype is not
97
97
  # used for instance lookups, so it need not be bound here.
98
98
  if self.__unbound is None:
99
- descriptor = self
99
+ self.__unbound = _Unbound(self)
100
+ return self.__unbound
100
101
 
101
- # As in the previous function wrapper, the receiver is named self.
102
- @functools.wraps(self.__wrapped__) # type: ignore
103
- def unbound(self, *args, **kwargs):
104
- return descriptor.__get__(self)(*args, **kwargs)
105
102
 
106
- self.__unbound = unbound # type: ignore
107
- return self.__unbound
103
+ class _Unbound:
104
+ """Callable returned on class access.
105
+
106
+ Deliberately not a function: unittest.mock.create_autospec() only strips
107
+ the receiver from plain functions found in the class dict, so a function
108
+ here would leave the receiver in an autospec'd instance's signature.
109
+ """
110
+
111
+ def __init__(self, descriptor):
112
+ functools.update_wrapper(self, descriptor.__wrapped__)
113
+ self.__descriptor = descriptor
114
+
115
+ # Receiver is taken from *args so autospec'd instances accept any call,
116
+ # as they did before class access returned a callable.
117
+ def __call__(self, /, *args, **kwargs):
118
+ if not args:
119
+ msg = "Cannot call @cachedmethod through the class without an instance"
120
+ raise TypeError(msg)
121
+ obj, *args = args
122
+ return self.__descriptor.__get__(obj)(*args, **kwargs)
108
123
 
109
124
 
110
125
  class _ClassMethodDescriptor(_MethodDescriptor):
@@ -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.4
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>
@@ -5,6 +5,39 @@ from typing import Any, Protocol
5
5
  import cachetools
6
6
 
7
7
 
8
+ class CountedLock:
9
+ def __init__(self):
10
+ self.count = 0
11
+
12
+ def __enter__(self):
13
+ self.count += 1
14
+
15
+ def __exit__(self, *exc):
16
+ pass
17
+
18
+
19
+ class CountedCondition(CountedLock):
20
+ def __init__(self):
21
+ super().__init__()
22
+ self.wait_count = 0
23
+ self.notify_count = 0
24
+
25
+ def wait(self, timeout=None):
26
+ self.wait_count += 1
27
+ return True
28
+
29
+ def wait_for(self, predicate, timeout=None):
30
+ res = predicate()
31
+ self.wait_count += 1
32
+ return res
33
+
34
+ def notify(self, n=1):
35
+ self.notify_count += 1
36
+
37
+ def notify_all(self):
38
+ self.notify_count += 1
39
+
40
+
8
41
  class SimplifiedTestCaseProtocol(Protocol):
9
42
  def assertEqual(self, first: Any, second: Any, msg: Any = None) -> None: ...
10
43
  def assertNotEqual(self, first: Any, second: Any, msg: Any = None) -> None: ...
@@ -323,6 +356,13 @@ class CacheTestMixin(SimplifiedTestCaseProtocol):
323
356
  self.assertEqual(3, cache[3])
324
357
  self.assertEqual(7, cache[4])
325
358
 
359
+ # growing an existing entry may have to evict other entries
360
+ cache[4] = 9
361
+ self.assertEqual(1, len(cache))
362
+ self.assertEqual(9, cache.currsize)
363
+ self.assertNotIn(3, cache)
364
+ self.assertEqual(9, cache[4])
365
+
326
366
  def test_getsizeof_replace_same_size(self):
327
367
  cache = self.Cache(maxsize=10, getsizeof=lambda x: x)
328
368
 
@@ -453,39 +493,3 @@ class CacheTestMixin(SimplifiedTestCaseProtocol):
453
493
  cache = pickle.loads(pickle.dumps(source))
454
494
  self.assertEqual(n, len(cache))
455
495
  self.assertEqual(source, cache)
456
-
457
-
458
- class CountedLock:
459
- def __init__(self):
460
- self.count = 0
461
-
462
- def __enter__(self):
463
- self.count += 1
464
-
465
- def __exit__(self, *exc):
466
- pass
467
-
468
-
469
- class CountedCondition(CountedLock):
470
- def __init__(self):
471
- super().__init__()
472
- self.wait_count = 0
473
- self.notify_count = 0
474
-
475
- # only wait_for() and notify_all() are currently used, but wait()
476
- # and notify() are also required according to the docs...
477
-
478
- def wait(self, timeout=None):
479
- self.wait_count += 1
480
- return True
481
-
482
- def wait_for(self, predicate, timeout=None):
483
- res = predicate()
484
- self.wait_count += 1
485
- return res
486
-
487
- def notify(self, n=1):
488
- self.notify_count += 1
489
-
490
- def notify_all(self):
491
- self.notify_count += 1
@@ -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)
@@ -502,6 +500,17 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
502
500
 
503
501
  bar = foo
504
502
 
503
+ def test_decorator_same_name(self):
504
+ # assigning to a second owner under the same name is accepted
505
+ class Other(Cached):
506
+ get = Cached.__dict__["get"]
507
+
508
+ other = Other(self.cache(2))
509
+
510
+ self.assertEqual(other.get(0), 0)
511
+ self.assertEqual(other.get(0), 0)
512
+ self.assertEqual(other.get(1), 1)
513
+
505
514
  def test_decorator_no_set_name(self):
506
515
  class Cached:
507
516
  def foo(_self):
@@ -636,7 +645,7 @@ class CacheMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
636
645
 
637
646
  def test_undecorated_override(self):
638
647
  class Child(Cached):
639
- def get_info(self, value): # type: ignore[reportIncompatibleVariableOverride]
648
+ def get_info(self, value): # type: ignore
640
649
  return super().get_info(value) + 1
641
650
 
642
651
  cache = self.cache(2)
@@ -778,6 +787,10 @@ class UnboundMethodTest(unittest.TestCase):
778
787
  self.assertEqual(cached.get(1), 0)
779
788
  self.assertEqual(cached.count, 1)
780
789
 
790
+ def test_class_call_without_instance(self):
791
+ with self.assertRaises(TypeError):
792
+ Cached.get() # type: ignore
793
+
781
794
  def test_class_access_identity(self):
782
795
  self.assertIs(Cached.get, Cached.get)
783
796
  self.assertIs(Cached.get_info, Cached.get_info)
@@ -787,6 +800,8 @@ class UnboundMethodTest(unittest.TestCase):
787
800
  def test_wrapped_method(self):
788
801
  cached = Cached(Cache(2))
789
802
  self.assertIs(Cached.get.__wrapped__, cached.get.__wrapped__)
803
+ self.assertEqual(Cached.get.__name__, "get")
804
+ self.assertEqual(Cached.get.__qualname__, "Cached.get")
790
805
  self.assertEqual(Cached.get.__wrapped__(cached, 1), 0)
791
806
  self.assertEqual(Cached.get.__wrapped__(cached, 1), 1)
792
807
  self.assertEqual(len(cached.cache), 0)
@@ -798,3 +813,13 @@ class UnboundMethodTest(unittest.TestCase):
798
813
  warnings.simplefilter("always")
799
814
  unittest.mock.create_autospec(Cached, instance=True)
800
815
  self.assertEqual(len(w), 0)
816
+
817
+ def test_autospec_call(self):
818
+ import unittest.mock
819
+
820
+ mock = unittest.mock.create_autospec(Cached, instance=True)
821
+ mock.get.return_value = 42
822
+ self.assertEqual(mock.get(1), 42)
823
+ mock.get.assert_called_with(1)
824
+ self.assertEqual(mock.get(value=1), 42)
825
+ mock.get.assert_called_with(value=1)
@@ -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)
@@ -217,6 +217,19 @@ class TLRUCacheTest(unittest.TestCase, CacheTestMixin):
217
217
  self.assertNotIn(2, cache)
218
218
  self.assertNotIn(3, cache)
219
219
 
220
+ def test_ttu_iter_expired(self):
221
+ cache = TLRUCache[int, int, int](
222
+ maxsize=2, ttu=lambda _k, _v, t: t + 2, timer=Timer()
223
+ )
224
+ cache[1] = 1
225
+ cache.timer.tick()
226
+ cache[2] = 2
227
+ cache.timer.tick()
228
+ # a bare for loop skips expired items without removing them
229
+ self.assertEqual([2], [key for key in cache])
230
+ self.assertEqual({(1, 1)}, set(cache.expire()))
231
+ self.assertEqual([2], [key for key in cache])
232
+
220
233
  def test_ttu_expired(self):
221
234
  cache = TLRUCache[int, None, int](
222
235
  maxsize=1, ttu=lambda k, _, t: t + k, timer=Timer()
@@ -177,6 +177,17 @@ class TTLCacheTest(unittest.TestCase, CacheTestMixin):
177
177
  self.assertNotIn(2, cache)
178
178
  self.assertNotIn(3, cache)
179
179
 
180
+ def test_ttl_iter_expired(self):
181
+ cache = TTLCache[int, int, int](maxsize=2, ttl=2, timer=Timer())
182
+ cache[1] = 1
183
+ cache.timer.tick()
184
+ cache[2] = 2
185
+ cache.timer.tick()
186
+ # a bare for loop skips expired items without removing them
187
+ self.assertEqual([2], [key for key in cache])
188
+ self.assertEqual({(1, 1)}, set(cache.expire()))
189
+ self.assertEqual([2], [key for key in cache])
190
+
180
191
  def test_ttl_atomic(self):
181
192
  cache = TTLCache[int, int, int](maxsize=1, ttl=2, timer=Timer(auto=True))
182
193
  cache[1] = 1
@@ -6,7 +6,7 @@ deps =
6
6
  pytest
7
7
  pytest-cov
8
8
  commands =
9
- py.test --basetemp={envtmpdir} --cov=cachetools --cov-report term-missing {posargs}
9
+ py.test --basetemp={envtmpdir} --cov=cachetools --cov-branch --cov-report term-missing {posargs}
10
10
 
11
11
  [testenv:docs]
12
12
  deps =
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