cachetools 5.5.2__tar.gz → 6.0.0b2__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 (33) hide show
  1. {cachetools-5.5.2 → cachetools-6.0.0b2}/CHANGELOG.rst +24 -0
  2. {cachetools-5.5.2/src/cachetools.egg-info → cachetools-6.0.0b2}/PKG-INFO +3 -4
  3. {cachetools-5.5.2 → cachetools-6.0.0b2}/docs/index.rst +1 -25
  4. {cachetools-5.5.2 → cachetools-6.0.0b2}/setup.cfg +0 -2
  5. {cachetools-5.5.2 → cachetools-6.0.0b2}/src/cachetools/__init__.py +18 -97
  6. cachetools-5.5.2/src/cachetools/_decorators.py → cachetools-6.0.0b2/src/cachetools/_cached.py +93 -8
  7. cachetools-6.0.0b2/src/cachetools/_cachedmethod.py +108 -0
  8. {cachetools-5.5.2 → cachetools-6.0.0b2}/src/cachetools/func.py +6 -22
  9. {cachetools-5.5.2 → cachetools-6.0.0b2/src/cachetools.egg-info}/PKG-INFO +3 -4
  10. {cachetools-5.5.2 → cachetools-6.0.0b2}/src/cachetools.egg-info/SOURCES.txt +2 -2
  11. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_cached.py +151 -0
  12. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_cachedmethod.py +138 -20
  13. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_func.py +0 -12
  14. cachetools-5.5.2/tests/test_mru.py +0 -63
  15. {cachetools-5.5.2 → cachetools-6.0.0b2}/LICENSE +0 -0
  16. {cachetools-5.5.2 → cachetools-6.0.0b2}/MANIFEST.in +0 -0
  17. {cachetools-5.5.2 → cachetools-6.0.0b2}/README.rst +0 -0
  18. {cachetools-5.5.2 → cachetools-6.0.0b2}/docs/conf.py +0 -0
  19. {cachetools-5.5.2 → cachetools-6.0.0b2}/pyproject.toml +0 -0
  20. {cachetools-5.5.2 → cachetools-6.0.0b2}/setup.py +0 -0
  21. {cachetools-5.5.2 → cachetools-6.0.0b2}/src/cachetools/keys.py +0 -0
  22. {cachetools-5.5.2 → cachetools-6.0.0b2}/src/cachetools.egg-info/dependency_links.txt +0 -0
  23. {cachetools-5.5.2 → cachetools-6.0.0b2}/src/cachetools.egg-info/top_level.txt +0 -0
  24. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/__init__.py +0 -0
  25. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_cache.py +0 -0
  26. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_fifo.py +0 -0
  27. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_keys.py +0 -0
  28. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_lfu.py +0 -0
  29. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_lru.py +0 -0
  30. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_rr.py +0 -0
  31. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_tlru.py +0 -0
  32. {cachetools-5.5.2 → cachetools-6.0.0b2}/tests/test_ttl.py +0 -0
  33. {cachetools-5.5.2 → cachetools-6.0.0b2}/tox.ini +0 -0
@@ -1,3 +1,27 @@
1
+ v6.0.0 (UNRELEASED)
2
+ ===================
3
+
4
+ - Require Python 3.9 or later (breaking change).
5
+
6
+ - Remove ``MRUCache`` and the ``@func.mru_cache`` decorator (breaking
7
+ change).
8
+
9
+ - Add an optional ``condition`` parameter to the ``@cached`` and
10
+ ``@cachedmethod`` decorators, which, when used with a
11
+ ``threading.Condition`` instance, should improve `cache stampede
12
+ <https://en.wikipedia.org/wiki/Cache_stampede>`_ issues in massively
13
+ parallel environments. Note that this will incur some performance
14
+ penalty, and therefore has to be enabled explicitly.
15
+
16
+ - Convert the ``cachetools.func`` decorators to use a
17
+ ``threading.Condition`` instance to deal with `cache stampede
18
+ <https://en.wikipedia.org/wiki/Cache_stampede>`_ issues. Note that
19
+ this *may* result in a noticable performance degradation, depending
20
+ on your actual use case.
21
+
22
+ - Update CI environment.
23
+
24
+
1
25
  v5.5.2 (2025-02-20)
2
26
  ===================
3
27
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 5.5.2
3
+ Version: 6.0.0b2
4
4
  Summary: Extensible memoizing collections and decorators
5
5
  Home-page: https://github.com/tkem/cachetools/
6
6
  Author: Thomas Kemmer
@@ -13,8 +13,6 @@ Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
16
  Classifier: Programming Language :: Python :: 3.9
19
17
  Classifier: Programming Language :: Python :: 3.10
20
18
  Classifier: Programming Language :: Python :: 3.11
@@ -23,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
23
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
22
  Requires-Python: >=3.7
25
23
  License-File: LICENSE
24
+ Dynamic: license-file
26
25
 
27
26
  cachetools
28
27
  ========================================================================
@@ -98,18 +98,6 @@ inserted into the cache.
98
98
  This class discards the least recently used items first to make
99
99
  space when necessary.
100
100
 
101
- .. autoclass:: MRUCache(maxsize, getsizeof=None)
102
- :members: popitem
103
-
104
- This class discards the most recently used items first to make
105
- space when necessary.
106
-
107
- .. deprecated:: 5.4
108
-
109
- `MRUCache` has been deprecated due to lack of use, to reduce
110
- maintenance. Please choose another cache implementation that suits
111
- your needs.
112
-
113
101
  .. autoclass:: RRCache(maxsize, choice=random.choice, getsizeof=None)
114
102
  :members: choice, popitem
115
103
 
@@ -174,7 +162,7 @@ inserted into the cache.
174
162
  value of `timer()`.
175
163
 
176
164
  .. testcode::
177
-
165
+
178
166
  def my_ttu(_key, value, now):
179
167
  # assume value.ttu contains the item's time-to-use in seconds
180
168
  # note that the _key argument is ignored in this example
@@ -688,18 +676,6 @@ all the decorators in this module are thread-safe by default.
688
676
  saves up to `maxsize` results based on a Least Recently Used (LRU)
689
677
  algorithm.
690
678
 
691
- .. decorator:: mru_cache(user_function)
692
- mru_cache(maxsize=128, typed=False)
693
-
694
- Decorator that wraps a function with a memoizing callable that
695
- saves up to `maxsize` results based on a Most Recently Used (MRU)
696
- algorithm.
697
-
698
- .. deprecated:: 5.4
699
-
700
- The `mru_cache` decorator has been deprecated due to lack of use.
701
- Please choose a decorator based on some other algorithm.
702
-
703
679
  .. decorator:: rr_cache(user_function)
704
680
  rr_cache(maxsize=128, choice=random.choice, typed=False)
705
681
 
@@ -16,8 +16,6 @@ classifiers =
16
16
  Operating System :: OS Independent
17
17
  Programming Language :: Python
18
18
  Programming Language :: Python :: 3
19
- Programming Language :: Python :: 3.7
20
- Programming Language :: Python :: 3.8
21
19
  Programming Language :: Python :: 3.9
22
20
  Programming Language :: Python :: 3.10
23
21
  Programming Language :: Python :: 3.11
@@ -5,7 +5,6 @@ __all__ = (
5
5
  "FIFOCache",
6
6
  "LFUCache",
7
7
  "LRUCache",
8
- "MRUCache",
9
8
  "RRCache",
10
9
  "TLRUCache",
11
10
  "TTLCache",
@@ -13,7 +12,7 @@ __all__ = (
13
12
  "cachedmethod",
14
13
  )
15
14
 
16
- __version__ = "5.5.2"
15
+ __version__ = "6.0.0b2"
17
16
 
18
17
  import collections
19
18
  import collections.abc
@@ -23,7 +22,8 @@ import random
23
22
  import time
24
23
 
25
24
  from . import keys
26
- from ._decorators import _cached_wrapper
25
+ from ._cached import _cached_wrapper
26
+ from ._cachedmethod import _cachedmethod_wrapper
27
27
 
28
28
 
29
29
  class _DefaultSize:
@@ -237,47 +237,6 @@ class LRUCache(Cache):
237
237
  self.__order[key] = None
238
238
 
239
239
 
240
- class MRUCache(Cache):
241
- """Most Recently Used (MRU) cache implementation."""
242
-
243
- def __init__(self, maxsize, getsizeof=None):
244
- from warnings import warn
245
-
246
- warn("MRUCache is deprecated", DeprecationWarning, stacklevel=2)
247
-
248
- Cache.__init__(self, maxsize, getsizeof)
249
- self.__order = collections.OrderedDict()
250
-
251
- def __getitem__(self, key, cache_getitem=Cache.__getitem__):
252
- value = cache_getitem(self, key)
253
- if key in self: # __missing__ may not store item
254
- self.__update(key)
255
- return value
256
-
257
- def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
258
- cache_setitem(self, key, value)
259
- self.__update(key)
260
-
261
- def __delitem__(self, key, cache_delitem=Cache.__delitem__):
262
- cache_delitem(self, key)
263
- del self.__order[key]
264
-
265
- def popitem(self):
266
- """Remove and return the `(key, value)` pair most recently used."""
267
- try:
268
- key = next(iter(self.__order))
269
- except StopIteration:
270
- raise KeyError("%s is empty" % type(self).__name__) from None
271
- else:
272
- return (key, self.pop(key))
273
-
274
- def __update(self, key):
275
- try:
276
- self.__order.move_to_end(key, last=False)
277
- except KeyError:
278
- self.__order[key] = None
279
-
280
-
281
240
  class RRCache(Cache):
282
241
  """Random Replacement (RR) cache implementation."""
283
242
 
@@ -636,11 +595,21 @@ _CacheInfo = collections.namedtuple(
636
595
  )
637
596
 
638
597
 
639
- def cached(cache, key=keys.hashkey, lock=None, info=False):
598
+ def cached(cache, key=keys.hashkey, lock=None, condition=None, info=False):
640
599
  """Decorator to wrap a function with a memoizing callable that saves
641
600
  results in a cache.
642
601
 
643
602
  """
603
+ if isinstance(condition, bool):
604
+ from warnings import warn
605
+
606
+ warn(
607
+ "passing `info` as positional parameter is deprecated",
608
+ DeprecationWarning,
609
+ stacklevel=2,
610
+ )
611
+ info = condition
612
+ condition = None
644
613
 
645
614
  def decorator(func):
646
615
  if info:
@@ -659,9 +628,9 @@ def cached(cache, key=keys.hashkey, lock=None, info=False):
659
628
  def make_info(hits, misses):
660
629
  return _CacheInfo(hits, misses, 0, 0)
661
630
 
662
- wrapper = _cached_wrapper(func, cache, key, lock, make_info)
631
+ wrapper = _cached_wrapper(func, cache, key, lock, condition, info=make_info)
663
632
  else:
664
- wrapper = _cached_wrapper(func, cache, key, lock, None)
633
+ wrapper = _cached_wrapper(func, cache, key, lock, condition)
665
634
 
666
635
  wrapper.cache = cache
667
636
  wrapper.cache_key = key
@@ -672,66 +641,18 @@ def cached(cache, key=keys.hashkey, lock=None, info=False):
672
641
  return decorator
673
642
 
674
643
 
675
- def cachedmethod(cache, key=keys.methodkey, lock=None):
644
+ def cachedmethod(cache, key=keys.methodkey, lock=None, condition=None):
676
645
  """Decorator to wrap a class or instance method with a memoizing
677
646
  callable that saves results in a cache.
678
647
 
679
648
  """
680
649
 
681
650
  def decorator(method):
682
- if lock is None:
683
-
684
- def wrapper(self, *args, **kwargs):
685
- c = cache(self)
686
- if c is None:
687
- return method(self, *args, **kwargs)
688
- k = key(self, *args, **kwargs)
689
- try:
690
- return c[k]
691
- except KeyError:
692
- pass # key not found
693
- v = method(self, *args, **kwargs)
694
- try:
695
- c[k] = v
696
- except ValueError:
697
- pass # value too large
698
- return v
699
-
700
- def clear(self):
701
- c = cache(self)
702
- if c is not None:
703
- c.clear()
704
-
705
- else:
706
-
707
- def wrapper(self, *args, **kwargs):
708
- c = cache(self)
709
- if c is None:
710
- return method(self, *args, **kwargs)
711
- k = key(self, *args, **kwargs)
712
- try:
713
- with lock(self):
714
- return c[k]
715
- except KeyError:
716
- pass # key not found
717
- v = method(self, *args, **kwargs)
718
- # in case of a race, prefer the item already in the cache
719
- try:
720
- with lock(self):
721
- return c.setdefault(k, v)
722
- except ValueError:
723
- return v # value too large
724
-
725
- def clear(self):
726
- c = cache(self)
727
- if c is not None:
728
- with lock(self):
729
- c.clear()
651
+ wrapper = _cachedmethod_wrapper(method, cache, key, lock, condition)
730
652
 
731
653
  wrapper.cache = cache
732
654
  wrapper.cache_key = key
733
655
  wrapper.cache_lock = lock
734
- wrapper.cache_clear = clear
735
656
 
736
657
  return functools.update_wrapper(wrapper, method)
737
658
 
@@ -1,4 +1,48 @@
1
- """Extensible memoizing decorator helpers."""
1
+ """Function decorator helpers."""
2
+
3
+
4
+ def _cached_condition_info(func, cache, key, lock, cond, info):
5
+ hits = misses = 0
6
+ pending = set()
7
+
8
+ def wrapper(*args, **kwargs):
9
+ nonlocal hits, misses
10
+ k = key(*args, **kwargs)
11
+ with lock:
12
+ cond.wait_for(lambda: k not in pending)
13
+ try:
14
+ result = cache[k]
15
+ hits += 1
16
+ return result
17
+ except KeyError:
18
+ pending.add(k)
19
+ misses += 1
20
+ try:
21
+ v = func(*args, **kwargs)
22
+ with lock:
23
+ try:
24
+ cache[k] = v
25
+ except ValueError:
26
+ pass # value too large
27
+ return v
28
+ finally:
29
+ with lock:
30
+ pending.remove(k)
31
+ cond.notify_all()
32
+
33
+ def cache_clear():
34
+ nonlocal hits, misses
35
+ with lock:
36
+ cache.clear()
37
+ hits = misses = 0
38
+
39
+ def cache_info():
40
+ with lock:
41
+ return info(hits, misses)
42
+
43
+ wrapper.cache_clear = cache_clear
44
+ wrapper.cache_info = cache_info
45
+ return wrapper
2
46
 
3
47
 
4
48
  def _cached_locked_info(func, cache, key, lock, info):
@@ -83,6 +127,39 @@ def _uncached_info(func, info):
83
127
  return wrapper
84
128
 
85
129
 
130
+ def _cached_condition(func, cache, key, lock, cond):
131
+ pending = set()
132
+
133
+ def wrapper(*args, **kwargs):
134
+ k = key(*args, **kwargs)
135
+ with lock:
136
+ cond.wait_for(lambda: k not in pending)
137
+ try:
138
+ result = cache[k]
139
+ return result
140
+ except KeyError:
141
+ pending.add(k)
142
+ try:
143
+ v = func(*args, **kwargs)
144
+ with lock:
145
+ try:
146
+ cache[k] = v
147
+ except ValueError:
148
+ pass # value too large
149
+ return v
150
+ finally:
151
+ with lock:
152
+ pending.remove(k)
153
+ cond.notify_all()
154
+
155
+ def cache_clear():
156
+ with lock:
157
+ cache.clear()
158
+
159
+ wrapper.cache_clear = cache_clear
160
+ return wrapper
161
+
162
+
86
163
  def _cached_locked(func, cache, key, lock):
87
164
  def wrapper(*args, **kwargs):
88
165
  k = key(*args, **kwargs)
@@ -133,20 +210,28 @@ def _uncached(func):
133
210
  return wrapper
134
211
 
135
212
 
136
- def _cached_wrapper(func, cache, key, lock, info):
213
+ def _cached_wrapper(func, cache, key, lock=None, cond=None, info=None):
137
214
  if info is not None:
138
215
  if cache is None:
139
216
  wrapper = _uncached_info(func, info)
140
- elif lock is None:
141
- wrapper = _cached_unlocked_info(func, cache, key, info)
142
- else:
217
+ elif cond is not None and lock is not None:
218
+ wrapper = _cached_condition_info(func, cache, key, lock, cond, info)
219
+ elif cond is not None:
220
+ wrapper = _cached_condition_info(func, cache, key, cond, cond, info)
221
+ elif lock is not None:
143
222
  wrapper = _cached_locked_info(func, cache, key, lock, info)
223
+ else:
224
+ wrapper = _cached_unlocked_info(func, cache, key, info)
144
225
  else:
145
226
  if cache is None:
146
227
  wrapper = _uncached(func)
147
- elif lock is None:
148
- wrapper = _cached_unlocked(func, cache, key)
149
- else:
228
+ elif cond is not None and lock is not None:
229
+ wrapper = _cached_condition(func, cache, key, lock, cond)
230
+ elif cond is not None:
231
+ wrapper = _cached_condition(func, cache, key, cond, cond)
232
+ elif lock is not None:
150
233
  wrapper = _cached_locked(func, cache, key, lock)
234
+ else:
235
+ wrapper = _cached_unlocked(func, cache, key)
151
236
  wrapper.cache_info = None
152
237
  return wrapper
@@ -0,0 +1,108 @@
1
+ """Method decorator helpers."""
2
+
3
+ import weakref
4
+
5
+
6
+ def _cachedmethod_condition(method, cache, key, lock, cond):
7
+ pending = weakref.WeakKeyDictionary()
8
+
9
+ def wrapper(self, *args, **kwargs):
10
+ c = cache(self)
11
+ if c is None:
12
+ return method(self, *args, **kwargs)
13
+ k = key(self, *args, **kwargs)
14
+ with lock(self):
15
+ p = pending.setdefault(self, set())
16
+ cond(self).wait_for(lambda: k not in p)
17
+ try:
18
+ return c[k]
19
+ except KeyError:
20
+ p.add(k)
21
+ try:
22
+ v = method(self, *args, **kwargs)
23
+ with lock(self):
24
+ try:
25
+ c[k] = v
26
+ except ValueError:
27
+ pass # value too large
28
+ return v
29
+ finally:
30
+ with lock(self):
31
+ pending[self].remove(k)
32
+ cond(self).notify_all()
33
+
34
+ def cache_clear(self):
35
+ c = cache(self)
36
+ if c is not None:
37
+ with lock(self):
38
+ c.clear()
39
+
40
+ wrapper.cache_clear = cache_clear
41
+ return wrapper
42
+
43
+
44
+ def _cachedmethod_locked(method, cache, key, lock):
45
+ def wrapper(self, *args, **kwargs):
46
+ c = cache(self)
47
+ if c is None:
48
+ return method(self, *args, **kwargs)
49
+ k = key(self, *args, **kwargs)
50
+ with lock(self):
51
+ try:
52
+ return c[k]
53
+ except KeyError:
54
+ pass # key not found
55
+ v = method(self, *args, **kwargs)
56
+ # in case of a race, prefer the item already in the cache
57
+ with lock(self):
58
+ try:
59
+ return c.setdefault(k, v)
60
+ except ValueError:
61
+ return v # value too large
62
+
63
+ def cache_clear(self):
64
+ c = cache(self)
65
+ if c is not None:
66
+ with lock(self):
67
+ c.clear()
68
+
69
+ wrapper.cache_clear = cache_clear
70
+ return wrapper
71
+
72
+
73
+ def _cachedmethod_unlocked(method, cache, key):
74
+ def wrapper(self, *args, **kwargs):
75
+ c = cache(self)
76
+ if c is None:
77
+ return method(self, *args, **kwargs)
78
+ k = key(self, *args, **kwargs)
79
+ try:
80
+ return c[k]
81
+ except KeyError:
82
+ pass # key not found
83
+ v = method(self, *args, **kwargs)
84
+ try:
85
+ c[k] = v
86
+ except ValueError:
87
+ pass # value too large
88
+ return v
89
+
90
+ def cache_clear(self):
91
+ c = cache(self)
92
+ if c is not None:
93
+ c.clear()
94
+
95
+ wrapper.cache_clear = cache_clear
96
+ return wrapper
97
+
98
+
99
+ def _cachedmethod_wrapper(func, cache, key, lock=None, cond=None):
100
+ if cond is not None and lock is not None:
101
+ wrapper = _cachedmethod_condition(func, cache, key, lock, cond)
102
+ elif cond is not None:
103
+ wrapper = _cachedmethod_condition(func, cache, key, cond, cond)
104
+ elif lock is not None:
105
+ wrapper = _cachedmethod_locked(func, cache, key, lock)
106
+ else:
107
+ wrapper = _cachedmethod_unlocked(func, cache, key)
108
+ return wrapper
@@ -1,17 +1,18 @@
1
1
  """`functools.lru_cache` compatible memoizing function decorators."""
2
2
 
3
- __all__ = ("fifo_cache", "lfu_cache", "lru_cache", "mru_cache", "rr_cache", "ttl_cache")
3
+ __all__ = ("fifo_cache", "lfu_cache", "lru_cache", "rr_cache", "ttl_cache")
4
4
 
5
+ import functools
5
6
  import math
6
7
  import random
7
8
  import time
8
9
 
9
10
  try:
10
- from threading import RLock
11
+ from threading import Condition
11
12
  except ImportError: # pragma: no cover
12
- from dummy_threading import RLock
13
+ from dummy_threading import Condition
13
14
 
14
- from . import FIFOCache, LFUCache, LRUCache, MRUCache, RRCache, TTLCache
15
+ from . import FIFOCache, LFUCache, LRUCache, RRCache, TTLCache
15
16
  from . import cached
16
17
  from . import keys
17
18
 
@@ -28,7 +29,7 @@ class _UnboundTTLCache(TTLCache):
28
29
  def _cache(cache, maxsize, typed):
29
30
  def decorator(func):
30
31
  key = keys.typedkey if typed else keys.hashkey
31
- wrapper = cached(cache=cache, key=key, lock=RLock(), info=True)(func)
32
+ wrapper = cached(cache=cache, key=key, condition=Condition(), info=True)(func)
32
33
  wrapper.cache_parameters = lambda: {"maxsize": maxsize, "typed": typed}
33
34
  return wrapper
34
35
 
@@ -77,23 +78,6 @@ def lru_cache(maxsize=128, typed=False):
77
78
  return _cache(LRUCache(maxsize), maxsize, typed)
78
79
 
79
80
 
80
- def mru_cache(maxsize=128, typed=False):
81
- """Decorator to wrap a function with a memoizing callable that saves
82
- up to `maxsize` results based on a Most Recently Used (MRU)
83
- algorithm.
84
- """
85
- from warnings import warn
86
-
87
- warn("@mru_cache is deprecated", DeprecationWarning, stacklevel=2)
88
-
89
- if maxsize is None:
90
- return _cache({}, None, typed)
91
- elif callable(maxsize):
92
- return _cache(MRUCache(128), 128, typed)(maxsize)
93
- else:
94
- return _cache(MRUCache(maxsize), maxsize, typed)
95
-
96
-
97
81
  def rr_cache(maxsize=128, choice=random.choice, typed=False):
98
82
  """Decorator to wrap a function with a memoizing callable that saves
99
83
  up to `maxsize` results based on a Random Replacement (RR)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 5.5.2
3
+ Version: 6.0.0b2
4
4
  Summary: Extensible memoizing collections and decorators
5
5
  Home-page: https://github.com/tkem/cachetools/
6
6
  Author: Thomas Kemmer
@@ -13,8 +13,6 @@ Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
16
  Classifier: Programming Language :: Python :: 3.9
19
17
  Classifier: Programming Language :: Python :: 3.10
20
18
  Classifier: Programming Language :: Python :: 3.11
@@ -23,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
23
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
22
  Requires-Python: >=3.7
25
23
  License-File: LICENSE
24
+ Dynamic: license-file
26
25
 
27
26
  cachetools
28
27
  ========================================================================
@@ -9,7 +9,8 @@ tox.ini
9
9
  docs/conf.py
10
10
  docs/index.rst
11
11
  src/cachetools/__init__.py
12
- src/cachetools/_decorators.py
12
+ src/cachetools/_cached.py
13
+ src/cachetools/_cachedmethod.py
13
14
  src/cachetools/func.py
14
15
  src/cachetools/keys.py
15
16
  src/cachetools.egg-info/PKG-INFO
@@ -25,7 +26,6 @@ tests/test_func.py
25
26
  tests/test_keys.py
26
27
  tests/test_lfu.py
27
28
  tests/test_lru.py
28
- tests/test_mru.py
29
29
  tests/test_rr.py
30
30
  tests/test_tlru.py
31
31
  tests/test_ttl.py
@@ -1,4 +1,5 @@
1
1
  import unittest
2
+ import warnings
2
3
 
3
4
  import cachetools
4
5
  import cachetools.keys
@@ -15,6 +16,19 @@ class CountedLock:
15
16
  pass
16
17
 
17
18
 
19
+ class CountedCondition(CountedLock):
20
+ def __init__(self):
21
+ CountedLock.__init__(self)
22
+ self.wait_count = 0
23
+ self.notify_count = 0
24
+
25
+ def wait_for(self, predicate):
26
+ self.wait_count += 1
27
+
28
+ def notify_all(self):
29
+ self.notify_count += 1
30
+
31
+
18
32
  class DecoratorTestMixin:
19
33
  def cache(self, minsize):
20
34
  raise NotImplementedError
@@ -95,6 +109,45 @@ class DecoratorTestMixin:
95
109
  self.assertEqual(wrapper(1), 1)
96
110
  self.assertEqual(lock.count, 5)
97
111
 
112
+ def test_decorator_condition(self):
113
+ cache = self.cache(2)
114
+ lock = cond = CountedCondition()
115
+ wrapper = cachetools.cached(cache, condition=cond)(self.func)
116
+
117
+ self.assertEqual(len(cache), 0)
118
+ self.assertEqual(wrapper(0), 0)
119
+ self.assertEqual(lock.count, 3)
120
+ self.assertEqual(cond.wait_count, 1)
121
+ self.assertEqual(cond.notify_count, 1)
122
+ self.assertEqual(wrapper(1), 1)
123
+ self.assertEqual(lock.count, 6)
124
+ self.assertEqual(cond.wait_count, 2)
125
+ self.assertEqual(cond.notify_count, 2)
126
+ self.assertEqual(wrapper(1), 1)
127
+ self.assertEqual(lock.count, 7)
128
+ self.assertEqual(cond.wait_count, 3)
129
+ self.assertEqual(cond.notify_count, 2)
130
+
131
+ def test_decorator_lock_condition(self):
132
+ cache = self.cache(2)
133
+ lock = CountedLock()
134
+ cond = CountedCondition()
135
+ wrapper = cachetools.cached(cache, lock=lock, condition=cond)(self.func)
136
+
137
+ self.assertEqual(len(cache), 0)
138
+ self.assertEqual(wrapper(0), 0)
139
+ self.assertEqual(lock.count, 3)
140
+ self.assertEqual(cond.wait_count, 1)
141
+ self.assertEqual(cond.notify_count, 1)
142
+ self.assertEqual(wrapper(1), 1)
143
+ self.assertEqual(lock.count, 6)
144
+ self.assertEqual(cond.wait_count, 2)
145
+ self.assertEqual(cond.notify_count, 2)
146
+ self.assertEqual(wrapper(1), 1)
147
+ self.assertEqual(lock.count, 7)
148
+ self.assertEqual(cond.wait_count, 3)
149
+ self.assertEqual(cond.notify_count, 2)
150
+
98
151
  def test_decorator_wrapped(self):
99
152
  cache = self.cache(2)
100
153
  wrapper = cachetools.cached(cache)(self.func)
@@ -145,6 +198,17 @@ class DecoratorTestMixin:
145
198
  self.assertEqual(len(cache), 0)
146
199
  self.assertEqual(lock.count, 3)
147
200
 
201
+ def test_decorator_clear_condition(self):
202
+ cache = self.cache(2)
203
+ lock = cond = CountedCondition()
204
+ wrapper = cachetools.cached(cache, condition=cond)(self.func)
205
+ self.assertEqual(wrapper(0), 0)
206
+ self.assertEqual(len(cache), 1)
207
+ self.assertEqual(lock.count, 3)
208
+ wrapper.cache_clear()
209
+ self.assertEqual(len(cache), 0)
210
+ self.assertEqual(lock.count, 4)
211
+
148
212
 
149
213
  class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
150
214
  def cache(self, minsize):
@@ -188,6 +252,69 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
188
252
  self.assertEqual(wrapper.cache_info(), (0, 0, 2, 0))
189
253
  self.assertEqual(lock.count, 11)
190
254
 
255
+ def test_decorator_lock_info_deprecated(self):
256
+ cache = self.cache(2)
257
+ key = cachetools.keys.hashkey
258
+ lock = CountedLock()
259
+ with warnings.catch_warnings(record=True) as w:
260
+ warnings.simplefilter("always")
261
+ # passing `ìnfo` as positional parameter is deprecated
262
+ wrapper = cachetools.cached(cache, key, lock, True)(self.func)
263
+ self.assertEqual(len(w), 1)
264
+ self.assertIs(w[0].category, DeprecationWarning)
265
+ self.assertEqual(wrapper.cache_info(), (0, 0, 2, 0))
266
+
267
+ def test_decorator_condition_info(self):
268
+ cache = self.cache(2)
269
+ lock = cond = CountedCondition()
270
+ wrapper = cachetools.cached(cache, condition=cond, info=True)(self.func)
271
+ self.assertEqual(wrapper.cache_info(), (0, 0, 2, 0))
272
+ self.assertEqual(lock.count, 1)
273
+ self.assertEqual(wrapper(0), 0)
274
+ self.assertEqual(lock.count, 4)
275
+ self.assertEqual(wrapper.cache_info(), (0, 1, 2, 1))
276
+ self.assertEqual(lock.count, 5)
277
+ self.assertEqual(wrapper(1), 1)
278
+ self.assertEqual(lock.count, 8)
279
+ self.assertEqual(wrapper.cache_info(), (0, 2, 2, 2))
280
+ self.assertEqual(lock.count, 9)
281
+ self.assertEqual(wrapper(0), 0)
282
+ self.assertEqual(lock.count, 10)
283
+ self.assertEqual(wrapper.cache_info(), (1, 2, 2, 2))
284
+ self.assertEqual(lock.count, 11)
285
+ wrapper.cache_clear()
286
+ self.assertEqual(lock.count, 12)
287
+ self.assertEqual(len(cache), 0)
288
+ self.assertEqual(wrapper.cache_info(), (0, 0, 2, 0))
289
+ self.assertEqual(lock.count, 13)
290
+
291
+ def test_decorator_lock_condition_info(self):
292
+ cache = self.cache(2)
293
+ lock = CountedLock()
294
+ cond = CountedCondition()
295
+ wrapper = cachetools.cached(cache, lock=lock, condition=cond, info=True)(
296
+ self.func
297
+ )
298
+ self.assertEqual(wrapper.cache_info(), (0, 0, 2, 0))
299
+ self.assertEqual(lock.count, 1)
300
+ self.assertEqual(wrapper(0), 0)
301
+ self.assertEqual(lock.count, 4)
302
+ self.assertEqual(wrapper.cache_info(), (0, 1, 2, 1))
303
+ self.assertEqual(lock.count, 5)
304
+ self.assertEqual(wrapper(1), 1)
305
+ self.assertEqual(lock.count, 8)
306
+ self.assertEqual(wrapper.cache_info(), (0, 2, 2, 2))
307
+ self.assertEqual(lock.count, 9)
308
+ self.assertEqual(wrapper(0), 0)
309
+ self.assertEqual(lock.count, 10)
310
+ self.assertEqual(wrapper.cache_info(), (1, 2, 2, 2))
311
+ self.assertEqual(lock.count, 11)
312
+ wrapper.cache_clear()
313
+ self.assertEqual(lock.count, 12)
314
+ self.assertEqual(len(cache), 0)
315
+ self.assertEqual(wrapper.cache_info(), (0, 0, 2, 0))
316
+ self.assertEqual(lock.count, 13)
317
+
191
318
  def test_zero_size_cache_decorator(self):
192
319
  cache = self.cache(0)
193
320
  wrapper = cachetools.cached(cache)(self.func)
@@ -206,6 +333,16 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
206
333
  self.assertEqual(len(cache), 0)
207
334
  self.assertEqual(lock.count, 2)
208
335
 
336
+ def test_zero_size_cache_decorator_condition(self):
337
+ cache = self.cache(0)
338
+ lock = cond = CountedCondition()
339
+ wrapper = cachetools.cached(cache, condition=cond)(self.func)
340
+
341
+ self.assertEqual(len(cache), 0)
342
+ self.assertEqual(wrapper(0), 0)
343
+ self.assertEqual(len(cache), 0)
344
+ self.assertEqual(lock.count, 3)
345
+
209
346
  def test_zero_size_cache_decorator_info(self):
210
347
  cache = self.cache(0)
211
348
  wrapper = cachetools.cached(cache, info=True)(self.func)
@@ -214,6 +351,20 @@ class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
214
351
  self.assertEqual(wrapper(0), 0)
215
352
  self.assertEqual(wrapper.cache_info(), (0, 1, 0, 0))
216
353
 
354
+ def test_zero_size_cache_decorator_lock_info(self):
355
+ cache = self.cache(0)
356
+ lock = CountedLock()
357
+ wrapper = cachetools.cached(cache, lock=lock, info=True)(self.func)
358
+
359
+ self.assertEqual(len(cache), 0)
360
+ self.assertEqual(wrapper.cache_info(), (0, 0, 0, 0))
361
+ self.assertEqual(lock.count, 1)
362
+ self.assertEqual(wrapper(0), 0)
363
+ self.assertEqual(len(cache), 0)
364
+ self.assertEqual(lock.count, 3)
365
+ self.assertEqual(wrapper.cache_info(), (0, 1, 0, 0))
366
+ self.assertEqual(lock.count, 4)
367
+
217
368
 
218
369
  class DictWrapperTest(unittest.TestCase, DecoratorTestMixin):
219
370
  def cache(self, minsize):
@@ -25,18 +25,44 @@ class Locked:
25
25
  def __init__(self, cache):
26
26
  self.cache = cache
27
27
  self.count = 0
28
+ self.lock_count = 0
28
29
 
29
30
  @cachedmethod(lambda self: self.cache, lock=lambda self: self)
30
31
  def get(self, value):
31
- return self.count
32
+ count = self.count
33
+ self.count += 1
34
+ return count
32
35
 
33
36
  def __enter__(self):
34
- self.count += 1
37
+ self.lock_count += 1
35
38
 
36
39
  def __exit__(self, *exc):
37
40
  pass
38
41
 
39
42
 
43
+ class Conditioned(Locked):
44
+ def __init__(self, cache):
45
+ Locked.__init__(self, cache)
46
+ self.wait_count = 0
47
+ self.notify_count = 0
48
+
49
+ @cachedmethod(lambda self: self.cache, condition=lambda self: self)
50
+ def get(self, value):
51
+ return Locked.get.__wrapped__(self, value)
52
+
53
+ @cachedmethod(
54
+ lambda self: self.cache, lock=lambda self: self, condition=lambda self: self
55
+ )
56
+ def get_lock(self, value):
57
+ return Locked.get.__wrapped__(self, value)
58
+
59
+ def wait_for(self, predicate):
60
+ self.wait_count += 1
61
+
62
+ def notify_all(self):
63
+ self.notify_count += 1
64
+
65
+
40
66
  class Unhashable:
41
67
  def __init__(self, cache):
42
68
  self.cache = cache
@@ -152,29 +178,110 @@ class CachedMethodTest(unittest.TestCase):
152
178
  def test_locked_dict(self):
153
179
  cached = Locked({})
154
180
 
155
- self.assertEqual(cached.get(0), 1)
156
- self.assertEqual(cached.get(1), 3)
157
- self.assertEqual(cached.get(1), 3)
181
+ self.assertEqual(cached.get(0), 0)
182
+ self.assertEqual(cached.lock_count, 2)
183
+ self.assertEqual(cached.get(1), 1)
184
+ self.assertEqual(cached.lock_count, 4)
185
+ self.assertEqual(cached.get(1), 1)
186
+ self.assertEqual(cached.lock_count, 5)
187
+ self.assertEqual(cached.get(1.0), 1)
188
+ self.assertEqual(cached.lock_count, 6)
189
+ self.assertEqual(cached.get(1.0), 1)
190
+ self.assertEqual(cached.lock_count, 7)
191
+
192
+ cached.cache.clear()
193
+ self.assertEqual(cached.get(1), 2)
194
+ self.assertEqual(cached.lock_count, 9)
195
+
196
+ def test_locked_nospace(self):
197
+ cached = Locked(LRUCache(maxsize=0))
198
+
199
+ self.assertEqual(cached.get(0), 0)
200
+ self.assertEqual(cached.lock_count, 2)
201
+ self.assertEqual(cached.get(1), 1)
202
+ self.assertEqual(cached.lock_count, 4)
203
+ self.assertEqual(cached.get(1), 2)
204
+ self.assertEqual(cached.lock_count, 6)
158
205
  self.assertEqual(cached.get(1.0), 3)
159
- self.assertEqual(cached.get(2.0), 7)
206
+ self.assertEqual(cached.lock_count, 8)
207
+ self.assertEqual(cached.get(1.0), 4)
208
+ self.assertEqual(cached.lock_count, 10)
160
209
 
161
210
  def test_locked_nocache(self):
162
211
  cached = Locked(None)
163
212
 
164
213
  self.assertEqual(cached.get(0), 0)
165
- self.assertEqual(cached.get(1), 0)
166
- self.assertEqual(cached.get(1), 0)
167
- self.assertEqual(cached.get(1.0), 0)
168
- self.assertEqual(cached.get(1.0), 0)
214
+ self.assertEqual(cached.get(1), 1)
215
+ self.assertEqual(cached.get(1), 2)
216
+ self.assertEqual(cached.get(1.0), 3)
217
+ self.assertEqual(cached.get(1.0), 4)
218
+ self.assertEqual(cached.lock_count, 0)
169
219
 
170
- def test_locked_nospace(self):
171
- cached = Locked(LRUCache(maxsize=0))
220
+ def test_condition_dict(self):
221
+ cached = Conditioned({})
172
222
 
173
- self.assertEqual(cached.get(0), 1)
174
- self.assertEqual(cached.get(1), 3)
175
- self.assertEqual(cached.get(1), 5)
176
- self.assertEqual(cached.get(1.0), 7)
177
- self.assertEqual(cached.get(1.0), 9)
223
+ self.assertEqual(cached.get(0), 0)
224
+ self.assertEqual(cached.lock_count, 3)
225
+ self.assertEqual(cached.wait_count, 1)
226
+ self.assertEqual(cached.notify_count, 1)
227
+ self.assertEqual(cached.get(1), 1)
228
+ self.assertEqual(cached.lock_count, 6)
229
+ self.assertEqual(cached.wait_count, 2)
230
+ self.assertEqual(cached.notify_count, 2)
231
+ self.assertEqual(cached.get(1), 1)
232
+ self.assertEqual(cached.lock_count, 7)
233
+ self.assertEqual(cached.wait_count, 3)
234
+ self.assertEqual(cached.notify_count, 2)
235
+ self.assertEqual(cached.get(1.0), 1)
236
+ self.assertEqual(cached.lock_count, 8)
237
+ self.assertEqual(cached.wait_count, 4)
238
+ self.assertEqual(cached.notify_count, 2)
239
+ self.assertEqual(cached.get(1.0), 1)
240
+ self.assertEqual(cached.lock_count, 9)
241
+ self.assertEqual(cached.wait_count, 5)
242
+ self.assertEqual(cached.notify_count, 2)
243
+
244
+ cached.cache.clear()
245
+ self.assertEqual(cached.get(1), 2)
246
+ self.assertEqual(cached.lock_count, 12)
247
+ self.assertEqual(cached.wait_count, 6)
248
+ self.assertEqual(cached.notify_count, 3)
249
+
250
+ def test_condition_nospace(self):
251
+ cached = Conditioned(LRUCache(maxsize=0))
252
+
253
+ self.assertEqual(cached.get(0), 0)
254
+ self.assertEqual(cached.lock_count, 3)
255
+ self.assertEqual(cached.wait_count, 1)
256
+ self.assertEqual(cached.notify_count, 1)
257
+ self.assertEqual(cached.get(1), 1)
258
+ self.assertEqual(cached.lock_count, 6)
259
+ self.assertEqual(cached.wait_count, 2)
260
+ self.assertEqual(cached.notify_count, 2)
261
+ self.assertEqual(cached.get(1), 2)
262
+ self.assertEqual(cached.lock_count, 9)
263
+ self.assertEqual(cached.wait_count, 3)
264
+ self.assertEqual(cached.notify_count, 3)
265
+ self.assertEqual(cached.get(1.0), 3)
266
+ self.assertEqual(cached.lock_count, 12)
267
+ self.assertEqual(cached.wait_count, 4)
268
+ self.assertEqual(cached.notify_count, 4)
269
+ self.assertEqual(cached.get(1.0), 4)
270
+ self.assertEqual(cached.lock_count, 15)
271
+ self.assertEqual(cached.wait_count, 5)
272
+ self.assertEqual(cached.notify_count, 5)
273
+
274
+ def test_condition_nocache(self):
275
+ cached = Conditioned(None)
276
+
277
+ self.assertEqual(cached.get(0), 0)
278
+ self.assertEqual(cached.get(1), 1)
279
+ self.assertEqual(cached.get(1), 2)
280
+ self.assertEqual(cached.get(1.0), 3)
281
+ self.assertEqual(cached.get(1.0), 4)
282
+ self.assertEqual(cached.lock_count, 0)
283
+ self.assertEqual(cached.wait_count, 0)
284
+ self.assertEqual(cached.notify_count, 0)
178
285
 
179
286
  def test_unhashable(self):
180
287
  cached = Unhashable(LRUCache(maxsize=0))
@@ -226,9 +333,20 @@ class CachedMethodTest(unittest.TestCase):
226
333
  cache = {}
227
334
  cached = Locked(cache)
228
335
 
229
- self.assertEqual(cached.get(0), 1)
336
+ self.assertEqual(cached.get(0), 0)
337
+ self.assertEqual(len(cache), 1)
338
+ self.assertEqual(cached.lock_count, 2)
339
+ cached.get.cache_clear(cached)
340
+ self.assertEqual(len(cache), 0)
341
+ self.assertEqual(cached.lock_count, 3)
342
+
343
+ def test_clear_condition(self):
344
+ cache = {}
345
+ cached = Conditioned(cache)
346
+
347
+ self.assertEqual(cached.get(0), 0)
230
348
  self.assertEqual(len(cache), 1)
231
- self.assertEqual(cached.count, 2)
349
+ self.assertEqual(cached.lock_count, 3)
232
350
  cached.get.cache_clear(cached)
233
351
  self.assertEqual(len(cache), 0)
234
- self.assertEqual(cached.count, 3)
352
+ self.assertEqual(cached.lock_count, 4)
@@ -111,18 +111,6 @@ class LRUDecoratorTest(unittest.TestCase, DecoratorTestMixin):
111
111
  DECORATOR = staticmethod(cachetools.func.lru_cache)
112
112
 
113
113
 
114
- class MRUDecoratorTest(unittest.TestCase, DecoratorTestMixin):
115
- def decorator(self, maxsize, **kwargs):
116
- import warnings
117
-
118
- with warnings.catch_warnings(record=True) as w:
119
- warnings.simplefilter("always")
120
- d = cachetools.func.mru_cache(maxsize, **kwargs)
121
- self.assertNotEqual(len(w), 0)
122
- self.assertIs(w[0].category, DeprecationWarning)
123
- return d
124
-
125
-
126
114
  class RRDecoratorTest(unittest.TestCase, DecoratorTestMixin):
127
115
  DECORATOR = staticmethod(cachetools.func.rr_cache)
128
116
 
@@ -1,63 +0,0 @@
1
- import unittest
2
- import warnings
3
-
4
- from cachetools import MRUCache
5
-
6
- from . import CacheTestMixin
7
-
8
-
9
- class MRUCacheTest(unittest.TestCase, CacheTestMixin):
10
- # TODO: method to create cache that can be overridden
11
- Cache = MRUCache
12
-
13
- def test_evict__writes_only(self):
14
- with warnings.catch_warnings(record=True) as w:
15
- warnings.simplefilter("always")
16
- cache = MRUCache(maxsize=2)
17
- self.assertEqual(len(w), 1)
18
- self.assertIs(w[0].category, DeprecationWarning)
19
-
20
- cache[1] = 1
21
- cache[2] = 2
22
- cache[3] = 3 # Evicts 1 because nothing's been used yet
23
-
24
- assert len(cache) == 2
25
- assert 1 not in cache, "Wrong key was evicted. Should have been '1'."
26
- assert 2 in cache
27
- assert 3 in cache
28
-
29
- def test_evict__with_access(self):
30
- with warnings.catch_warnings(record=True) as w:
31
- warnings.simplefilter("always")
32
- cache = MRUCache(maxsize=2)
33
- self.assertEqual(len(w), 1)
34
- self.assertIs(w[0].category, DeprecationWarning)
35
-
36
- cache[1] = 1
37
- cache[2] = 2
38
- cache[1]
39
- cache[2]
40
- cache[3] = 3 # Evicts 2
41
- assert 2 not in cache, "Wrong key was evicted. Should have been '2'."
42
- assert 1 in cache
43
- assert 3 in cache
44
-
45
- def test_evict__with_delete(self):
46
- with warnings.catch_warnings(record=True) as w:
47
- warnings.simplefilter("always")
48
- cache = MRUCache(maxsize=2)
49
- self.assertEqual(len(w), 1)
50
- self.assertIs(w[0].category, DeprecationWarning)
51
-
52
- cache[1] = 1
53
- cache[2] = 2
54
- del cache[2]
55
- cache[3] = 3 # Doesn't evict anything because we just deleted 2
56
-
57
- assert 2 not in cache
58
- assert 1 in cache
59
-
60
- cache[4] = 4 # Should evict 1 as we just accessed it with __contains__
61
- assert 1 not in cache
62
- assert 3 in cache
63
- assert 4 in cache
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes