cachetools 7.2.5__tar.gz → 7.2.7__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.5 → cachetools-7.2.7}/CHANGELOG.rst +14 -0
  2. {cachetools-7.2.5/src/cachetools.egg-info → cachetools-7.2.7}/PKG-INFO +1 -1
  3. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/__init__.py +93 -81
  4. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/_cached.py +6 -2
  5. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/_cachedmethod.py +6 -2
  6. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/_descriptor.py +43 -37
  7. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/keys.py +22 -11
  8. {cachetools-7.2.5 → cachetools-7.2.7/src/cachetools.egg-info}/PKG-INFO +1 -1
  9. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_cached.py +8 -8
  10. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_cachedmethod.py +99 -26
  11. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_classmethod.py +43 -0
  12. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_keys.py +1 -2
  13. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_threading.py +60 -11
  14. {cachetools-7.2.5 → cachetools-7.2.7}/LICENSE +0 -0
  15. {cachetools-7.2.5 → cachetools-7.2.7}/MANIFEST.in +0 -0
  16. {cachetools-7.2.5 → cachetools-7.2.7}/README.rst +0 -0
  17. {cachetools-7.2.5 → cachetools-7.2.7}/docs/conf.py +0 -0
  18. {cachetools-7.2.5 → cachetools-7.2.7}/docs/index.rst +0 -0
  19. {cachetools-7.2.5 → cachetools-7.2.7}/pyproject.toml +0 -0
  20. {cachetools-7.2.5 → cachetools-7.2.7}/setup.cfg +0 -0
  21. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/__init__.pyi +0 -0
  22. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/func.py +0 -0
  23. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/func.pyi +0 -0
  24. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/keys.pyi +0 -0
  25. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/py.typed +0 -0
  26. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools.egg-info/SOURCES.txt +0 -0
  27. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools.egg-info/dependency_links.txt +0 -0
  28. {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools.egg-info/top_level.txt +0 -0
  29. {cachetools-7.2.5 → cachetools-7.2.7}/tests/__init__.py +0 -0
  30. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_cache.py +0 -0
  31. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_fifo.py +0 -0
  32. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_func.py +0 -0
  33. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_lfu.py +0 -0
  34. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_lru.py +0 -0
  35. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_rr.py +0 -0
  36. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_tlru.py +0 -0
  37. {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_ttl.py +0 -0
  38. {cachetools-7.2.5 → cachetools-7.2.7}/tox.ini +0 -0
@@ -1,3 +1,17 @@
1
+ v7.2.7 (2026-10-28)
2
+ ===================
3
+
4
+ - Various performance improvements.
5
+
6
+ - Minor unit test improvements.
7
+
8
+
9
+ v7.2.6 (2026-10-26)
10
+ ===================
11
+
12
+ - Various performance improvements.
13
+
14
+
1
15
  v7.2.5 (2026-10-25)
2
16
  ===================
3
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.5
3
+ Version: 7.2.7
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.5"
15
+ __version__ = "7.2.7"
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
@@ -193,7 +195,9 @@ class FIFOCache(Cache):
193
195
  except StopIteration:
194
196
  raise KeyError("%s is empty" % type(self).__name__) from None
195
197
  else:
196
- return (key, self.pop(key))
198
+ value = Cache.__getitem__(self, key)
199
+ del self[key]
200
+ return (key, value)
197
201
 
198
202
  def clear(self):
199
203
  Cache.clear(self)
@@ -224,7 +228,7 @@ class LFUCache(Cache):
224
228
 
225
229
  def __getitem__(self, key, cache_getitem=Cache.__getitem__):
226
230
  value = cache_getitem(self, key)
227
- if key in self: # __missing__ may not store item
231
+ if key in self.__links: # __missing__ may not store item
228
232
  self.__touch(key)
229
233
  return value
230
234
 
@@ -257,7 +261,9 @@ class LFUCache(Cache):
257
261
  if curr is root:
258
262
  raise KeyError("%s is empty" % type(self).__name__) from None
259
263
  key = next(iter(curr.keys)) # remove an arbitrary element
260
- return (key, self.pop(key))
264
+ value = Cache.__getitem__(self, key)
265
+ del self[key]
266
+ return (key, value)
261
267
 
262
268
  def clear(self):
263
269
  Cache.clear(self)
@@ -293,8 +299,10 @@ class LRUCache(Cache):
293
299
 
294
300
  def __getitem__(self, key, cache_getitem=Cache.__getitem__):
295
301
  value = cache_getitem(self, key)
296
- if key in self: # __missing__ may not store item
297
- self.__touch(key)
302
+ try:
303
+ self.__order.move_to_end(key)
304
+ except KeyError: # __missing__ may not store item
305
+ pass
298
306
  return value
299
307
 
300
308
  def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
@@ -312,7 +320,9 @@ class LRUCache(Cache):
312
320
  except StopIteration:
313
321
  raise KeyError("%s is empty" % type(self).__name__) from None
314
322
  else:
315
- return (key, self.pop(key))
323
+ value = Cache.__getitem__(self, key)
324
+ del self[key]
325
+ return (key, value)
316
326
 
317
327
  def clear(self):
318
328
  Cache.clear(self)
@@ -361,7 +371,9 @@ class RRCache(Cache):
361
371
  if not keys:
362
372
  raise KeyError("%s is empty" % type(self).__name__) from None
363
373
  key = self.__choice(keys)
364
- return (key, self.pop(key))
374
+ value = Cache.__getitem__(self, key)
375
+ del self[key]
376
+ return (key, value)
365
377
 
366
378
  def clear(self):
367
379
  Cache.clear(self)
@@ -482,27 +494,28 @@ class TTLCache(_TimedCache):
482
494
  return self.timer() < link.expires
483
495
 
484
496
  def __getitem__(self, key, cache_getitem=Cache.__getitem__):
497
+ links = self.__links
485
498
  try:
486
- link = self.__getlink(key)
499
+ link = links[key]
487
500
  except KeyError:
488
- expired = False
501
+ return cache_getitem(self, key)
502
+ links.move_to_end(key)
503
+ if self.timer() < link.expires:
504
+ return cache_getitem(self, key)
489
505
  else:
490
- expired = not (self.timer() < link.expires)
491
- if expired:
492
506
  return self.__missing__(key)
493
- else:
494
- return cache_getitem(self, key)
495
507
 
496
508
  def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
497
509
  with self.timer as time:
498
510
  expires = time + self.__ttl
499
511
  self.expire(time)
500
512
  cache_setitem(self, key, value)
501
- try:
502
- link = self.__getlink(key)
503
- except KeyError:
504
- 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)
505
517
  else:
518
+ links.move_to_end(key)
506
519
  link.unlink()
507
520
  link.expires = expires
508
521
  link.next = root = self.__root
@@ -550,6 +563,8 @@ class TTLCache(_TimedCache):
550
563
  time = self.timer()
551
564
  root = self.__root
552
565
  curr = root.next
566
+ if curr is root or time < curr.expires:
567
+ return [] # fast path: nothing has expired
553
568
  links = self.__links
554
569
  expired = []
555
570
  cache_delitem = Cache.__delitem__
@@ -575,7 +590,9 @@ class TTLCache(_TimedCache):
575
590
  except StopIteration:
576
591
  raise KeyError("%s is empty" % type(self).__name__) from None
577
592
  else:
578
- return (key, self.pop(key))
593
+ value = Cache.__getitem__(self, key)
594
+ del self[key]
595
+ return (key, value)
579
596
 
580
597
  def clear(self):
581
598
  _TimedCache.clear(self)
@@ -583,11 +600,6 @@ class TTLCache(_TimedCache):
583
600
  root.prev = root.next = root
584
601
  self.__links.clear()
585
602
 
586
- def __getlink(self, key):
587
- value = self.__links[key]
588
- self.__links.move_to_end(key)
589
- return value
590
-
591
603
 
592
604
  class TLRUCache(_TimedCache):
593
605
  """Time-aware Least Recently Used (TLRU) cache implementation."""
@@ -621,16 +633,16 @@ class TLRUCache(_TimedCache):
621
633
  return self.timer() < item.expires
622
634
 
623
635
  def __getitem__(self, key, cache_getitem=Cache.__getitem__):
636
+ items = self.__items
624
637
  try:
625
- item = self.__getitem(key)
638
+ item = items[key]
626
639
  except KeyError:
627
- expired = False
640
+ return cache_getitem(self, key)
641
+ items.move_to_end(key)
642
+ if self.timer() < item.expires:
643
+ return cache_getitem(self, key)
628
644
  else:
629
- expired = not (self.timer() < item.expires)
630
- if expired:
631
645
  return self.__missing__(key)
632
- else:
633
- return cache_getitem(self, key)
634
646
 
635
647
  def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
636
648
  with self.timer as time:
@@ -643,11 +655,12 @@ class TLRUCache(_TimedCache):
643
655
  cache_setitem(self, key, value)
644
656
  # removing an existing item would break the heap structure, so
645
657
  # only mark it as removed for now
646
- try:
647
- self.__getitem(key).removed = True
648
- except KeyError:
649
- pass
650
- 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)
651
664
  heapq.heappush(self.__order, item)
652
665
 
653
666
  def __delitem__(self, key, cache_delitem=Cache.__delitem__):
@@ -684,6 +697,8 @@ class TLRUCache(_TimedCache):
684
697
  if len(order) > len(items) * self.__HEAP_CLEANUP_FACTOR:
685
698
  self.__order = order = [item for item in order if not item.removed]
686
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
687
702
  expired = []
688
703
  cache_delitem = Cache.__delitem__
689
704
  cache_getitem = Cache.__getitem__
@@ -707,18 +722,15 @@ class TLRUCache(_TimedCache):
707
722
  except StopIteration:
708
723
  raise KeyError("%s is empty" % type(self).__name__) from None
709
724
  else:
710
- return (key, self.pop(key))
725
+ value = Cache.__getitem__(self, key)
726
+ del self[key]
727
+ return (key, value)
711
728
 
712
729
  def clear(self):
713
730
  _TimedCache.clear(self)
714
731
  self.__items.clear()
715
732
  del self.__order[:]
716
733
 
717
- def __getitem(self, key):
718
- value = self.__items[key]
719
- self.__items.move_to_end(key)
720
- return value
721
-
722
734
  def __delitem(self, key, cache_delitem=Cache.__delitem__):
723
735
  try:
724
736
  self.__items.pop(key).removed = True
@@ -25,7 +25,9 @@ def _condition_info(func, cache, key, lock, cond, info):
25
25
  nonlocal hits, misses
26
26
  k = key(*args, **kwargs)
27
27
  with lock:
28
- cond.wait_for(lambda: k not in pending)
28
+ # apparently significantly faster than cond.wait_for(...)
29
+ while k in pending:
30
+ cond.wait()
29
31
  try:
30
32
  result = cache[k]
31
33
  hits += 1
@@ -158,7 +160,9 @@ def _condition(func, cache, key, lock, cond):
158
160
  def wrapper(*args, **kwargs):
159
161
  k = key(*args, **kwargs)
160
162
  with lock:
161
- cond.wait_for(lambda: k not in pending)
163
+ # apparently significantly faster than cond.wait_for(...)
164
+ while k in pending:
165
+ cond.wait()
162
166
  try:
163
167
  result = cache[k]
164
168
  return result
@@ -79,7 +79,9 @@ def _condition_info(method, cache, key, lock, cond, info):
79
79
  key = self.cache_key(*args, **kwargs)
80
80
 
81
81
  with lock:
82
- cond.wait_for(lambda: key not in self.__pending)
82
+ # apparently significantly faster than cond.wait_for(...)
83
+ while key in self.__pending:
84
+ cond.wait()
83
85
  try:
84
86
  result = cache[key]
85
87
  self.__hits += 1
@@ -193,7 +195,9 @@ def _condition(method, cache, key, lock, cond):
193
195
  c = cache(self)
194
196
  k = key(self, *args, **kwargs)
195
197
  with lock(self):
196
- cond(self).wait_for(lambda: k not in pending)
198
+ # apparently significantly faster than cond.wait_for(...)
199
+ while k in pending:
200
+ cond(self).wait()
197
201
  try:
198
202
  return c[k]
199
203
  except KeyError:
@@ -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.
@@ -4,20 +4,31 @@ __all__ = ("hashkey", "methodkey", "typedkey", "typedmethodkey")
4
4
 
5
5
 
6
6
  class _HashedTuple(tuple):
7
- """A tuple that ensures that hash() will be called no more than once
8
- per element, since cache decorators will hash the key multiple
9
- times on a cache miss. See also _HashedSeq in the standard
10
- library functools implementation.
7
+ """A tuple subclass used for cache keys.
11
8
 
12
- """
9
+ Unlike _HashedSeq in the standard library functools implementation,
10
+ the hash value is not cached: the built-in tuple hash is faster than
11
+ a Python-level __hash__ that caches it.
13
12
 
14
- __hashvalue = None # default value, set in instance on first use
13
+ """
15
14
 
16
- def __hash__(self, hash=tuple.__hash__):
17
- hashvalue = self.__hashvalue
18
- if hashvalue is None:
19
- self.__hashvalue = hashvalue = hash(self)
20
- return hashvalue
15
+ # A Python-level __hash__ that caches the hash costs more than it
16
+ # saves: hashing a 2-tuple three times takes 161 ns as _HashedTuple
17
+ # and 49 ns as a plain tuple (C tuple hash, 18 ns each); a dict
18
+ # lookup keyed by one takes 50 vs 12 ns. Every cache operation
19
+ # hashes the key again (3 __hash__ calls per @cached hit, 12 per
20
+ # evicting miss by cProfile, 11.5% of miss time in __hash__ alone).
21
+ # The cached hash does not pay off even for a key wrapping a 200-int
22
+ # tuple (166 vs 49 ns for three hashes), only for elements whose own
23
+ # __hash__ is slow.
24
+ #
25
+ # __hashvalue = None # default value, set in instance on first use
26
+ #
27
+ # def __hash__(self, hash=tuple.__hash__):
28
+ # hashvalue = self.__hashvalue
29
+ # if hashvalue is None:
30
+ # self.__hashvalue = hashvalue = hash(self)
31
+ # return hashvalue
21
32
 
22
33
  def __add__(self, other, add=tuple.__add__):
23
34
  return _HashedTuple(add(self, other))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.5
3
+ Version: 7.2.7
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>
@@ -98,15 +98,15 @@ class DecoratorTestMixin(SimplifiedTestCaseProtocol):
98
98
  self.assertEqual(len(cache), 0)
99
99
  self.assertEqual(wrapper(0), 0)
100
100
  self.assertEqual(lock.count, 3)
101
- self.assertEqual(cond.wait_count, 1)
101
+ self.assertEqual(cond.wait_count, 0)
102
102
  self.assertEqual(cond.notify_count, 1)
103
103
  self.assertEqual(wrapper(1), 1)
104
104
  self.assertEqual(lock.count, 6)
105
- self.assertEqual(cond.wait_count, 2)
105
+ self.assertEqual(cond.wait_count, 0)
106
106
  self.assertEqual(cond.notify_count, 2)
107
107
  self.assertEqual(wrapper(1), 1)
108
108
  self.assertEqual(lock.count, 7)
109
- self.assertEqual(cond.wait_count, 3)
109
+ self.assertEqual(cond.wait_count, 0)
110
110
  self.assertEqual(cond.notify_count, 2)
111
111
 
112
112
  def test_decorator_lock_cond(self):
@@ -118,15 +118,15 @@ class DecoratorTestMixin(SimplifiedTestCaseProtocol):
118
118
  self.assertEqual(len(cache), 0)
119
119
  self.assertEqual(wrapper(0), 0)
120
120
  self.assertEqual(lock.count, 3)
121
- self.assertEqual(cond.wait_count, 1)
121
+ self.assertEqual(cond.wait_count, 0)
122
122
  self.assertEqual(cond.notify_count, 1)
123
123
  self.assertEqual(wrapper(1), 1)
124
124
  self.assertEqual(lock.count, 6)
125
- self.assertEqual(cond.wait_count, 2)
125
+ self.assertEqual(cond.wait_count, 0)
126
126
  self.assertEqual(cond.notify_count, 2)
127
127
  self.assertEqual(wrapper(1), 1)
128
128
  self.assertEqual(lock.count, 7)
129
- self.assertEqual(cond.wait_count, 3)
129
+ self.assertEqual(cond.wait_count, 0)
130
130
  self.assertEqual(cond.notify_count, 2)
131
131
 
132
132
  def test_decorator_cond_error(self):
@@ -138,7 +138,7 @@ class DecoratorTestMixin(SimplifiedTestCaseProtocol):
138
138
  wrapper(0)
139
139
  self.assertEqual(len(cache), 0)
140
140
  self.assertEqual(lock.count, 2)
141
- self.assertEqual(cond.wait_count, 1)
141
+ self.assertEqual(cond.wait_count, 0)
142
142
  self.assertEqual(cond.notify_count, 1)
143
143
 
144
144
  # verify pending set is cleaned up, otherwise this might deadlock
@@ -146,7 +146,7 @@ class DecoratorTestMixin(SimplifiedTestCaseProtocol):
146
146
  wrapper(0)
147
147
  self.assertEqual(len(cache), 0)
148
148
  self.assertEqual(lock.count, 4)
149
- self.assertEqual(cond.wait_count, 2)
149
+ self.assertEqual(cond.wait_count, 0)
150
150
  self.assertEqual(cond.notify_count, 2)
151
151
 
152
152
  def test_decorator_wrapped(self):
@@ -150,29 +150,29 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
150
150
 
151
151
  self.assertEqual(cached.get_cond(0), 0)
152
152
  self.assertEqual(cached.cond.count, 3)
153
- self.assertEqual(cached.cond.wait_count, 1)
153
+ self.assertEqual(cached.cond.wait_count, 0)
154
154
  self.assertEqual(cached.cond.notify_count, 1)
155
155
  self.assertEqual(cached.get_cond(1), 1)
156
156
  self.assertEqual(cached.cond.count, 6)
157
- self.assertEqual(cached.cond.wait_count, 2)
157
+ self.assertEqual(cached.cond.wait_count, 0)
158
158
  self.assertEqual(cached.cond.notify_count, 2)
159
159
  self.assertEqual(cached.get_cond(1), 1)
160
160
  self.assertEqual(cached.cond.count, 7)
161
- self.assertEqual(cached.cond.wait_count, 3)
161
+ self.assertEqual(cached.cond.wait_count, 0)
162
162
  self.assertEqual(cached.cond.notify_count, 2)
163
163
  self.assertEqual(cached.get_cond(1.0), 1)
164
164
  self.assertEqual(cached.cond.count, 8)
165
- self.assertEqual(cached.cond.wait_count, 4)
165
+ self.assertEqual(cached.cond.wait_count, 0)
166
166
  self.assertEqual(cached.cond.notify_count, 2)
167
167
  self.assertEqual(cached.get_cond(1.0), 1)
168
168
  self.assertEqual(cached.cond.count, 9)
169
- self.assertEqual(cached.cond.wait_count, 5)
169
+ self.assertEqual(cached.cond.wait_count, 0)
170
170
  self.assertEqual(cached.cond.notify_count, 2)
171
171
 
172
172
  cached.cache.clear()
173
173
  self.assertEqual(cached.get_cond(1), 2)
174
174
  self.assertEqual(cached.cond.count, 12)
175
- self.assertEqual(cached.cond.wait_count, 6)
175
+ self.assertEqual(cached.cond.wait_count, 0)
176
176
  self.assertEqual(cached.cond.notify_count, 3)
177
177
 
178
178
  def test_decorator_info(self):
@@ -255,19 +255,19 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
255
255
  self.assertEqual(cached.cond.count, 1)
256
256
  self.assertEqual(wrapper(0), 0)
257
257
  self.assertEqual(cached.cond.count, 4)
258
- self.assertEqual(cached.cond.wait_count, 1)
258
+ self.assertEqual(cached.cond.wait_count, 0)
259
259
  self.assertEqual(cached.cond.notify_count, 1)
260
260
  self.assertEqual(wrapper.cache_info(), (0, 1, maxsize, 1))
261
261
  self.assertEqual(cached.cond.count, 5)
262
262
  self.assertEqual(wrapper(1), 1)
263
263
  self.assertEqual(cached.cond.count, 8)
264
- self.assertEqual(cached.cond.wait_count, 2)
264
+ self.assertEqual(cached.cond.wait_count, 0)
265
265
  self.assertEqual(cached.cond.notify_count, 2)
266
266
  self.assertEqual(wrapper.cache_info(), (0, 2, maxsize, 2))
267
267
  self.assertEqual(cached.cond.count, 9)
268
268
  self.assertEqual(wrapper(0), 0)
269
269
  self.assertEqual(cached.cond.count, 10)
270
- self.assertEqual(cached.cond.wait_count, 3)
270
+ self.assertEqual(cached.cond.wait_count, 0)
271
271
  self.assertEqual(cached.cond.notify_count, 2)
272
272
  self.assertEqual(wrapper.cache_info(), (1, 2, maxsize, 2))
273
273
  self.assertEqual(cached.cond.count, 11)
@@ -279,13 +279,13 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
279
279
  self.assertEqual(cached.cond.count, 13)
280
280
  self.assertEqual(wrapper(0), 2)
281
281
  self.assertEqual(cached.cond.count, 16)
282
- self.assertEqual(cached.cond.wait_count, 4)
282
+ self.assertEqual(cached.cond.wait_count, 0)
283
283
  self.assertEqual(cached.cond.notify_count, 3)
284
284
  self.assertEqual(wrapper.cache_info(), (0, 1, maxsize, 1))
285
285
  self.assertEqual(cached.cond.count, 17)
286
286
  self.assertEqual(wrapper(0), 2)
287
287
  self.assertEqual(cached.cond.count, 18)
288
- self.assertEqual(cached.cond.wait_count, 5)
288
+ self.assertEqual(cached.cond.wait_count, 0)
289
289
  self.assertEqual(cached.cond.notify_count, 3)
290
290
  self.assertEqual(wrapper.cache_info(), (1, 1, maxsize, 1))
291
291
  self.assertEqual(cached.cond.count, 19)
@@ -306,19 +306,19 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
306
306
  self.assertEqual(cached.lock.count, 1)
307
307
  self.assertEqual(wrapper(0), 0)
308
308
  self.assertEqual(cached.lock.count, 4)
309
- self.assertEqual(cached.cond.wait_count, 1)
309
+ self.assertEqual(cached.cond.wait_count, 0)
310
310
  self.assertEqual(cached.cond.notify_count, 1)
311
311
  self.assertEqual(wrapper.cache_info(), (0, 1, maxsize, 1))
312
312
  self.assertEqual(cached.lock.count, 5)
313
313
  self.assertEqual(wrapper(1), 1)
314
314
  self.assertEqual(cached.lock.count, 8)
315
- self.assertEqual(cached.cond.wait_count, 2)
315
+ self.assertEqual(cached.cond.wait_count, 0)
316
316
  self.assertEqual(cached.cond.notify_count, 2)
317
317
  self.assertEqual(wrapper.cache_info(), (0, 2, maxsize, 2))
318
318
  self.assertEqual(cached.lock.count, 9)
319
319
  self.assertEqual(wrapper(0), 0)
320
320
  self.assertEqual(cached.lock.count, 10)
321
- self.assertEqual(cached.cond.wait_count, 3)
321
+ self.assertEqual(cached.cond.wait_count, 0)
322
322
  self.assertEqual(cached.cond.notify_count, 2)
323
323
  self.assertEqual(wrapper.cache_info(), (1, 2, maxsize, 2))
324
324
  self.assertEqual(cached.lock.count, 11)
@@ -330,13 +330,13 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
330
330
  self.assertEqual(cached.lock.count, 13)
331
331
  self.assertEqual(wrapper(0), 2)
332
332
  self.assertEqual(cached.lock.count, 16)
333
- self.assertEqual(cached.cond.wait_count, 4)
333
+ self.assertEqual(cached.cond.wait_count, 0)
334
334
  self.assertEqual(cached.cond.notify_count, 3)
335
335
  self.assertEqual(wrapper.cache_info(), (0, 1, maxsize, 1))
336
336
  self.assertEqual(cached.lock.count, 17)
337
337
  self.assertEqual(wrapper(0), 2)
338
338
  self.assertEqual(cached.lock.count, 18)
339
- self.assertEqual(cached.cond.wait_count, 5)
339
+ self.assertEqual(cached.cond.wait_count, 0)
340
340
  self.assertEqual(cached.cond.notify_count, 3)
341
341
  self.assertEqual(wrapper.cache_info(), (1, 1, maxsize, 1))
342
342
  self.assertEqual(cached.lock.count, 19)
@@ -352,14 +352,14 @@ class MethodDecoratorTestMixin(SimplifiedTestCaseProtocol):
352
352
  with self.assertRaises(ValueError):
353
353
  cached.get_cond_error(0)
354
354
  self.assertEqual(cached.cond.count, 2)
355
- self.assertEqual(cached.cond.wait_count, 1)
355
+ self.assertEqual(cached.cond.wait_count, 0)
356
356
  self.assertEqual(cached.cond.notify_count, 1)
357
357
 
358
358
  # verify pending set is cleaned up, otherwise this might deadlock
359
359
  with self.assertRaises(ValueError):
360
360
  cached.get_cond_error(0)
361
361
  self.assertEqual(cached.cond.count, 4)
362
- self.assertEqual(cached.cond.wait_count, 2)
362
+ self.assertEqual(cached.cond.wait_count, 0)
363
363
  self.assertEqual(cached.cond.notify_count, 2)
364
364
 
365
365
  def test_decorator_wrapped(self):
@@ -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
@@ -701,36 +774,36 @@ class CacheMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
701
774
 
702
775
  self.assertEqual(cached.get_cond(0), 0)
703
776
  self.assertEqual(cached.cond.count, 3)
704
- self.assertEqual(cached.cond.wait_count, 1)
777
+ self.assertEqual(cached.cond.wait_count, 0)
705
778
  self.assertEqual(cached.cond.notify_count, 1)
706
779
  self.assertEqual(cached.get_cond(1), 1)
707
780
  self.assertEqual(cached.cond.count, 6)
708
- self.assertEqual(cached.cond.wait_count, 2)
781
+ self.assertEqual(cached.cond.wait_count, 0)
709
782
  self.assertEqual(cached.cond.notify_count, 2)
710
783
  self.assertEqual(cached.get_cond(1), 2)
711
784
  self.assertEqual(cached.cond.count, 9)
712
- self.assertEqual(cached.cond.wait_count, 3)
785
+ self.assertEqual(cached.cond.wait_count, 0)
713
786
  self.assertEqual(cached.cond.notify_count, 3)
714
787
  self.assertEqual(cached.get_cond(1.0), 3)
715
788
  self.assertEqual(cached.cond.count, 12)
716
- self.assertEqual(cached.cond.wait_count, 4)
789
+ self.assertEqual(cached.cond.wait_count, 0)
717
790
  self.assertEqual(cached.cond.notify_count, 4)
718
791
  self.assertEqual(cached.get_cond(1.0), 4)
719
792
  self.assertEqual(cached.cond.count, 15)
720
- self.assertEqual(cached.cond.wait_count, 5)
793
+ self.assertEqual(cached.cond.wait_count, 0)
721
794
  self.assertEqual(cached.cond.notify_count, 5)
722
795
 
723
796
  self.assertEqual(cached.get_cond_info(0), 5)
724
797
  self.assertEqual(cached.cond.count, 18)
725
- self.assertEqual(cached.cond.wait_count, 6)
798
+ self.assertEqual(cached.cond.wait_count, 0)
726
799
  self.assertEqual(cached.cond.notify_count, 6)
727
800
  self.assertEqual(cached.get_cond_info(1), 6)
728
801
  self.assertEqual(cached.cond.count, 21)
729
- self.assertEqual(cached.cond.wait_count, 7)
802
+ self.assertEqual(cached.cond.wait_count, 0)
730
803
  self.assertEqual(cached.cond.notify_count, 7)
731
804
  self.assertEqual(cached.get_cond(1.0), 7)
732
805
  self.assertEqual(cached.cond.count, 24)
733
- self.assertEqual(cached.cond.wait_count, 8)
806
+ self.assertEqual(cached.cond.wait_count, 0)
734
807
  self.assertEqual(cached.cond.notify_count, 8)
735
808
 
736
809
  def test_shared_cache(self):
@@ -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
@@ -82,10 +82,9 @@ class CacheKeysTest(unittest.TestCase):
82
82
  import pickle
83
83
 
84
84
  for k in [key(), key("abc"), key("abc", 123), key("abc", q="abc")]:
85
- # white-box test: assert cached hash value is not pickled
86
85
  self.assertEqual(len(k.__dict__), 0)
87
86
  h = hash(k)
88
- self.assertEqual(len(k.__dict__), 1)
87
+ self.assertEqual(h, hash(tuple(k)))
89
88
  pickled = pickle.loads(pickle.dumps(k))
90
89
  self.assertEqual(len(pickled.__dict__), 0)
91
90
  self.assertEqual(k, pickled)
@@ -1,9 +1,19 @@
1
1
  import unittest
2
- from threading import Barrier, Condition, Lock, Thread
2
+ from threading import Barrier, Condition, Event, Lock, Thread
3
3
 
4
4
  from cachetools import LRUCache, cached, cachedmethod
5
5
 
6
6
 
7
+ class WaitCondition(Condition):
8
+ def __init__(self):
9
+ super().__init__()
10
+ self.waiting = Event()
11
+
12
+ def wait(self, timeout=None):
13
+ self.waiting.set()
14
+ return super().wait(timeout)
15
+
16
+
7
17
  class ThreadingTest(unittest.TestCase):
8
18
  NTHREADS = 10
9
19
 
@@ -43,27 +53,66 @@ class ThreadingTest(unittest.TestCase):
43
53
  for info in (False, True):
44
54
  with self.subTest(info=info):
45
55
  cache = LRUCache(1)
56
+ cond = WaitCondition()
57
+ started = Event()
58
+ release = Event()
59
+ calls = []
46
60
 
47
- @cached(cache, condition=Condition(), info=info)
48
- def func():
49
- result = object()
50
- return result
61
+ @cached(cache, condition=cond, info=info)
62
+ def func(calls=calls, started=started, release=release):
63
+ calls.append(None)
64
+ started.set()
65
+ release.wait(self.TIMEOUT)
66
+ return object()
51
67
 
52
- self.run_cond_test(func, cache, info)
68
+ self.run_cond_wait_test(func, cond, started, release, calls)
53
69
 
54
70
  def test_cachedmethod_cond(self):
55
71
  for info in (False, True):
56
72
  with self.subTest(info=info):
57
73
  cache = LRUCache(1)
58
- cond = Condition()
74
+ cond = WaitCondition()
75
+ started = Event()
76
+ release = Event()
77
+ calls = []
59
78
 
60
79
  class Cached:
61
80
  @cachedmethod(lambda _: cache, condition=lambda _: cond, info=info)
62
- def method(self):
63
- result = object()
64
- return result
81
+ def method(
82
+ self,
83
+ calls=calls,
84
+ started=started,
85
+ release=release,
86
+ timeout=self.TIMEOUT,
87
+ ):
88
+ calls.append(None)
89
+ started.set()
90
+ release.wait(timeout)
91
+ return object()
92
+
93
+ func = Cached().method
94
+ self.run_cond_wait_test(func, cond, started, release, calls)
95
+
96
+ def run_cond_wait_test(self, func, cond, started, release, calls):
97
+ results = [None] * 2
98
+
99
+ def run(index):
100
+ results[index] = func()
65
101
 
66
- self.run_cond_test(Cached().method, cache, info)
102
+ first = Thread(target=run, args=[0])
103
+ first.start()
104
+ self.assertTrue(started.wait(self.TIMEOUT))
105
+ second = Thread(target=run, args=[1])
106
+ second.start()
107
+ # second caller must block on the pending key
108
+ self.assertTrue(cond.waiting.wait(self.TIMEOUT))
109
+ release.set()
110
+ for thread in (first, second):
111
+ thread.join(timeout=self.TIMEOUT)
112
+ self.assertFalse(thread.is_alive())
113
+ self.assertEqual(len(calls), 1)
114
+ self.assertIsNotNone(results[0])
115
+ self.assertIs(results[0], results[1])
67
116
 
68
117
  def run_lock_test(self, func, cache, info):
69
118
  self.run_threading_test(func, cache)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes