cachetools 7.2.1__tar.gz → 7.2.2__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 (39) hide show
  1. {cachetools-7.2.1 → cachetools-7.2.2}/CHANGELOG.rst +15 -0
  2. {cachetools-7.2.1/src/cachetools.egg-info → cachetools-7.2.2}/PKG-INFO +1 -1
  3. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/__init__.py +3 -2
  4. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/__init__.pyi +21 -0
  5. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/_cachedmethod.py +7 -98
  6. cachetools-7.2.2/src/cachetools/_descriptor.py +126 -0
  7. {cachetools-7.2.1 → cachetools-7.2.2/src/cachetools.egg-info}/PKG-INFO +1 -1
  8. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools.egg-info/SOURCES.txt +1 -0
  9. {cachetools-7.2.1 → cachetools-7.2.2}/tests/__init__.py +2 -2
  10. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_cached.py +23 -2
  11. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_cachedmethod.py +112 -4
  12. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_func.py +25 -2
  13. cachetools-7.2.2/tests/test_threading.py +103 -0
  14. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_ttl.py +16 -0
  15. cachetools-7.2.1/tests/test_threading.py +0 -122
  16. {cachetools-7.2.1 → cachetools-7.2.2}/LICENSE +0 -0
  17. {cachetools-7.2.1 → cachetools-7.2.2}/MANIFEST.in +0 -0
  18. {cachetools-7.2.1 → cachetools-7.2.2}/README.rst +0 -0
  19. {cachetools-7.2.1 → cachetools-7.2.2}/docs/conf.py +0 -0
  20. {cachetools-7.2.1 → cachetools-7.2.2}/docs/index.rst +0 -0
  21. {cachetools-7.2.1 → cachetools-7.2.2}/pyproject.toml +0 -0
  22. {cachetools-7.2.1 → cachetools-7.2.2}/setup.cfg +0 -0
  23. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/_cached.py +0 -0
  24. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/func.py +0 -0
  25. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/func.pyi +0 -0
  26. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/keys.py +0 -0
  27. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/keys.pyi +0 -0
  28. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools/py.typed +0 -0
  29. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools.egg-info/dependency_links.txt +0 -0
  30. {cachetools-7.2.1 → cachetools-7.2.2}/src/cachetools.egg-info/top_level.txt +0 -0
  31. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_cache.py +0 -0
  32. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_classmethod.py +0 -0
  33. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_fifo.py +0 -0
  34. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_keys.py +0 -0
  35. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_lfu.py +0 -0
  36. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_lru.py +0 -0
  37. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_rr.py +0 -0
  38. {cachetools-7.2.1 → cachetools-7.2.2}/tests/test_tlru.py +0 -0
  39. {cachetools-7.2.1 → cachetools-7.2.2}/tox.ini +0 -0
@@ -1,3 +1,18 @@
1
+ v7.2.2 (2026-10-15)
2
+ ===================
3
+
4
+ - Fix ``@cachedmethod`` for subclasses that override a decorated
5
+ method and call the base class implementation via ``super()``.
6
+
7
+ - Restore class access to instance methods decorated with
8
+ ``@cachedmethod``.
9
+
10
+ - Improve ``TTLCache`` robustness when calculating expiration time
11
+ fails.
12
+
13
+ - Various unit test improvements.
14
+
15
+
1
16
  v7.2.1 (2026-10-05)
2
17
  ===================
3
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.1
3
+ Version: 7.2.2
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>
@@ -12,7 +12,7 @@ __all__ = (
12
12
  "cachedmethod",
13
13
  )
14
14
 
15
- __version__ = "7.2.1"
15
+ __version__ = "7.2.2"
16
16
 
17
17
  import collections
18
18
  import collections.abc
@@ -495,6 +495,7 @@ class TTLCache(_TimedCache):
495
495
 
496
496
  def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
497
497
  with self.timer as time:
498
+ expires = time + self.__ttl
498
499
  self.expire(time)
499
500
  cache_setitem(self, key, value)
500
501
  try:
@@ -503,7 +504,7 @@ class TTLCache(_TimedCache):
503
504
  self.__links[key] = link = TTLCache._Link(key)
504
505
  else:
505
506
  link.unlink()
506
- link.expires = time + self.__ttl
507
+ link.expires = expires
507
508
  link.next = root = self.__root
508
509
  link.prev = prev = root.prev
509
510
  prev.next = root.prev = link
@@ -191,6 +191,17 @@ def cached(
191
191
  info: Literal[True],
192
192
  ) -> Callable[[Callable[_P, _R]], _cached_wrapper_info[_P, _R]]: ...
193
193
 
194
+ @type_check_only
195
+ class _cachedmethod_unbound(Generic[_P, _R]):
196
+ __wrapped__: Callable[Concatenate[Any, _P], _R]
197
+ __name__: str
198
+ __doc__: str | None
199
+ @overload
200
+ def __call__(self, obj: Any, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
201
+ # deprecated @classmethod use
202
+ @overload
203
+ def __call__(self, /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
204
+
194
205
  @type_check_only
195
206
  class _cachedmethod_wrapper(Generic[_P, _R]):
196
207
  __wrapped__: Callable[Concatenate[Any, _P], _R]
@@ -201,6 +212,11 @@ class _cachedmethod_wrapper(Generic[_P, _R]):
201
212
  cache_lock: AbstractContextManager[Any] | None = None
202
213
  cache_condition: _AbstractCondition | None = None
203
214
  def __set_name__(self, owner: type, name: str) -> None: ...
215
+ @overload
216
+ def __get__(
217
+ self, obj: None, objtype: type | None = None
218
+ ) -> _cachedmethod_unbound[_P, _R]: ...
219
+ @overload
204
220
  def __get__(
205
221
  self, obj: Any, objtype: type | None = None
206
222
  ) -> _cachedmethod_wrapper[_P, _R]: ...
@@ -209,6 +225,11 @@ class _cachedmethod_wrapper(Generic[_P, _R]):
209
225
 
210
226
  @type_check_only
211
227
  class _cachedmethod_wrapper_info(_cachedmethod_wrapper[_P, _R]):
228
+ @overload
229
+ def __get__(
230
+ self, obj: None, objtype: type | None = None
231
+ ) -> _cachedmethod_unbound[_P, _R]: ...
232
+ @overload
212
233
  def __get__(
213
234
  self, obj: Any, objtype: type | None = None
214
235
  ) -> _cachedmethod_wrapper_info[_P, _R]: ...
@@ -5,24 +5,9 @@
5
5
  __all__ = ()
6
6
 
7
7
  import functools
8
- import warnings
9
8
  import weakref
10
9
 
11
-
12
- def _warn_classmethod(stacklevel):
13
- warnings.warn(
14
- "decorating class methods with @cachedmethod is deprecated",
15
- DeprecationWarning,
16
- stacklevel=stacklevel,
17
- )
18
-
19
-
20
- def _warn_instance_dict(msg, stacklevel):
21
- warnings.warn(
22
- msg,
23
- DeprecationWarning,
24
- stacklevel=stacklevel,
25
- )
10
+ from ._descriptor import _ClassMethodDescriptor, _MethodDescriptor, _warn_classmethod
26
11
 
27
12
 
28
13
  def _none(_):
@@ -65,82 +50,6 @@ class _WrapperBase:
65
50
  return self.__cond(self._obj)
66
51
 
67
52
 
68
- class _DescriptorBase:
69
- """Descriptor base class implementing the basic descriptor protocol."""
70
-
71
- def __init__(self, deprecated=False):
72
- self.__attrname = None
73
- self.__deprecated = deprecated
74
-
75
- def __set_name__(self, owner, name):
76
- if self.__attrname is None:
77
- self.__attrname = name
78
- elif name != self.__attrname:
79
- raise TypeError(
80
- "Cannot assign the same @cachedmethod to two different names "
81
- f"({self.__attrname!r} and {name!r})."
82
- )
83
-
84
- def __get__(self, obj, objtype=None):
85
- wrapper = self.Wrapper(obj) # type: ignore
86
- if obj is None:
87
- # Return the wrapper itself without modification when accessed
88
- # through the class to support class-level introspection, such
89
- # as for mocking with autospec=True in unittest.mock.
90
- pass
91
- elif self.__attrname is not None:
92
- # replace descriptor instance with wrapper in instance dict
93
- try:
94
- # In case of a race condition where another thread already replaced
95
- # the descriptor, prefer the initial wrapper.
96
- wrapper = obj.__dict__.setdefault(self.__attrname, wrapper)
97
- except AttributeError:
98
- # not all objects have __dict__ (e.g. class defines slots)
99
- msg = (
100
- f"No '__dict__' attribute on {type(obj).__name__!r} "
101
- f"instance to cache {self.__attrname!r} property."
102
- )
103
- if self.__deprecated:
104
- _warn_instance_dict(msg, 3)
105
- else:
106
- raise TypeError(msg) from None
107
- except TypeError:
108
- msg = (
109
- f"The '__dict__' attribute on {type(obj).__name__!r} "
110
- f"instance does not support item assignment for "
111
- f"caching {self.__attrname!r} property."
112
- )
113
- if self.__deprecated:
114
- _warn_instance_dict(msg, 3)
115
- else:
116
- raise TypeError(msg) from None
117
- elif self.__deprecated:
118
- pass # deprecated @classmethod, warning already raised elsewhere
119
- else:
120
- msg = "Cannot use @cachedmethod instance without calling __set_name__ on it"
121
- raise TypeError(msg) from None
122
- return wrapper
123
-
124
-
125
- class _DeprecatedDescriptorBase(_DescriptorBase):
126
- """Descriptor base class supporting deprecated @classmethod use."""
127
-
128
- def __init__(self, wrapper, cache_clear):
129
- super().__init__(deprecated=True)
130
- self.__wrapper = wrapper
131
- self.__cache_clear = cache_clear
132
-
133
- # called for @classmethod with Python >= 3.13
134
- def __call__(self, *args, **kwargs):
135
- _warn_classmethod(stacklevel=3)
136
- return self.__wrapper(*args, **kwargs)
137
-
138
- # backward-compatible @classmethod handling with Python >= 3.13
139
- def cache_clear(self, objtype):
140
- _warn_classmethod(stacklevel=3)
141
- return self.__cache_clear(objtype)
142
-
143
-
144
53
  # At least for now, the implementation prefers clarity and performance
145
54
  # over ease of maintenance, thus providing separate descriptors for
146
55
  # all valid combinations of decorator parameters lock, condition and
@@ -148,7 +57,7 @@ class _DeprecatedDescriptorBase(_DescriptorBase):
148
57
 
149
58
 
150
59
  def _condition_info(method, cache, key, lock, cond, info):
151
- class Descriptor(_DescriptorBase):
60
+ class Descriptor(_MethodDescriptor):
152
61
  class Wrapper(_WrapperBase):
153
62
  def __init__(self, obj):
154
63
  super().__init__(obj, method, cache, key, lock, cond)
@@ -196,7 +105,7 @@ def _condition_info(method, cache, key, lock, cond, info):
196
105
 
197
106
 
198
107
  def _locked_info(method, cache, key, lock, info):
199
- class Descriptor(_DescriptorBase):
108
+ class Descriptor(_MethodDescriptor):
200
109
  class Wrapper(_WrapperBase):
201
110
  def __init__(self, obj):
202
111
  super().__init__(obj, method, cache, key, lock)
@@ -236,7 +145,7 @@ def _locked_info(method, cache, key, lock, info):
236
145
 
237
146
 
238
147
  def _unlocked_info(method, cache, key, info):
239
- class Descriptor(_DescriptorBase):
148
+ class Descriptor(_MethodDescriptor):
240
149
  class Wrapper(_WrapperBase):
241
150
  def __init__(self, obj):
242
151
  super().__init__(obj, method, cache, key)
@@ -303,7 +212,7 @@ def _condition(method, cache, key, lock, cond):
303
212
  p = pending.setdefault(self, set())
304
213
  return wrapper(self, p, *args, **kwargs)
305
214
 
306
- class Descriptor(_DeprecatedDescriptorBase):
215
+ class Descriptor(_ClassMethodDescriptor):
307
216
  class Wrapper(_WrapperBase):
308
217
  def __init__(self, obj):
309
218
  super().__init__(obj, method, cache, key, lock, cond)
@@ -343,7 +252,7 @@ def _locked(method, cache, key, lock):
343
252
  with lock(self):
344
253
  c.clear()
345
254
 
346
- class Descriptor(_DeprecatedDescriptorBase):
255
+ class Descriptor(_ClassMethodDescriptor):
347
256
  class Wrapper(_WrapperBase):
348
257
  def __init__(self, obj):
349
258
  super().__init__(obj, method, cache, key, lock)
@@ -377,7 +286,7 @@ def _unlocked(method, cache, key):
377
286
  c = cache(self)
378
287
  c.clear()
379
288
 
380
- class Descriptor(_DeprecatedDescriptorBase):
289
+ class Descriptor(_ClassMethodDescriptor):
381
290
  class Wrapper(_WrapperBase):
382
291
  def __init__(self, obj):
383
292
  super().__init__(obj, method, cache, key)
@@ -0,0 +1,126 @@
1
+ """Descriptor helpers."""
2
+
3
+ __all__ = ()
4
+
5
+ import functools
6
+ import warnings
7
+
8
+
9
+ def _warn_classmethod(stacklevel):
10
+ warnings.warn(
11
+ "decorating class methods with @cachedmethod is deprecated",
12
+ DeprecationWarning,
13
+ stacklevel=stacklevel,
14
+ )
15
+
16
+
17
+ def _warn_instance_dict(msg, stacklevel):
18
+ warnings.warn(
19
+ msg,
20
+ DeprecationWarning,
21
+ stacklevel=stacklevel,
22
+ )
23
+
24
+
25
+ class _MethodDescriptor:
26
+ """Descriptor class implementing the basic descriptor protocol."""
27
+
28
+ def __init__(self, deprecated=False):
29
+ self.__attrname = None
30
+ self.__deprecated = deprecated
31
+ self.__unbound = None
32
+
33
+ def __set_name__(self, owner, name):
34
+ if self.__attrname is None:
35
+ self.__attrname = name
36
+ elif name != self.__attrname:
37
+ raise TypeError(
38
+ "Cannot assign the same @cachedmethod to two different names "
39
+ f"({self.__attrname!r} and {name!r})."
40
+ )
41
+
42
+ def __get__(self, obj, objtype=None):
43
+ if obj is None:
44
+ # objtype is not used for instance lookups, so it need not be bound here
45
+ return self.__get_unbound()
46
+ elif self.__attrname is not None:
47
+ # replace descriptor instance with wrapper in instance dict
48
+ wrapper = self.Wrapper(obj) # type: ignore
49
+ try:
50
+ descriptor = next(
51
+ (
52
+ cls.__dict__[self.__attrname]
53
+ for cls in type(obj).__mro__
54
+ if self.__attrname in cls.__dict__
55
+ ),
56
+ None,
57
+ )
58
+ if descriptor is self:
59
+ # In case of a race condition where another thread already replaced
60
+ # the descriptor, prefer the initial wrapper.
61
+ wrapper = obj.__dict__.setdefault(self.__attrname, wrapper)
62
+ else:
63
+ # A super() lookup must neither reuse the override's wrapper
64
+ # nor replace it, so preserve state separately for each descriptor.
65
+ wrappers = obj.__dict__.setdefault("__cachetools_cachedmethods", {})
66
+ wrapper = wrappers.setdefault(self, wrapper)
67
+ except AttributeError:
68
+ # not all objects have __dict__ (e.g. class defines slots)
69
+ msg = (
70
+ f"No '__dict__' attribute on {type(obj).__name__!r} "
71
+ f"instance to cache {self.__attrname!r} property."
72
+ )
73
+ if self.__deprecated:
74
+ _warn_instance_dict(msg, 3)
75
+ else:
76
+ raise TypeError(msg) from None
77
+ except TypeError:
78
+ msg = (
79
+ f"The '__dict__' attribute on {type(obj).__name__!r} "
80
+ f"instance does not support item assignment for "
81
+ f"caching {self.__attrname!r} property."
82
+ )
83
+ if self.__deprecated:
84
+ _warn_instance_dict(msg, 3)
85
+ else:
86
+ raise TypeError(msg) from None
87
+ elif self.__deprecated:
88
+ # deprecated @classmethod, warning already raised elsewhere
89
+ wrapper = self.Wrapper(obj) # type: ignore
90
+ else:
91
+ msg = "Cannot use @cachedmethod instance without calling __set_name__ on it"
92
+ raise TypeError(msg) from None
93
+ return wrapper
94
+
95
+ def __get_unbound(self):
96
+ # Created once and shared by all class accesses; objtype is not
97
+ # used for instance lookups, so it need not be bound here.
98
+ if self.__unbound is None:
99
+ descriptor = self
100
+
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
+
106
+ self.__unbound = unbound # type: ignore
107
+ return self.__unbound
108
+
109
+
110
+ class _ClassMethodDescriptor(_MethodDescriptor):
111
+ """Descriptor base class supporting deprecated @classmethod use."""
112
+
113
+ def __init__(self, wrapper, cache_clear):
114
+ super().__init__(deprecated=True)
115
+ self.__wrapper = wrapper
116
+ self.__cache_clear = cache_clear
117
+
118
+ # called for @classmethod with Python >= 3.13
119
+ def __call__(self, *args, **kwargs):
120
+ _warn_classmethod(stacklevel=3)
121
+ return self.__wrapper(*args, **kwargs)
122
+
123
+ # backward-compatible @classmethod handling with Python >= 3.13
124
+ def cache_clear(self, objtype):
125
+ _warn_classmethod(stacklevel=3)
126
+ return self.__cache_clear(objtype)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.1
3
+ Version: 7.2.2
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>
@@ -10,6 +10,7 @@ src/cachetools/__init__.py
10
10
  src/cachetools/__init__.pyi
11
11
  src/cachetools/_cached.py
12
12
  src/cachetools/_cachedmethod.py
13
+ src/cachetools/_descriptor.py
13
14
  src/cachetools/func.py
14
15
  src/cachetools/func.pyi
15
16
  src/cachetools/keys.py
@@ -5,7 +5,7 @@ from typing import Any, Protocol
5
5
  import cachetools
6
6
 
7
7
 
8
- class _TestCaseProtocol(Protocol):
8
+ class SimplifiedTestCaseProtocol(Protocol):
9
9
  def assertEqual(self, first: Any, second: Any, msg: Any = None) -> None: ...
10
10
  def assertNotEqual(self, first: Any, second: Any, msg: Any = None) -> None: ...
11
11
  def assertTrue(self, expr: Any, msg: Any = None) -> None: ...
@@ -29,7 +29,7 @@ class _TestCaseProtocol(Protocol):
29
29
  ) -> Any: ...
30
30
 
31
31
 
32
- class CacheTestMixin(_TestCaseProtocol):
32
+ class CacheTestMixin(SimplifiedTestCaseProtocol):
33
33
  Cache: type[cachetools.Cache]
34
34
 
35
35
  def test_defaults(self):
@@ -4,10 +4,10 @@ import warnings
4
4
  import cachetools
5
5
  import cachetools.keys
6
6
 
7
- from . import CountedCondition, CountedLock, _TestCaseProtocol
7
+ from . import CountedCondition, CountedLock, SimplifiedTestCaseProtocol
8
8
 
9
9
 
10
- class DecoratorTestMixin(_TestCaseProtocol):
10
+ class DecoratorTestMixin(SimplifiedTestCaseProtocol):
11
11
  def cache(self, minsize):
12
12
  raise NotImplementedError
13
13
 
@@ -380,6 +380,27 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
380
380
  self.assertEqual(wrapper.cache_info(), (0, 1, 0, 0))
381
381
  self.assertEqual(lock.count, 5)
382
382
 
383
+ def test_decorator_method(self):
384
+ """Though it is not encouraged, like functools.lru_cache, the
385
+ @cached decorator may be used on methods, too.
386
+
387
+ """
388
+
389
+ class Cached:
390
+ @cachetools.cached(self.cache(2), key=cachetools.keys.methodkey, info=True)
391
+ def get(self, value):
392
+ return value
393
+
394
+ cached = Cached()
395
+ self.assertEqual(cached.get(1), 1) # type: ignore
396
+ self.assertEqual(cached.get(1), 1) # type: ignore
397
+ 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))
400
+ 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))
403
+
383
404
 
384
405
  class DictWrapperTest(unittest.TestCase, DecoratorTestMixin):
385
406
  def cache(self, minsize):
@@ -1,10 +1,12 @@
1
+ # TODO: type stub overloads for unbound methods?
2
+ # pyright: reportCallIssue=false
3
+
1
4
  import unittest
2
- import unittest.mock
3
5
  import warnings
4
6
 
5
7
  from cachetools import Cache, cachedmethod, keys
6
8
 
7
- from . import CountedCondition, CountedLock, _TestCaseProtocol
9
+ from . import CountedCondition, CountedLock, SimplifiedTestCaseProtocol
8
10
 
9
11
 
10
12
  class Cached:
@@ -77,7 +79,7 @@ class Unhashable(Cached):
77
79
  raise TypeError("unhashable type")
78
80
 
79
81
 
80
- class MethodDecoratorTestMixin(_TestCaseProtocol):
82
+ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
81
83
  def cache(self, _minsize, **_kwargs):
82
84
  raise NotImplementedError
83
85
 
@@ -632,6 +634,69 @@ class CacheMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
632
634
  self.assertEqual(cached.get(2), 2)
633
635
  self.assertNotIn(2, cache.values())
634
636
 
637
+ def test_undecorated_override(self):
638
+ class Child(Cached):
639
+ def get_info(self, value): # type: ignore[reportIncompatibleVariableOverride]
640
+ return super().get_info(value) + 1
641
+
642
+ cache = self.cache(2)
643
+ cached = Child(cache)
644
+ self.assertEqual(cached.get_info(0), 1)
645
+ self.assertEqual(cached.get_info(0), 1)
646
+ self.assertEqual(super(Child, cached).get_info(0), 0)
647
+ self.assertEqual(cached.get_info(1), 2)
648
+ self.assertEqual(super(Child, cached).get_info(1), 1)
649
+ self.assertNotIn("get_info", vars(cached))
650
+ self.assertEqual(super(Child, cached).get_info.cache_info(), (3, 2, 2, 2))
651
+
652
+ def test_decorated_override(self):
653
+ class Child(Cached):
654
+ def __init__(self, base_cache, child_cache):
655
+ super().__init__(base_cache)
656
+ self.child_cache = child_cache
657
+
658
+ @cachedmethod(lambda self: self.child_cache, info=True)
659
+ def get_info(self, value):
660
+ return super().get_info(value) + 1
661
+
662
+ cached = Child(self.cache(2), self.cache(2))
663
+ self.assertEqual(cached.get_info(0), 1)
664
+ self.assertEqual(cached.get_info(0), 1)
665
+ self.assertEqual(super(Child, cached).get_info(0), 0)
666
+ self.assertEqual(cached.get_info(1), 2)
667
+ self.assertEqual(super(Child, cached).get_info(1), 1)
668
+ self.assertIn("get_info", vars(cached))
669
+ self.assertEqual(cached.get_info.cache_info(), (1, 2, 2, 2))
670
+ self.assertEqual(super(Child, cached).get_info.cache_info(), (2, 2, 2, 2))
671
+ cached.get_info.cache_clear()
672
+ self.assertEqual(cached.get_info.cache_info(), (0, 0, 2, 0))
673
+ self.assertEqual(super(Child, cached).get_info.cache_info(), (2, 2, 2, 2))
674
+ self.assertEqual(cached.get_info(0), 1)
675
+ self.assertEqual(cached.get_info.cache_info(), (0, 1, 2, 1))
676
+ self.assertEqual(super(Child, cached).get_info.cache_info(), (3, 2, 2, 2))
677
+ super(Child, cached).get_info.cache_clear()
678
+ self.assertEqual(cached.get_info.cache_info(), (0, 1, 2, 1))
679
+ self.assertEqual(super(Child, cached).get_info.cache_info(), (0, 0, 2, 0))
680
+
681
+ def test_multilevel_override(self):
682
+ class Child(Cached):
683
+ @cachedmethod(lambda self: self.cache, key=keys.typedmethodkey)
684
+ def get_typed(self, value):
685
+ return super().get_typed(float(value)) + 1
686
+
687
+ class GrandChild(Child):
688
+ @cachedmethod(lambda self: self.cache, key=keys.typedmethodkey, info=True)
689
+ def get_typed(self, value):
690
+ return super().get_typed(int(value)) + 1
691
+
692
+ cache = self.cache(5) # shared cache, but different key types
693
+ cached = GrandChild(cache)
694
+ self.assertEqual(cached.get_typed("0"), 2)
695
+ self.assertEqual(cached.get_typed("0"), 2)
696
+ self.assertEqual(cached.get_typed.cache_info(), (1, 1, 5, 3))
697
+ self.assertEqual(super(GrandChild, cached).get_typed(0), 1)
698
+ self.assertEqual(super(Child, cached).get_typed(0.0), 0)
699
+
635
700
 
636
701
  class DictMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
637
702
  def cache(self, minsize, **_kwargs):
@@ -683,8 +748,51 @@ class NoneMethodTest(unittest.TestCase):
683
748
  wrapper.cache_info()
684
749
 
685
750
 
686
- class AutospecTest(unittest.TestCase):
751
+ class UnboundMethodTest(unittest.TestCase):
752
+ def test_class_call(self):
753
+ cached = Cached(Cache(2))
754
+ self.assertEqual(Cached.get(cached, 1), 0)
755
+ self.assertEqual(cached.get(1), 0)
756
+ self.assertEqual(Cached.get(cached, 2), 1)
757
+ self.assertEqual(cached.get(2), 1)
758
+ self.assertEqual(cached.count, 2)
759
+ other = Cached(Cache(2), count=10)
760
+ self.assertEqual(Cached.get(other, 1), 10)
761
+ self.assertEqual(cached.get(1), 0)
762
+
763
+ def test_class_call_info(self):
764
+ cached = Cached(Cache(2))
765
+ self.assertEqual(Cached.get_info(cached, 1), 0)
766
+ self.assertEqual(cached.get_info(1), 0)
767
+ self.assertEqual(Cached.get_info(cached, 2), 1)
768
+ self.assertEqual(cached.get_info(2), 1)
769
+ self.assertEqual(cached.count, 2)
770
+ self.assertEqual(cached.get_info.cache_info(), (2, 2, 2, 2))
771
+ other = Cached(Cache(2), count=10)
772
+ self.assertEqual(Cached.get_info(other, 1), 10)
773
+ self.assertEqual(cached.get_info(1), 0)
774
+
775
+ def test_base_class_call(self):
776
+ cached = Unhashable(Cache(2))
777
+ self.assertEqual(Cached.get(cached, 1), 0)
778
+ self.assertEqual(cached.get(1), 0)
779
+ self.assertEqual(cached.count, 1)
780
+
781
+ def test_class_access_identity(self):
782
+ self.assertIs(Cached.get, Cached.get)
783
+ self.assertIs(Cached.get_info, Cached.get_info)
784
+ self.assertIs(Unhashable.get, Cached.get)
785
+ self.assertIsNot(Cached.get, Cached.get_info)
786
+
787
+ def test_wrapped_method(self):
788
+ cached = Cached(Cache(2))
789
+ self.assertIs(Cached.get.__wrapped__, cached.get.__wrapped__)
790
+ self.assertEqual(Cached.get.__wrapped__(cached, 1), 0)
791
+ self.assertEqual(Cached.get.__wrapped__(cached, 1), 1)
792
+ self.assertEqual(len(cached.cache), 0)
793
+
687
794
  def test_autospec_no_warnings(self):
795
+ import unittest.mock
688
796
 
689
797
  with warnings.catch_warnings(record=True) as w:
690
798
  warnings.simplefilter("always")
@@ -4,10 +4,10 @@ from typing import Any
4
4
 
5
5
  import cachetools.func
6
6
 
7
- from . import _TestCaseProtocol
7
+ from . import SimplifiedTestCaseProtocol
8
8
 
9
9
 
10
- class DecoratorTestMixin(_TestCaseProtocol):
10
+ class DecoratorTestMixin(SimplifiedTestCaseProtocol):
11
11
  DECORATOR: Callable[..., Any]
12
12
 
13
13
  def decorator(self, maxsize, **kwargs):
@@ -109,6 +109,29 @@ class DecoratorTestMixin(_TestCaseProtocol):
109
109
  # Then do a call which will cause a deadlock with a non-reentrant lock.
110
110
  self.assertEqual(cached(RecursiveEquals(True)), RecursiveEquals(True))
111
111
 
112
+ def test_decorator_method(self):
113
+ """Though it is not encouraged, like functools.lru_cache, the
114
+ cachetools.func decorators may be used on methods, too.
115
+
116
+ """
117
+
118
+ decorator = self.decorator(maxsize=2)
119
+
120
+ class Cached:
121
+ @decorator
122
+ def get(self, n):
123
+ return n
124
+
125
+ cached = Cached()
126
+ self.assertEqual(cached.get(1), 1)
127
+ self.assertEqual(cached.get(1), 1)
128
+ self.assertEqual(Cached.get(cached, 1), 1)
129
+ self.assertEqual(cached.get.cache_info(), (2, 1, 2, 1))
130
+ self.assertEqual(Cached.get.cache_info(), (2, 1, 2, 1))
131
+ cached.get.cache_clear()
132
+ self.assertEqual(cached.get.cache_info(), (0, 0, 2, 0))
133
+ self.assertEqual(Cached.get.cache_info(), (0, 0, 2, 0))
134
+
112
135
 
113
136
  class FIFODecoratorTest(unittest.TestCase, DecoratorTestMixin):
114
137
  DECORATOR = staticmethod(cachetools.func.fifo_cache)
@@ -0,0 +1,103 @@
1
+ import unittest
2
+ from threading import Barrier, Condition, Lock, Thread
3
+
4
+ from cachetools import LRUCache, cached, cachedmethod
5
+
6
+
7
+ class ThreadingTest(unittest.TestCase):
8
+ NTHREADS = 10
9
+
10
+ TIMEOUT = 10
11
+
12
+ def test_cached_lock(self):
13
+ for info in (False, True):
14
+ with self.subTest(info=info):
15
+ barrier = Barrier(self.NTHREADS, timeout=self.TIMEOUT)
16
+ cache = LRUCache(1)
17
+
18
+ @cached(cache, lock=Lock(), info=info)
19
+ def func(barrier=barrier):
20
+ result = object()
21
+ barrier.wait()
22
+ return result
23
+
24
+ self.run_lock_test(func, cache, info)
25
+
26
+ def test_cachedmethod_lock(self):
27
+ for info in (False, True):
28
+ with self.subTest(info=info):
29
+ barrier = Barrier(self.NTHREADS, timeout=self.TIMEOUT)
30
+ cache = LRUCache(1)
31
+ lock = Lock()
32
+
33
+ class Cached:
34
+ @cachedmethod(lambda _: cache, lock=lambda _: lock, info=info)
35
+ def method(self, barrier=barrier):
36
+ result = object()
37
+ barrier.wait()
38
+ return result
39
+
40
+ self.run_lock_test(Cached().method, cache, info)
41
+
42
+ def test_cached_cond(self):
43
+ for info in (False, True):
44
+ with self.subTest(info=info):
45
+ cache = LRUCache(1)
46
+
47
+ @cached(cache, condition=Condition(), info=info)
48
+ def func():
49
+ result = object()
50
+ return result
51
+
52
+ self.run_cond_test(func, cache, info)
53
+
54
+ def test_cachedmethod_cond(self):
55
+ for info in (False, True):
56
+ with self.subTest(info=info):
57
+ cache = LRUCache(1)
58
+ cond = Condition()
59
+
60
+ class Cached:
61
+ @cachedmethod(lambda _: cache, condition=lambda _: cond, info=info)
62
+ def method(self):
63
+ result = object()
64
+ return result
65
+
66
+ self.run_cond_test(Cached().method, cache, info)
67
+
68
+ def run_lock_test(self, func, cache, info):
69
+ self.run_threading_test(func, cache)
70
+
71
+ if info:
72
+ cache_info = func.cache_info()
73
+ self.assertEqual(cache_info.hits + cache_info.misses, self.NTHREADS)
74
+ self.assertGreaterEqual(cache_info.misses, 1)
75
+
76
+ def run_cond_test(self, func, cache, info):
77
+ self.run_threading_test(func, cache)
78
+
79
+ if info:
80
+ cache_info = func.cache_info()
81
+ self.assertEqual(cache_info.hits, self.NTHREADS - 1)
82
+ self.assertEqual(cache_info.misses, 1)
83
+
84
+ def run_threading_test(self, func, cache):
85
+ results = [None] * self.NTHREADS
86
+
87
+ def run(index):
88
+ results[index] = func()
89
+
90
+ threads = [Thread(target=run, args=[i]) for i in range(self.NTHREADS)]
91
+ for thread in threads:
92
+ thread.start()
93
+ for thread in threads:
94
+ thread.join(timeout=self.TIMEOUT)
95
+ self.assertFalse(thread.is_alive())
96
+ self.assertEqual(len(cache), 1)
97
+
98
+ # multiple threads may compute the value, but the first one
99
+ # should always win and all concurrent invocations should
100
+ # return the same result
101
+ cached_result = next(iter(cache.values()))
102
+ for result in results:
103
+ self.assertIs(result, cached_result)
@@ -218,6 +218,22 @@ class TTLCacheTest(unittest.TestCase, CacheTestMixin):
218
218
  self.assertEqual([(1, 1)], list(items))
219
219
  self.assertEqual(0, len(cache))
220
220
 
221
+ def test_ttl_datetime_overflow(self):
222
+ from datetime import datetime, timedelta
223
+
224
+ cache = TTLCache[str, int, datetime](
225
+ maxsize=1, ttl=timedelta(days=1), timer=lambda: datetime.max
226
+ )
227
+
228
+ with self.assertRaises(OverflowError):
229
+ cache["overflow"] = 1
230
+
231
+ self.assertNotIn("overflow", cache)
232
+ self.assertEqual(0, len(cache))
233
+ self.assertEqual(0, cache.currsize)
234
+ self.assertEqual([], list(cache))
235
+ self.assertEqual([], cache.expire())
236
+
221
237
  def test_ttl_clear(self):
222
238
  cache = TTLCache[int, int, int](maxsize=2, ttl=2, timer=Timer())
223
239
 
@@ -1,122 +0,0 @@
1
- import threading
2
- import time
3
- import unittest
4
- from typing import Any
5
-
6
- from cachetools import LRUCache, cached, cachedmethod
7
-
8
- count = 0
9
-
10
-
11
- @cached(cache=LRUCache(1), condition=threading.Condition(), info=True)
12
- def func():
13
- global count
14
- time.sleep(1.0)
15
- count += 1
16
- return count
17
-
18
-
19
- @cached(cache=LRUCache(1), lock=threading.Lock(), info=True)
20
- def locked_func():
21
- global count
22
- time.sleep(1.0)
23
- count += 1
24
- return 42
25
-
26
-
27
- class ThreadingTest(unittest.TestCase):
28
- NTHREADS = 10
29
-
30
- TIMEOUT = 10
31
-
32
- cache: LRUCache[Any, int] = LRUCache(1)
33
-
34
- cond = threading.Condition()
35
-
36
- lock = threading.Lock()
37
-
38
- count = 0
39
-
40
- @cachedmethod(
41
- cache=lambda self: self.cache, condition=lambda self: self.cond, info=True
42
- )
43
- def method(self):
44
- time.sleep(1.0)
45
- self.count += 1
46
- return 42
47
-
48
- @cachedmethod(cache=lambda self: self.cache, lock=lambda self: self.lock, info=True)
49
- def locked_method(self):
50
- time.sleep(1.0)
51
- self.count += 1
52
- return 42
53
-
54
- def test_cached_stampede(self):
55
- global count
56
- count = 0
57
- threads = [threading.Thread(target=func) for i in range(self.NTHREADS)]
58
- for t in threads:
59
- t.start()
60
- for t in threads:
61
- t.join(timeout=self.TIMEOUT)
62
- self.assertFalse(t.is_alive())
63
-
64
- self.assertEqual(count, 1)
65
-
66
- info = func.cache_info()
67
- self.assertEqual(info.hits, self.NTHREADS - 1)
68
- self.assertEqual(info.misses, 1)
69
-
70
- def test_cachedmethod_stampede(self):
71
- self.cache = LRUCache(1)
72
- self.count = 0
73
- threads = [threading.Thread(target=self.method) for i in range(self.NTHREADS)]
74
- for t in threads:
75
- t.start()
76
- for t in threads:
77
- t.join(timeout=self.TIMEOUT)
78
- self.assertFalse(t.is_alive())
79
-
80
- self.assertEqual(self.count, 1)
81
-
82
- info = self.method.cache_info()
83
- self.assertEqual(info.hits, self.NTHREADS - 1)
84
- self.assertEqual(info.misses, 1)
85
-
86
- def test_cached_locked(self):
87
- global count
88
- count = 0
89
- threads = [threading.Thread(target=locked_func) for i in range(self.NTHREADS)]
90
- for t in threads:
91
- t.start()
92
- for t in threads:
93
- t.join(timeout=self.TIMEOUT)
94
- self.assertFalse(t.is_alive())
95
-
96
- # Without condition/stampede prevention, func is called by multiple
97
- # threads, but all return the same cached value (42).
98
- self.assertGreaterEqual(count, 1)
99
-
100
- info = locked_func.cache_info()
101
- self.assertEqual(info.hits + info.misses, self.NTHREADS)
102
- self.assertGreaterEqual(info.misses, 1)
103
-
104
- def test_cachedmethod_locked(self):
105
- self.cache = LRUCache(1)
106
- self.count = 0
107
- threads = [
108
- threading.Thread(target=self.locked_method) for i in range(self.NTHREADS)
109
- ]
110
- for t in threads:
111
- t.start()
112
- for t in threads:
113
- t.join(timeout=self.TIMEOUT)
114
- self.assertFalse(t.is_alive())
115
-
116
- # Multiple threads may compute the value, but setdefault ensures
117
- # the first cached result wins and all return 42.
118
- self.assertGreaterEqual(self.count, 1)
119
-
120
- info = self.locked_method.cache_info()
121
- self.assertEqual(info.hits + info.misses, self.NTHREADS)
122
- self.assertGreaterEqual(info.misses, 1)
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