cachetools 7.2.6__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.6 → cachetools-7.2.8}/CHANGELOG.rst +17 -0
  2. {cachetools-7.2.6/src/cachetools.egg-info → cachetools-7.2.8}/PKG-INFO +1 -1
  3. {cachetools-7.2.6 → cachetools-7.2.8}/docs/index.rst +12 -0
  4. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/__init__.py +58 -60
  5. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/__init__.pyi +7 -9
  6. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/_cachedmethod.py +10 -10
  7. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/_descriptor.py +43 -37
  8. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/keys.py +2 -2
  9. {cachetools-7.2.6 → cachetools-7.2.8/src/cachetools.egg-info}/PKG-INFO +1 -1
  10. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_cachedmethod.py +73 -0
  11. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_classmethod.py +43 -0
  12. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_keys.py +10 -0
  13. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_threading.py +86 -10
  14. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_tlru.py +20 -0
  15. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_ttl.py +15 -0
  16. {cachetools-7.2.6 → cachetools-7.2.8}/LICENSE +0 -0
  17. {cachetools-7.2.6 → cachetools-7.2.8}/MANIFEST.in +0 -0
  18. {cachetools-7.2.6 → cachetools-7.2.8}/README.rst +0 -0
  19. {cachetools-7.2.6 → cachetools-7.2.8}/docs/conf.py +0 -0
  20. {cachetools-7.2.6 → cachetools-7.2.8}/pyproject.toml +0 -0
  21. {cachetools-7.2.6 → cachetools-7.2.8}/setup.cfg +0 -0
  22. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/_cached.py +0 -0
  23. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/func.py +0 -0
  24. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/func.pyi +0 -0
  25. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/keys.pyi +0 -0
  26. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools/py.typed +0 -0
  27. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools.egg-info/SOURCES.txt +0 -0
  28. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools.egg-info/dependency_links.txt +0 -0
  29. {cachetools-7.2.6 → cachetools-7.2.8}/src/cachetools.egg-info/top_level.txt +0 -0
  30. {cachetools-7.2.6 → cachetools-7.2.8}/tests/__init__.py +0 -0
  31. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_cache.py +0 -0
  32. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_cached.py +0 -0
  33. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_fifo.py +0 -0
  34. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_func.py +0 -0
  35. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_lfu.py +0 -0
  36. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_lru.py +0 -0
  37. {cachetools-7.2.6 → cachetools-7.2.8}/tests/test_rr.py +0 -0
  38. {cachetools-7.2.6 → cachetools-7.2.8}/tox.ini +0 -0
@@ -1,3 +1,20 @@
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
+
10
+ v7.2.7 (2026-10-28)
11
+ ===================
12
+
13
+ - Various performance improvements.
14
+
15
+ - Minor unit test improvements.
16
+
17
+
1
18
  v7.2.6 (2026-10-26)
2
19
  ===================
3
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.6
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.6"
15
+ __version__ = "7.2.8"
16
16
 
17
17
  import collections
18
18
  import collections.abc
@@ -24,30 +24,12 @@ import time
24
24
  from . import keys
25
25
 
26
26
 
27
- class _DefaultSize:
28
- """A minimal "fake" dict that returns a constant size 1 for any key."""
29
-
30
- __slots__ = ()
31
-
32
- def __getitem__(self, _key):
33
- return 1
34
-
35
- def __setitem__(self, _key, _value):
36
- pass
37
-
38
- def pop(self, _key):
39
- return 1
40
-
41
- def clear(self):
42
- pass
43
-
44
-
45
27
  class Cache(collections.abc.MutableMapping):
46
28
  """Mutable mapping to serve as a simple cache or cache base class."""
47
29
 
48
30
  __marker = object()
49
31
 
50
- __size = _DefaultSize()
32
+ __size = None
51
33
 
52
34
  def __init__(self, maxsize, getsizeof=None):
53
35
  if maxsize < 0:
@@ -75,30 +57,48 @@ class Cache(collections.abc.MutableMapping):
75
57
  return self.__missing__(key)
76
58
 
77
59
  def __setitem__(self, key, value):
60
+ data = self.__data
61
+ size = self.__size
78
62
  maxsize = self.__maxsize
79
- size = self.getsizeof(value)
80
- if size < 0:
81
- raise ValueError("value size must be non-negative")
82
- if size > maxsize:
83
- raise ValueError("value too large")
84
- if key not in self.__data:
85
- diffsize = size
86
- while self.__currsize + diffsize > maxsize:
87
- self.popitem()
63
+ if size is None:
64
+ if maxsize < 1:
65
+ raise ValueError("value too large")
66
+ elif key in data:
67
+ data[key] = value
68
+ else:
69
+ while self.__currsize + 1 > maxsize:
70
+ self.popitem()
71
+ data[key] = value
72
+ self.__currsize += 1
88
73
  else:
89
- diffsize = size - self.__size[key]
90
- while self.__currsize + diffsize > maxsize:
91
- self.popitem()
92
- if key not in self.__data:
93
- diffsize = size
94
- self.__data[key] = value
95
- self.__size[key] = size
96
- self.__currsize += diffsize
74
+ itemsize = self.getsizeof(value)
75
+ if itemsize > maxsize:
76
+ raise ValueError("value too large")
77
+ if itemsize < 0:
78
+ raise ValueError("value size must be non-negative")
79
+ if key not in data:
80
+ diffsize = itemsize
81
+ while self.__currsize + diffsize > maxsize:
82
+ self.popitem()
83
+ else:
84
+ diffsize = itemsize - size[key]
85
+ while self.__currsize + diffsize > maxsize:
86
+ self.popitem()
87
+ if key not in data:
88
+ diffsize = itemsize
89
+ data[key] = value
90
+ size[key] = itemsize
91
+ self.__currsize += diffsize
97
92
 
98
93
  def __delitem__(self, key):
99
- size = self.__size.pop(key)
100
- del self.__data[key]
101
- self.__currsize -= size
94
+ size = self.__size
95
+ if size is None:
96
+ del self.__data[key]
97
+ self.__currsize -= 1
98
+ else:
99
+ itemsize = size.pop(key)
100
+ del self.__data[key]
101
+ self.__currsize -= itemsize
102
102
 
103
103
  def __contains__(self, key):
104
104
  return key in self.__data
@@ -148,8 +148,10 @@ class Cache(collections.abc.MutableMapping):
148
148
  # optimized version for each Cache subclass.
149
149
 
150
150
  def clear(self):
151
+ size = self.__size
152
+ if size is not None:
153
+ size.clear()
151
154
  self.__data.clear()
152
- self.__size.clear()
153
155
  self.__currsize = 0
154
156
 
155
157
  @property
@@ -508,11 +510,12 @@ class TTLCache(_TimedCache):
508
510
  expires = time + self.__ttl
509
511
  self.expire(time)
510
512
  cache_setitem(self, key, value)
511
- try:
512
- link = self.__getlink(key)
513
- except KeyError:
514
- self.__links[key] = link = TTLCache._Link(key)
513
+ links = self.__links
514
+ link = links.get(key)
515
+ if link is None:
516
+ links[key] = link = TTLCache._Link(key)
515
517
  else:
518
+ links.move_to_end(key)
516
519
  link.unlink()
517
520
  link.expires = expires
518
521
  link.next = root = self.__root
@@ -560,6 +563,8 @@ class TTLCache(_TimedCache):
560
563
  time = self.timer()
561
564
  root = self.__root
562
565
  curr = root.next
566
+ if curr is root or time < curr.expires:
567
+ return [] # fast path: nothing has expired
563
568
  links = self.__links
564
569
  expired = []
565
570
  cache_delitem = Cache.__delitem__
@@ -595,11 +600,6 @@ class TTLCache(_TimedCache):
595
600
  root.prev = root.next = root
596
601
  self.__links.clear()
597
602
 
598
- def __getlink(self, key):
599
- value = self.__links[key]
600
- self.__links.move_to_end(key)
601
- return value
602
-
603
603
 
604
604
  class TLRUCache(_TimedCache):
605
605
  """Time-aware Least Recently Used (TLRU) cache implementation."""
@@ -655,11 +655,12 @@ class TLRUCache(_TimedCache):
655
655
  cache_setitem(self, key, value)
656
656
  # removing an existing item would break the heap structure, so
657
657
  # only mark it as removed for now
658
- try:
659
- self.__getitem(key).removed = True
660
- except KeyError:
661
- pass
662
- self.__items[key] = item = TLRUCache._Item(key, expires)
658
+ items = self.__items
659
+ item = items.get(key)
660
+ if item is not None:
661
+ item.removed = True
662
+ items.move_to_end(key)
663
+ items[key] = item = TLRUCache._Item(key, expires)
663
664
  heapq.heappush(self.__order, item)
664
665
 
665
666
  def __delitem__(self, key, cache_delitem=Cache.__delitem__):
@@ -696,6 +697,8 @@ class TLRUCache(_TimedCache):
696
697
  if len(order) > len(items) * self.__HEAP_CLEANUP_FACTOR:
697
698
  self.__order = order = [item for item in order if not item.removed]
698
699
  heapq.heapify(order)
700
+ if not order or (not order[0].removed and time < order[0].expires):
701
+ return [] # fast path: nothing has expired
699
702
  expired = []
700
703
  cache_delitem = Cache.__delitem__
701
704
  cache_getitem = Cache.__getitem__
@@ -728,11 +731,6 @@ class TLRUCache(_TimedCache):
728
731
  self.__items.clear()
729
732
  del self.__order[:]
730
733
 
731
- def __getitem(self, key):
732
- value = self.__items[key]
733
- self.__items.move_to_end(key)
734
- return value
735
-
736
734
  def __delitem(self, key, cache_delitem=Cache.__delitem__):
737
735
  try:
738
736
  self.__items.pop(key).removed = True
@@ -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
@@ -46,46 +46,34 @@ class _MethodDescriptor:
46
46
  # objtype is not used for instance lookups, so it need not be bound here
47
47
  return self.__get_unbound()
48
48
  elif self.__attrname is not None:
49
- # replace descriptor instance with wrapper in instance dict
50
- wrapper = self.Wrapper(obj, self) # type: ignore
51
- try:
52
- descriptor = next(
53
- (
54
- cls.__dict__[self.__attrname]
55
- for cls in type(obj).__mro__
56
- if self.__attrname in cls.__dict__
57
- ),
58
- None,
59
- )
60
- if descriptor is self:
61
- # In case of a race condition where another thread already replaced
62
- # the descriptor, prefer the initial wrapper.
49
+ descriptor = next(
50
+ (
51
+ cls.__dict__[self.__attrname]
52
+ for cls in type(obj).__mro__
53
+ if self.__attrname in cls.__dict__
54
+ ),
55
+ None,
56
+ )
57
+ if descriptor is self:
58
+ # In case of a race condition where another thread already replaced
59
+ # the descriptor, prefer the initial wrapper.
60
+ wrapper = self.Wrapper(obj, self) # type: ignore
61
+ try:
63
62
  wrapper = obj.__dict__.setdefault(self.__attrname, wrapper)
64
- else:
65
- # A super() lookup must neither reuse the override's wrapper
66
- # nor replace it, so preserve state separately for each descriptor.
63
+ except (AttributeError, TypeError) as e:
64
+ self.__instance_dict_error(obj, e)
65
+ else:
66
+ # A super() lookup must neither reuse the override's wrapper
67
+ # nor replace it, so preserve state separately for each descriptor.
68
+ try:
67
69
  wrappers = obj.__dict__.setdefault("__cachetools_cachedmethods", {})
70
+ except (AttributeError, TypeError) as e:
71
+ self.__instance_dict_error(obj, e)
72
+ wrappers = {} # deprecated fallback: throwaway wrapper
73
+ wrapper = wrappers.get(self)
74
+ if wrapper is None:
75
+ wrapper = self.Wrapper(obj, self) # type: ignore
68
76
  wrapper = wrappers.setdefault(self, wrapper)
69
- except AttributeError:
70
- # not all objects have __dict__ (e.g. class defines slots)
71
- msg = (
72
- f"No '__dict__' attribute on {type(obj).__name__!r} "
73
- f"instance to cache {self.__attrname!r} property."
74
- )
75
- if self.__deprecated:
76
- _warn_instance_dict(msg, 3)
77
- else:
78
- raise TypeError(msg) from None
79
- except TypeError:
80
- msg = (
81
- f"The '__dict__' attribute on {type(obj).__name__!r} "
82
- f"instance does not support item assignment for "
83
- f"caching {self.__attrname!r} property."
84
- )
85
- if self.__deprecated:
86
- _warn_instance_dict(msg, 3)
87
- else:
88
- raise TypeError(msg) from None
89
77
  elif self.__deprecated:
90
78
  # deprecated @classmethod, warning already raised elsewhere
91
79
  wrapper = self.Wrapper(obj, self) # type: ignore
@@ -110,6 +98,24 @@ class _MethodDescriptor:
110
98
  self.__unbound = _Unbound(self)
111
99
  return self.__unbound
112
100
 
101
+ def __instance_dict_error(self, obj, error):
102
+ if isinstance(error, AttributeError):
103
+ # not all objects have __dict__ (e.g. class defines slots)
104
+ msg = (
105
+ f"No '__dict__' attribute on {type(obj).__name__!r} "
106
+ f"instance to cache {self.__attrname!r}"
107
+ )
108
+ else:
109
+ # some objects may have an immutable __dict__
110
+ msg = (
111
+ f"The '__dict__' attribute on {type(obj).__name__!r} "
112
+ f"instance does not support item assignment for "
113
+ f"caching {self.__attrname!r}"
114
+ )
115
+ if not self.__deprecated:
116
+ raise TypeError(msg) from None
117
+ _warn_instance_dict(msg, 4)
118
+
113
119
 
114
120
  class _Unbound:
115
121
  """Callable returned on class access.
@@ -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.6
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>
@@ -565,6 +565,15 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
565
565
  def get_info(self, value):
566
566
  return value
567
567
 
568
+ class SlotsChild(Slots):
569
+ __slots__ = ()
570
+
571
+ def get(self, value): # type: ignore
572
+ return super().get(value)
573
+
574
+ def get_info(self, value): # type: ignore
575
+ return super().get_info(value)
576
+
568
577
  cache = self.cache(2)
569
578
  cached = Slots(cache)
570
579
 
@@ -579,6 +588,21 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
579
588
  self.assertEqual(len(w), 2)
580
589
  self.assertIs(w[0].category, DeprecationWarning)
581
590
  self.assertIs(w[1].category, DeprecationWarning)
591
+ self.assertEqual(w[0].filename, __file__)
592
+
593
+ child = SlotsChild(cache)
594
+
595
+ with self.assertRaisesRegex(TypeError, "No '__dict__' attribute"):
596
+ child.get_info(42)
597
+
598
+ with warnings.catch_warnings(record=True) as w:
599
+ warnings.simplefilter("always")
600
+ self.assertEqual(child.get(0), 0)
601
+ self.assertEqual(child.get(0), 0)
602
+ self.assertEqual(len(w), 2)
603
+ self.assertIs(w[0].category, DeprecationWarning)
604
+ self.assertIs(w[1].category, DeprecationWarning)
605
+ self.assertEqual(w[0].filename, __file__)
582
606
 
583
607
  def test_decorator_immutable_dict(self):
584
608
 
@@ -602,6 +626,13 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
602
626
  def get_info(self, value):
603
627
  return value
604
628
 
629
+ class ImmutableChild(Immutable):
630
+ def get(self, value): # type: ignore
631
+ return super().get(value)
632
+
633
+ def get_info(self, value): # type: ignore
634
+ return super().get_info(value)
635
+
605
636
  cache = self.cache(2)
606
637
  cached = Immutable(cache)
607
638
 
@@ -616,6 +647,48 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
616
647
  self.assertEqual(len(w), 2)
617
648
  self.assertIs(w[0].category, DeprecationWarning)
618
649
  self.assertIs(w[1].category, DeprecationWarning)
650
+ self.assertEqual(w[0].filename, __file__)
651
+
652
+ child = ImmutableChild(cache)
653
+
654
+ with self.assertRaisesRegex(TypeError, "does not support item assignment"):
655
+ child.get_info(42)
656
+
657
+ with warnings.catch_warnings(record=True) as w:
658
+ warnings.simplefilter("always")
659
+ self.assertEqual(child.get(0), 0)
660
+ self.assertEqual(child.get(0), 0)
661
+ self.assertEqual(len(w), 2)
662
+ self.assertIs(w[0].category, DeprecationWarning)
663
+ self.assertIs(w[1].category, DeprecationWarning)
664
+ self.assertEqual(w[0].filename, __file__)
665
+
666
+ def test_decorator_wrapper_error(self):
667
+ # a non-callable key makes Wrapper.__init__ raise TypeError
668
+ class Cached:
669
+ def __init__(self, cache):
670
+ self.cache = cache
671
+
672
+ @cachedmethod(lambda self: self.cache, key=None) # type: ignore
673
+ def get(self, value):
674
+ return value
675
+
676
+ @cachedmethod(lambda self: self.cache, key=None, info=True) # type: ignore
677
+ def get_info(self, value):
678
+ return value
679
+
680
+ class CachedChild(Cached):
681
+ def get(self, value): # type: ignore
682
+ return super().get(value)
683
+
684
+ def get_info(self, value):
685
+ return super().get_info(value)
686
+
687
+ cached = CachedChild(self.cache(2))
688
+ with self.assertRaisesRegex(TypeError, "must be callable"):
689
+ cached.get(0)
690
+ with self.assertRaisesRegex(TypeError, "must be callable"):
691
+ cached.get_info(0)
619
692
 
620
693
  def test_decorator_different_names(self):
621
694
  # RuntimeError for Python < 3.12, TypeError otherwise
@@ -36,6 +36,12 @@ class Cached:
36
36
  cls.count += 1
37
37
  return cls.count
38
38
 
39
+ @classmethod
40
+ @cachedmethod(lambda cls: cls.cache, info=True)
41
+ def get_info(cls, value):
42
+ cls.count += 1
43
+ return cls.count
44
+
39
45
 
40
46
  class CachedClassMethodTest(unittest.TestCase):
41
47
  def test(self):
@@ -114,6 +120,43 @@ class CachedClassMethodTest(unittest.TestCase):
114
120
  self.assertIs(w[0].category, DeprecationWarning)
115
121
  self.assertIs(w[1].category, DeprecationWarning)
116
122
 
123
+ def test_info(self):
124
+ # info=True requires an instance method, so calls fail on all
125
+ # supported Python versions
126
+ Cached.cache = LRUCache(2)
127
+ cached = Cached()
128
+
129
+ with warnings.catch_warnings():
130
+ warnings.simplefilter("ignore")
131
+ with self.assertRaises(TypeError):
132
+ cached.get_info(0)
133
+ with self.assertRaises(TypeError):
134
+ Cached.get_info(0)
135
+ self.assertEqual(len(Cached.cache), 0)
136
+
137
+ def test_shared_cache(self):
138
+ class Child(Cached):
139
+ pass
140
+
141
+ # cache is a class attribute, so it is shared with subclasses
142
+ Cached.cache = LRUCache(2)
143
+ Cached.count = 0
144
+
145
+ with warnings.catch_warnings():
146
+ warnings.simplefilter("ignore")
147
+ self.assertIs(Child.cache, Cached.cache)
148
+ self.assertEqual(Cached.get(0), 1)
149
+ self.assertEqual(Child.get(0), 1)
150
+ self.assertEqual(Child().get(0), 1)
151
+ self.assertEqual(len(Cached.cache), 1)
152
+ self.assertEqual(Child.get(1), 2)
153
+ self.assertEqual(Cached.get(1), 2)
154
+ self.assertEqual(len(Cached.cache), 2)
155
+
156
+ Child.get.cache_clear(Child) # type: ignore
157
+ self.assertEqual(len(Cached.cache), 0)
158
+ self.assertEqual(Cached.get(0), 2)
159
+
117
160
  def test_clear(self):
118
161
  Cached.cache = LRUCache(2)
119
162
  Cached.count = 0
@@ -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