cachetools 7.2.5__tar.gz → 7.2.6__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.6}/CHANGELOG.rst +6 -0
  2. {cachetools-7.2.5/src/cachetools.egg-info → cachetools-7.2.6}/PKG-INFO +1 -1
  3. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools/__init__.py +36 -22
  4. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools/_cached.py +6 -2
  5. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools/_cachedmethod.py +6 -2
  6. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools/keys.py +22 -11
  7. {cachetools-7.2.5 → cachetools-7.2.6/src/cachetools.egg-info}/PKG-INFO +1 -1
  8. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_cached.py +8 -8
  9. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_cachedmethod.py +26 -26
  10. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_keys.py +1 -2
  11. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_threading.py +60 -11
  12. {cachetools-7.2.5 → cachetools-7.2.6}/LICENSE +0 -0
  13. {cachetools-7.2.5 → cachetools-7.2.6}/MANIFEST.in +0 -0
  14. {cachetools-7.2.5 → cachetools-7.2.6}/README.rst +0 -0
  15. {cachetools-7.2.5 → cachetools-7.2.6}/docs/conf.py +0 -0
  16. {cachetools-7.2.5 → cachetools-7.2.6}/docs/index.rst +0 -0
  17. {cachetools-7.2.5 → cachetools-7.2.6}/pyproject.toml +0 -0
  18. {cachetools-7.2.5 → cachetools-7.2.6}/setup.cfg +0 -0
  19. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools/__init__.pyi +0 -0
  20. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools/_descriptor.py +0 -0
  21. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools/func.py +0 -0
  22. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools/func.pyi +0 -0
  23. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools/keys.pyi +0 -0
  24. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools/py.typed +0 -0
  25. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools.egg-info/SOURCES.txt +0 -0
  26. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools.egg-info/dependency_links.txt +0 -0
  27. {cachetools-7.2.5 → cachetools-7.2.6}/src/cachetools.egg-info/top_level.txt +0 -0
  28. {cachetools-7.2.5 → cachetools-7.2.6}/tests/__init__.py +0 -0
  29. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_cache.py +0 -0
  30. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_classmethod.py +0 -0
  31. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_fifo.py +0 -0
  32. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_func.py +0 -0
  33. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_lfu.py +0 -0
  34. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_lru.py +0 -0
  35. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_rr.py +0 -0
  36. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_tlru.py +0 -0
  37. {cachetools-7.2.5 → cachetools-7.2.6}/tests/test_ttl.py +0 -0
  38. {cachetools-7.2.5 → cachetools-7.2.6}/tox.ini +0 -0
@@ -1,3 +1,9 @@
1
+ v7.2.6 (2026-10-26)
2
+ ===================
3
+
4
+ - Various performance improvements.
5
+
6
+
1
7
  v7.2.5 (2026-10-25)
2
8
  ===================
3
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachetools
3
- Version: 7.2.5
3
+ Version: 7.2.6
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.6"
16
16
 
17
17
  import collections
18
18
  import collections.abc
@@ -193,7 +193,9 @@ class FIFOCache(Cache):
193
193
  except StopIteration:
194
194
  raise KeyError("%s is empty" % type(self).__name__) from None
195
195
  else:
196
- return (key, self.pop(key))
196
+ value = Cache.__getitem__(self, key)
197
+ del self[key]
198
+ return (key, value)
197
199
 
198
200
  def clear(self):
199
201
  Cache.clear(self)
@@ -224,7 +226,7 @@ class LFUCache(Cache):
224
226
 
225
227
  def __getitem__(self, key, cache_getitem=Cache.__getitem__):
226
228
  value = cache_getitem(self, key)
227
- if key in self: # __missing__ may not store item
229
+ if key in self.__links: # __missing__ may not store item
228
230
  self.__touch(key)
229
231
  return value
230
232
 
@@ -257,7 +259,9 @@ class LFUCache(Cache):
257
259
  if curr is root:
258
260
  raise KeyError("%s is empty" % type(self).__name__) from None
259
261
  key = next(iter(curr.keys)) # remove an arbitrary element
260
- return (key, self.pop(key))
262
+ value = Cache.__getitem__(self, key)
263
+ del self[key]
264
+ return (key, value)
261
265
 
262
266
  def clear(self):
263
267
  Cache.clear(self)
@@ -293,8 +297,10 @@ class LRUCache(Cache):
293
297
 
294
298
  def __getitem__(self, key, cache_getitem=Cache.__getitem__):
295
299
  value = cache_getitem(self, key)
296
- if key in self: # __missing__ may not store item
297
- self.__touch(key)
300
+ try:
301
+ self.__order.move_to_end(key)
302
+ except KeyError: # __missing__ may not store item
303
+ pass
298
304
  return value
299
305
 
300
306
  def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
@@ -312,7 +318,9 @@ class LRUCache(Cache):
312
318
  except StopIteration:
313
319
  raise KeyError("%s is empty" % type(self).__name__) from None
314
320
  else:
315
- return (key, self.pop(key))
321
+ value = Cache.__getitem__(self, key)
322
+ del self[key]
323
+ return (key, value)
316
324
 
317
325
  def clear(self):
318
326
  Cache.clear(self)
@@ -361,7 +369,9 @@ class RRCache(Cache):
361
369
  if not keys:
362
370
  raise KeyError("%s is empty" % type(self).__name__) from None
363
371
  key = self.__choice(keys)
364
- return (key, self.pop(key))
372
+ value = Cache.__getitem__(self, key)
373
+ del self[key]
374
+ return (key, value)
365
375
 
366
376
  def clear(self):
367
377
  Cache.clear(self)
@@ -482,16 +492,16 @@ class TTLCache(_TimedCache):
482
492
  return self.timer() < link.expires
483
493
 
484
494
  def __getitem__(self, key, cache_getitem=Cache.__getitem__):
495
+ links = self.__links
485
496
  try:
486
- link = self.__getlink(key)
497
+ link = links[key]
487
498
  except KeyError:
488
- expired = False
499
+ return cache_getitem(self, key)
500
+ links.move_to_end(key)
501
+ if self.timer() < link.expires:
502
+ return cache_getitem(self, key)
489
503
  else:
490
- expired = not (self.timer() < link.expires)
491
- if expired:
492
504
  return self.__missing__(key)
493
- else:
494
- return cache_getitem(self, key)
495
505
 
496
506
  def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
497
507
  with self.timer as time:
@@ -575,7 +585,9 @@ class TTLCache(_TimedCache):
575
585
  except StopIteration:
576
586
  raise KeyError("%s is empty" % type(self).__name__) from None
577
587
  else:
578
- return (key, self.pop(key))
588
+ value = Cache.__getitem__(self, key)
589
+ del self[key]
590
+ return (key, value)
579
591
 
580
592
  def clear(self):
581
593
  _TimedCache.clear(self)
@@ -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:
@@ -707,7 +719,9 @@ class TLRUCache(_TimedCache):
707
719
  except StopIteration:
708
720
  raise KeyError("%s is empty" % type(self).__name__) from None
709
721
  else:
710
- return (key, self.pop(key))
722
+ value = Cache.__getitem__(self, key)
723
+ del self[key]
724
+ return (key, value)
711
725
 
712
726
  def clear(self):
713
727
  _TimedCache.clear(self)
@@ -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:
@@ -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.6
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):
@@ -701,36 +701,36 @@ class CacheMethodTest(unittest.TestCase, MethodDecoratorTestMixin):
701
701
 
702
702
  self.assertEqual(cached.get_cond(0), 0)
703
703
  self.assertEqual(cached.cond.count, 3)
704
- self.assertEqual(cached.cond.wait_count, 1)
704
+ self.assertEqual(cached.cond.wait_count, 0)
705
705
  self.assertEqual(cached.cond.notify_count, 1)
706
706
  self.assertEqual(cached.get_cond(1), 1)
707
707
  self.assertEqual(cached.cond.count, 6)
708
- self.assertEqual(cached.cond.wait_count, 2)
708
+ self.assertEqual(cached.cond.wait_count, 0)
709
709
  self.assertEqual(cached.cond.notify_count, 2)
710
710
  self.assertEqual(cached.get_cond(1), 2)
711
711
  self.assertEqual(cached.cond.count, 9)
712
- self.assertEqual(cached.cond.wait_count, 3)
712
+ self.assertEqual(cached.cond.wait_count, 0)
713
713
  self.assertEqual(cached.cond.notify_count, 3)
714
714
  self.assertEqual(cached.get_cond(1.0), 3)
715
715
  self.assertEqual(cached.cond.count, 12)
716
- self.assertEqual(cached.cond.wait_count, 4)
716
+ self.assertEqual(cached.cond.wait_count, 0)
717
717
  self.assertEqual(cached.cond.notify_count, 4)
718
718
  self.assertEqual(cached.get_cond(1.0), 4)
719
719
  self.assertEqual(cached.cond.count, 15)
720
- self.assertEqual(cached.cond.wait_count, 5)
720
+ self.assertEqual(cached.cond.wait_count, 0)
721
721
  self.assertEqual(cached.cond.notify_count, 5)
722
722
 
723
723
  self.assertEqual(cached.get_cond_info(0), 5)
724
724
  self.assertEqual(cached.cond.count, 18)
725
- self.assertEqual(cached.cond.wait_count, 6)
725
+ self.assertEqual(cached.cond.wait_count, 0)
726
726
  self.assertEqual(cached.cond.notify_count, 6)
727
727
  self.assertEqual(cached.get_cond_info(1), 6)
728
728
  self.assertEqual(cached.cond.count, 21)
729
- self.assertEqual(cached.cond.wait_count, 7)
729
+ self.assertEqual(cached.cond.wait_count, 0)
730
730
  self.assertEqual(cached.cond.notify_count, 7)
731
731
  self.assertEqual(cached.get_cond(1.0), 7)
732
732
  self.assertEqual(cached.cond.count, 24)
733
- self.assertEqual(cached.cond.wait_count, 8)
733
+ self.assertEqual(cached.cond.wait_count, 0)
734
734
  self.assertEqual(cached.cond.notify_count, 8)
735
735
 
736
736
  def test_shared_cache(self):
@@ -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