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.
- {cachetools-7.2.5 → cachetools-7.2.7}/CHANGELOG.rst +14 -0
- {cachetools-7.2.5/src/cachetools.egg-info → cachetools-7.2.7}/PKG-INFO +1 -1
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/__init__.py +93 -81
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/_cached.py +6 -2
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/_cachedmethod.py +6 -2
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/_descriptor.py +43 -37
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/keys.py +22 -11
- {cachetools-7.2.5 → cachetools-7.2.7/src/cachetools.egg-info}/PKG-INFO +1 -1
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_cached.py +8 -8
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_cachedmethod.py +99 -26
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_classmethod.py +43 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_keys.py +1 -2
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_threading.py +60 -11
- {cachetools-7.2.5 → cachetools-7.2.7}/LICENSE +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/MANIFEST.in +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/README.rst +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/docs/conf.py +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/docs/index.rst +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/pyproject.toml +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/setup.cfg +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/__init__.pyi +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/func.py +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/func.pyi +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/keys.pyi +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools/py.typed +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools.egg-info/SOURCES.txt +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools.egg-info/dependency_links.txt +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/src/cachetools.egg-info/top_level.txt +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/__init__.py +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_cache.py +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_fifo.py +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_func.py +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_lfu.py +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_lru.py +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_rr.py +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_tlru.py +0 -0
- {cachetools-7.2.5 → cachetools-7.2.7}/tests/test_ttl.py +0 -0
- {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
|
|
|
@@ -12,7 +12,7 @@ __all__ = (
|
|
|
12
12
|
"cachedmethod",
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
__version__ = "7.2.
|
|
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 =
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
499
|
+
link = links[key]
|
|
487
500
|
except KeyError:
|
|
488
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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 =
|
|
638
|
+
item = items[key]
|
|
626
639
|
except KeyError:
|
|
627
|
-
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
13
|
+
"""
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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))
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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=
|
|
48
|
-
def func():
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
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 =
|
|
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(
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
|
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
|