Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/cachetools/__init__.py: 76%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Extensible memoizing collections and decorators."""
3__all__ = (
4 "Cache",
5 "FIFOCache",
6 "LFUCache",
7 "LRUCache",
8 "RRCache",
9 "TLRUCache",
10 "TTLCache",
11 "cached",
12 "cachedmethod",
13)
15__version__ = "7.2.0"
17import collections
18import collections.abc
19import functools
20import heapq
21import random
22import time
24from . import keys
27class _DefaultSize:
28 """A minimal "fake" dict that returns a constant size 1 for any key."""
30 __slots__ = ()
32 def __getitem__(self, _key):
33 return 1
35 def __setitem__(self, _key, _value):
36 pass
38 def pop(self, _key):
39 return 1
41 def clear(self):
42 pass
45class Cache(collections.abc.MutableMapping):
46 """Mutable mapping to serve as a simple cache or cache base class."""
48 __marker = object()
50 __size = _DefaultSize()
52 def __init__(self, maxsize, getsizeof=None):
53 if maxsize < 0:
54 raise ValueError("maxsize must be non-negative")
55 if getsizeof:
56 self.getsizeof = getsizeof
57 if self.getsizeof is not Cache.getsizeof:
58 self.__size = {}
59 self.__data = {}
60 self.__currsize = 0
61 self.__maxsize = maxsize
63 def __repr__(self):
64 return "%s(%s, maxsize=%r, currsize=%r)" % (
65 type(self).__name__,
66 repr(self.__data),
67 self.__maxsize,
68 self.__currsize,
69 )
71 def __getitem__(self, key):
72 try:
73 return self.__data[key]
74 except KeyError:
75 return self.__missing__(key)
77 def __setitem__(self, key, value):
78 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()
88 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
98 def __delitem__(self, key):
99 size = self.__size.pop(key)
100 del self.__data[key]
101 self.__currsize -= size
103 def __contains__(self, key):
104 return key in self.__data
106 def __missing__(self, key):
107 raise KeyError(key)
109 def __iter__(self):
110 return iter(self.__data)
112 def __len__(self):
113 return len(self.__data)
115 # Note that we cannot simply inherit get(), pop() and setdefault()
116 # from MutableMapping, since these rely on __getitem__ throwing a
117 # KeyError on cache miss. This is not the case if __missing__ is
118 # implemented for a Cache subclass, so we have to roll our own,
119 # somewhat less elegant versions.
121 def get(self, key, default=None):
122 if key in self:
123 return self[key]
124 else:
125 return default
127 def pop(self, key, default=__marker):
128 if key in self:
129 value = self[key]
130 del self[key]
131 elif default is self.__marker:
132 raise KeyError(key)
133 else:
134 value = default
135 return value
137 def setdefault(self, key, default=None):
138 if key in self:
139 value = self[key]
140 else:
141 self[key] = value = default
142 return value
144 # Although the MutableMapping.clear() default implementation works
145 # perfectly well, it calls popitem() in a loop until the cache is
146 # empty, resulting in O(n) complexity. For large caches, this
147 # becomes a significant performance bottleneck, so we provide an
148 # optimized version for each Cache subclass.
150 def clear(self):
151 self.__data.clear()
152 self.__size.clear()
153 self.__currsize = 0
155 @property
156 def maxsize(self):
157 """The maximum size of the cache."""
158 return self.__maxsize
160 @property
161 def currsize(self):
162 """The current size of the cache."""
163 return self.__currsize
165 @staticmethod
166 def getsizeof(value):
167 """Return the size of a cache element's value."""
168 return 1
171class FIFOCache(Cache):
172 """First In First Out (FIFO) cache implementation."""
174 def __init__(self, maxsize, getsizeof=None):
175 Cache.__init__(self, maxsize, getsizeof)
176 self.__order = collections.OrderedDict()
178 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
179 cache_setitem(self, key, value)
180 if key in self.__order:
181 self.__order.move_to_end(key)
182 else:
183 self.__order[key] = None
185 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
186 cache_delitem(self, key)
187 del self.__order[key]
189 def popitem(self):
190 """Remove and return the `(key, value)` pair first inserted."""
191 try:
192 key = next(iter(self.__order))
193 except StopIteration:
194 raise KeyError("%s is empty" % type(self).__name__) from None
195 else:
196 return (key, self.pop(key))
198 def clear(self):
199 Cache.clear(self)
200 self.__order.clear()
203class LFUCache(Cache):
204 """Least Frequently Used (LFU) cache implementation."""
206 class _Link:
207 __slots__ = ("count", "keys", "next", "prev")
209 def __init__(self, count):
210 self.count = count
211 self.keys = set()
213 def unlink(self):
214 next = self.next
215 prev = self.prev
216 prev.next = next
217 next.prev = prev
219 def __init__(self, maxsize, getsizeof=None):
220 Cache.__init__(self, maxsize, getsizeof)
221 self.__root = root = LFUCache._Link(0) # sentinel
222 root.prev = root.next = root
223 self.__links = {}
225 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
226 value = cache_getitem(self, key)
227 if key in self: # __missing__ may not store item
228 self.__touch(key)
229 return value
231 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
232 cache_setitem(self, key, value)
233 if key in self.__links:
234 self.__touch(key)
235 return
236 root = self.__root
237 link = root.next
238 if link.count != 1:
239 link = LFUCache._Link(1)
240 link.next = root.next
241 root.next = link.next.prev = link
242 link.prev = root
243 link.keys.add(key)
244 self.__links[key] = link
246 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
247 cache_delitem(self, key)
248 link = self.__links.pop(key)
249 link.keys.remove(key)
250 if not link.keys:
251 link.unlink()
253 def popitem(self):
254 """Remove and return the `(key, value)` pair least frequently used."""
255 root = self.__root
256 curr = root.next
257 if curr is root:
258 raise KeyError("%s is empty" % type(self).__name__) from None
259 key = next(iter(curr.keys)) # remove an arbitrary element
260 return (key, self.pop(key))
262 def clear(self):
263 Cache.clear(self)
264 root = self.__root
265 root.prev = root.next = root
266 self.__links.clear()
268 def __touch(self, key):
269 """Increment use count"""
270 link = self.__links[key]
271 curr = link.next
272 if curr.count != link.count + 1:
273 if len(link.keys) == 1:
274 link.count += 1
275 return
276 curr = LFUCache._Link(link.count + 1)
277 curr.next = link.next
278 link.next = curr.next.prev = curr
279 curr.prev = link
280 curr.keys.add(key)
281 link.keys.remove(key)
282 if not link.keys:
283 link.unlink()
284 self.__links[key] = curr
287class LRUCache(Cache):
288 """Least Recently Used (LRU) cache implementation."""
290 def __init__(self, maxsize, getsizeof=None):
291 Cache.__init__(self, maxsize, getsizeof)
292 self.__order = collections.OrderedDict()
294 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
295 value = cache_getitem(self, key)
296 if key in self: # __missing__ may not store item
297 self.__touch(key)
298 return value
300 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
301 cache_setitem(self, key, value)
302 self.__touch(key)
304 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
305 cache_delitem(self, key)
306 del self.__order[key]
308 def popitem(self):
309 """Remove and return the `(key, value)` pair least recently used."""
310 try:
311 key = next(iter(self.__order))
312 except StopIteration:
313 raise KeyError("%s is empty" % type(self).__name__) from None
314 else:
315 return (key, self.pop(key))
317 def clear(self):
318 Cache.clear(self)
319 self.__order.clear()
321 def __touch(self, key):
322 """Mark as recently used"""
323 try:
324 self.__order.move_to_end(key)
325 except KeyError:
326 self.__order[key] = None
329class RRCache(Cache):
330 """Random Replacement (RR) cache implementation."""
332 def __init__(self, maxsize, choice=random.choice, getsizeof=None):
333 Cache.__init__(self, maxsize, getsizeof)
334 self.__choice = choice
335 self.__index = {}
336 self.__keys = []
338 @property
339 def choice(self):
340 """The `choice` function used by the cache."""
341 return self.__choice
343 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
344 cache_setitem(self, key, value)
345 if key not in self.__index:
346 self.__index[key] = len(self.__keys)
347 self.__keys.append(key)
349 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
350 cache_delitem(self, key)
351 index = self.__index.pop(key)
352 if index != len(self.__keys) - 1:
353 last = self.__keys[-1]
354 self.__keys[index] = last
355 self.__index[last] = index
356 self.__keys.pop()
358 def popitem(self):
359 """Remove and return a random `(key, value)` pair."""
360 try:
361 key = self.__choice(self.__keys)
362 except IndexError:
363 raise KeyError("%s is empty" % type(self).__name__) from None
364 else:
365 return (key, self.pop(key))
367 def clear(self):
368 Cache.clear(self)
369 self.__index.clear()
370 del self.__keys[:]
373class _TimedCache(Cache):
374 """Base class for time aware cache implementations."""
376 class _Timer:
377 def __init__(self, timer):
378 self.__timer = timer
379 self.__nesting = 0
381 def __call__(self):
382 if self.__nesting == 0:
383 return self.__timer()
384 else:
385 return self.__time
387 def __enter__(self):
388 if self.__nesting == 0:
389 self.__time = time = self.__timer()
390 else:
391 time = self.__time
392 self.__nesting += 1
393 return time
395 def __exit__(self, *exc):
396 self.__nesting -= 1
398 def __reduce__(self):
399 return _TimedCache._Timer, (self.__timer,)
401 def __getattr__(self, name):
402 return getattr(self.__timer, name)
404 def __init__(self, maxsize, timer, getsizeof=None):
405 Cache.__init__(self, maxsize, getsizeof)
406 self.__timer = _TimedCache._Timer(timer)
408 def __repr__(self, cache_repr=Cache.__repr__):
409 with self.__timer as time:
410 self.expire(time)
411 return cache_repr(self)
413 def __len__(self, cache_len=Cache.__len__):
414 with self.__timer as time:
415 self.expire(time)
416 return cache_len(self)
418 @property
419 def currsize(self):
420 with self.__timer as time:
421 self.expire(time)
422 return super().currsize
424 @property
425 def timer(self):
426 """The timer function used by the cache."""
427 return self.__timer
429 def get(self, *args, **kwargs):
430 with self.__timer:
431 return Cache.get(self, *args, **kwargs)
433 def pop(self, *args, **kwargs):
434 with self.__timer:
435 return Cache.pop(self, *args, **kwargs)
437 def setdefault(self, *args, **kwargs):
438 with self.__timer:
439 return Cache.setdefault(self, *args, **kwargs)
441 def clear(self):
442 # Subclasses must override to also reset their own time-tracking
443 # structures; we do not call expire() here since clear() should
444 # be O(1) regardless of cache contents.
445 Cache.clear(self)
447 def expire(self, time=None): # pragma: no cover
448 raise NotImplementedError
451class TTLCache(_TimedCache):
452 """LRU Cache implementation with per-item time-to-live (TTL) value."""
454 class _Link:
455 __slots__ = ("expires", "key", "next", "prev")
457 def __init__(self, key=None, expires=None):
458 self.key = key
459 self.expires = expires
461 def __reduce__(self):
462 return TTLCache._Link, (self.key, self.expires)
464 def unlink(self):
465 next = self.next
466 prev = self.prev
467 prev.next = next
468 next.prev = prev
470 def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None):
471 _TimedCache.__init__(self, maxsize, timer, getsizeof)
472 self.__root = root = TTLCache._Link()
473 root.prev = root.next = root
474 self.__links = collections.OrderedDict()
475 self.__ttl = ttl
477 def __contains__(self, key):
478 try:
479 link = self.__links[key] # no reordering
480 except KeyError:
481 return False
482 else:
483 return self.timer() < link.expires
485 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
486 try:
487 link = self.__getlink(key)
488 except KeyError:
489 expired = False
490 else:
491 expired = not (self.timer() < link.expires)
492 if expired:
493 return self.__missing__(key)
494 else:
495 return cache_getitem(self, key)
497 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
498 with self.timer as time:
499 self.expire(time)
500 cache_setitem(self, key, value)
501 try:
502 link = self.__getlink(key)
503 except KeyError:
504 self.__links[key] = link = TTLCache._Link(key)
505 else:
506 link.unlink()
507 link.expires = time + self.__ttl
508 link.next = root = self.__root
509 link.prev = prev = root.prev
510 prev.next = root.prev = link
512 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
513 cache_delitem(self, key)
514 link = self.__links.pop(key)
515 link.unlink()
516 if not (self.timer() < link.expires):
517 raise KeyError(key)
519 def __iter__(self):
520 root = self.__root
521 curr = root.next
522 while curr is not root:
523 # "freeze" time for iterator access
524 with self.timer as time:
525 if time < curr.expires:
526 yield curr.key
527 curr = curr.next
529 def __setstate__(self, state):
530 self.__dict__.update(state)
531 root = self.__root
532 root.prev = root.next = root
533 for link in sorted(self.__links.values(), key=lambda obj: obj.expires):
534 link.next = root
535 link.prev = prev = root.prev
536 prev.next = root.prev = link
537 self.expire(self.timer())
539 @property
540 def ttl(self):
541 """The time-to-live value of the cache's items."""
542 return self.__ttl
544 def expire(self, time=None):
545 """Remove expired items from the cache and return an iterable of the
546 expired `(key, value)` pairs.
548 """
549 if time is None:
550 time = self.timer()
551 root = self.__root
552 curr = root.next
553 links = self.__links
554 expired = []
555 cache_delitem = Cache.__delitem__
556 cache_getitem = Cache.__getitem__
557 while curr is not root and not (time < curr.expires):
558 expired.append((curr.key, cache_getitem(self, curr.key)))
559 cache_delitem(self, curr.key)
560 del links[curr.key]
561 next = curr.next
562 curr.unlink()
563 curr = next
564 return expired
566 def popitem(self):
567 """Remove and return the `(key, value)` pair least recently used that
568 has not already expired.
570 """
571 with self.timer as time:
572 self.expire(time)
573 try:
574 key = next(iter(self.__links))
575 except StopIteration:
576 raise KeyError("%s is empty" % type(self).__name__) from None
577 else:
578 return (key, self.pop(key))
580 def clear(self):
581 _TimedCache.clear(self)
582 root = self.__root
583 root.prev = root.next = root
584 self.__links.clear()
586 def __getlink(self, key):
587 value = self.__links[key]
588 self.__links.move_to_end(key)
589 return value
592class TLRUCache(_TimedCache):
593 """Time aware Least Recently Used (TLRU) cache implementation."""
595 __HEAP_CLEANUP_FACTOR = 2 # clean up the heap if size > N * len(items)
597 @functools.total_ordering
598 class _Item:
599 __slots__ = ("expires", "key", "removed")
601 def __init__(self, key=None, expires=None):
602 self.key = key
603 self.expires = expires
604 self.removed = False
606 def __lt__(self, other):
607 return self.expires < other.expires
609 def __init__(self, maxsize, ttu, timer=time.monotonic, getsizeof=None):
610 _TimedCache.__init__(self, maxsize, timer, getsizeof)
611 self.__items = collections.OrderedDict()
612 self.__order = []
613 self.__ttu = ttu
615 def __contains__(self, key):
616 try:
617 item = self.__items[key] # no reordering
618 except KeyError:
619 return False
620 else:
621 return self.timer() < item.expires
623 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
624 try:
625 item = self.__getitem(key)
626 except KeyError:
627 expired = False
628 else:
629 expired = not (self.timer() < item.expires)
630 if expired:
631 return self.__missing__(key)
632 else:
633 return cache_getitem(self, key)
635 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
636 with self.timer as time:
637 self.expire(time)
638 expires = self.__ttu(key, value, time)
639 if not (time < expires):
640 # updating an existing item with an already expired
641 # one should remove the existing item
642 return self.__delitem(key)
643 cache_setitem(self, key, value)
644 # removing an existing item would break the heap structure, so
645 # 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)
651 heapq.heappush(self.__order, item)
653 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
654 with self.timer as time:
655 # no self.expire() for performance reasons, e.g. self.clear() [#67]
656 cache_delitem(self, key)
657 item = self.__items.pop(key)
658 item.removed = True
659 if not (time < item.expires):
660 raise KeyError(key)
662 def __iter__(self):
663 for curr in self.__order:
664 # "freeze" time for iterator access
665 with self.timer as time:
666 if time < curr.expires and not curr.removed:
667 yield curr.key
669 @property
670 def ttu(self):
671 """The local time-to-use function used by the cache."""
672 return self.__ttu
674 def expire(self, time=None):
675 """Remove expired items from the cache and return an iterable of the
676 expired `(key, value)` pairs.
678 """
679 if time is None:
680 time = self.timer()
681 items = self.__items
682 order = self.__order
683 # clean up the heap if too many items are marked as removed
684 if len(order) > len(items) * self.__HEAP_CLEANUP_FACTOR:
685 self.__order = order = [item for item in order if not item.removed]
686 heapq.heapify(order)
687 expired = []
688 cache_delitem = Cache.__delitem__
689 cache_getitem = Cache.__getitem__
690 while order and (order[0].removed or not (time < order[0].expires)):
691 item = heapq.heappop(order)
692 if not item.removed:
693 expired.append((item.key, cache_getitem(self, item.key)))
694 cache_delitem(self, item.key)
695 del items[item.key]
696 return expired
698 def popitem(self):
699 """Remove and return the `(key, value)` pair least recently used that
700 has not already expired.
702 """
703 with self.timer as time:
704 self.expire(time)
705 try:
706 key = next(iter(self.__items))
707 except StopIteration:
708 raise KeyError("%s is empty" % type(self).__name__) from None
709 else:
710 return (key, self.pop(key))
712 def clear(self):
713 _TimedCache.clear(self)
714 self.__items.clear()
715 del self.__order[:]
717 def __getitem(self, key):
718 value = self.__items[key]
719 self.__items.move_to_end(key)
720 return value
722 def __delitem(self, key, cache_delitem=Cache.__delitem__):
723 try:
724 self.__items.pop(key).removed = True
725 except KeyError:
726 pass
727 else:
728 cache_delitem(self, key)
731# note that the runtime __name__ is "CacheInfo", as in stdlib:
732# https://github.com/python/cpython/blob/3.14/Lib/functools.py#L520
733_CacheInfo = collections.namedtuple(
734 "CacheInfo", ["hits", "misses", "maxsize", "currsize"]
735)
738def cached(cache, key=keys.hashkey, lock=None, condition=None, info=False):
739 """Decorator to wrap a function with a memoizing callable that saves
740 results in a cache.
742 """
743 from ._cached import _wrapper
745 def decorator(func):
746 if info:
747 if isinstance(cache, Cache):
749 def make_info(hits, misses):
750 return _CacheInfo(hits, misses, cache.maxsize, cache.currsize)
752 elif isinstance(cache, collections.abc.Mapping):
754 def make_info(hits, misses):
755 return _CacheInfo(hits, misses, None, len(cache))
757 else:
759 def make_info(hits, misses):
760 return _CacheInfo(hits, misses, 0, 0)
762 return _wrapper(func, cache, key, lock, condition, info=make_info)
763 else:
764 return _wrapper(func, cache, key, lock, condition)
766 return decorator
769def cachedmethod(cache, key=keys.methodkey, lock=None, condition=None, info=False):
770 """Decorator to wrap a method with a memoizing callable that saves
771 results in a cache.
773 """
774 from ._cachedmethod import _wrapper
776 def decorator(method):
777 if info:
779 def make_info(cache, hits, misses):
780 if isinstance(cache, Cache):
781 return _CacheInfo(hits, misses, cache.maxsize, cache.currsize)
782 elif isinstance(cache, collections.abc.Mapping):
783 return _CacheInfo(hits, misses, None, len(cache))
784 else:
785 raise TypeError("cache(self) must return a mutable mapping")
787 return _wrapper(method, cache, key, lock, condition, info=make_info)
788 else:
789 return _wrapper(method, cache, key, lock, condition)
791 return decorator