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.1.7"
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 getsizeof:
54 self.getsizeof = getsizeof
55 if self.getsizeof is not Cache.getsizeof:
56 self.__size = {}
57 self.__data = {}
58 self.__currsize = 0
59 self.__maxsize = maxsize
61 def __repr__(self):
62 return "%s(%s, maxsize=%r, currsize=%r)" % (
63 type(self).__name__,
64 repr(self.__data),
65 self.__maxsize,
66 self.__currsize,
67 )
69 def __getitem__(self, key):
70 try:
71 return self.__data[key]
72 except KeyError:
73 return self.__missing__(key)
75 def __setitem__(self, key, value):
76 maxsize = self.__maxsize
77 size = self.getsizeof(value)
78 if size < 0:
79 raise ValueError("value size must be non-negative")
80 if size > maxsize:
81 raise ValueError("value too large")
82 if key not in self.__data:
83 diffsize = size
84 while self.__currsize + diffsize > maxsize:
85 self.popitem()
86 else:
87 diffsize = size - self.__size[key]
88 while self.__currsize + diffsize > maxsize:
89 self.popitem()
90 if key not in self.__data:
91 diffsize = size
92 self.__data[key] = value
93 self.__size[key] = size
94 self.__currsize += diffsize
96 def __delitem__(self, key):
97 size = self.__size.pop(key)
98 del self.__data[key]
99 self.__currsize -= size
101 def __contains__(self, key):
102 return key in self.__data
104 def __missing__(self, key):
105 raise KeyError(key)
107 def __iter__(self):
108 return iter(self.__data)
110 def __len__(self):
111 return len(self.__data)
113 # Note that we cannot simply inherit get(), pop() and setdefault()
114 # from MutableMapping, since these rely on __getitem__ throwing a
115 # KeyError on cache miss. This is not the case if __missing__ is
116 # implemented for a Cache subclass, so we have to roll our own,
117 # somewhat less elegant versions.
119 def get(self, key, default=None):
120 if key in self:
121 return self[key]
122 else:
123 return default
125 def pop(self, key, default=__marker):
126 if key in self:
127 value = self[key]
128 del self[key]
129 elif default is self.__marker:
130 raise KeyError(key)
131 else:
132 value = default
133 return value
135 def setdefault(self, key, default=None):
136 if key in self:
137 value = self[key]
138 else:
139 self[key] = value = default
140 return value
142 # Although the MutableMapping.clear() default implementation works
143 # perfectly well, it calls popitem() in a loop until the cache is
144 # empty, resulting in O(n) complexity. For large caches, this
145 # becomes a significant performance bottleneck, so we provide an
146 # optimized version for each Cache subclass.
148 def clear(self):
149 self.__data.clear()
150 self.__size.clear()
151 self.__currsize = 0
153 @property
154 def maxsize(self):
155 """The maximum size of the cache."""
156 return self.__maxsize
158 @property
159 def currsize(self):
160 """The current size of the cache."""
161 return self.__currsize
163 @staticmethod
164 def getsizeof(value):
165 """Return the size of a cache element's value."""
166 return 1
169class FIFOCache(Cache):
170 """First In First Out (FIFO) cache implementation."""
172 def __init__(self, maxsize, getsizeof=None):
173 Cache.__init__(self, maxsize, getsizeof)
174 self.__order = collections.OrderedDict()
176 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
177 cache_setitem(self, key, value)
178 if key in self.__order:
179 self.__order.move_to_end(key)
180 else:
181 self.__order[key] = None
183 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
184 cache_delitem(self, key)
185 del self.__order[key]
187 def popitem(self):
188 """Remove and return the `(key, value)` pair first inserted."""
189 try:
190 key = next(iter(self.__order))
191 except StopIteration:
192 raise KeyError("%s is empty" % type(self).__name__) from None
193 else:
194 return (key, self.pop(key))
196 def clear(self):
197 Cache.clear(self)
198 self.__order.clear()
201class LFUCache(Cache):
202 """Least Frequently Used (LFU) cache implementation."""
204 class _Link:
205 __slots__ = ("count", "keys", "next", "prev")
207 def __init__(self, count):
208 self.count = count
209 self.keys = set()
211 def unlink(self):
212 next = self.next
213 prev = self.prev
214 prev.next = next
215 next.prev = prev
217 def __init__(self, maxsize, getsizeof=None):
218 Cache.__init__(self, maxsize, getsizeof)
219 self.__root = root = LFUCache._Link(0) # sentinel
220 root.prev = root.next = root
221 self.__links = {}
223 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
224 value = cache_getitem(self, key)
225 if key in self: # __missing__ may not store item
226 self.__touch(key)
227 return value
229 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
230 cache_setitem(self, key, value)
231 if key in self.__links:
232 self.__touch(key)
233 return
234 root = self.__root
235 link = root.next
236 if link.count != 1:
237 link = LFUCache._Link(1)
238 link.next = root.next
239 root.next = link.next.prev = link
240 link.prev = root
241 link.keys.add(key)
242 self.__links[key] = link
244 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
245 cache_delitem(self, key)
246 link = self.__links.pop(key)
247 link.keys.remove(key)
248 if not link.keys:
249 link.unlink()
251 def popitem(self):
252 """Remove and return the `(key, value)` pair least frequently used."""
253 root = self.__root
254 curr = root.next
255 if curr is root:
256 raise KeyError("%s is empty" % type(self).__name__) from None
257 key = next(iter(curr.keys)) # remove an arbitrary element
258 return (key, self.pop(key))
260 def clear(self):
261 Cache.clear(self)
262 root = self.__root
263 root.prev = root.next = root
264 self.__links.clear()
266 def __touch(self, key):
267 """Increment use count"""
268 link = self.__links[key]
269 curr = link.next
270 if curr.count != link.count + 1:
271 if len(link.keys) == 1:
272 link.count += 1
273 return
274 curr = LFUCache._Link(link.count + 1)
275 curr.next = link.next
276 link.next = curr.next.prev = curr
277 curr.prev = link
278 curr.keys.add(key)
279 link.keys.remove(key)
280 if not link.keys:
281 link.unlink()
282 self.__links[key] = curr
285class LRUCache(Cache):
286 """Least Recently Used (LRU) cache implementation."""
288 def __init__(self, maxsize, getsizeof=None):
289 Cache.__init__(self, maxsize, getsizeof)
290 self.__order = collections.OrderedDict()
292 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
293 value = cache_getitem(self, key)
294 if key in self: # __missing__ may not store item
295 self.__touch(key)
296 return value
298 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
299 cache_setitem(self, key, value)
300 self.__touch(key)
302 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
303 cache_delitem(self, key)
304 del self.__order[key]
306 def popitem(self):
307 """Remove and return the `(key, value)` pair least recently used."""
308 try:
309 key = next(iter(self.__order))
310 except StopIteration:
311 raise KeyError("%s is empty" % type(self).__name__) from None
312 else:
313 return (key, self.pop(key))
315 def clear(self):
316 Cache.clear(self)
317 self.__order.clear()
319 def __touch(self, key):
320 """Mark as recently used"""
321 try:
322 self.__order.move_to_end(key)
323 except KeyError:
324 self.__order[key] = None
327class RRCache(Cache):
328 """Random Replacement (RR) cache implementation."""
330 def __init__(self, maxsize, choice=random.choice, getsizeof=None):
331 Cache.__init__(self, maxsize, getsizeof)
332 self.__choice = choice
333 self.__index = {}
334 self.__keys = []
336 @property
337 def choice(self):
338 """The `choice` function used by the cache."""
339 return self.__choice
341 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
342 cache_setitem(self, key, value)
343 if key not in self.__index:
344 self.__index[key] = len(self.__keys)
345 self.__keys.append(key)
347 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
348 cache_delitem(self, key)
349 index = self.__index.pop(key)
350 if index != len(self.__keys) - 1:
351 last = self.__keys[-1]
352 self.__keys[index] = last
353 self.__index[last] = index
354 self.__keys.pop()
356 def popitem(self):
357 """Remove and return a random `(key, value)` pair."""
358 try:
359 key = self.__choice(self.__keys)
360 except IndexError:
361 raise KeyError("%s is empty" % type(self).__name__) from None
362 else:
363 return (key, self.pop(key))
365 def clear(self):
366 Cache.clear(self)
367 self.__index.clear()
368 del self.__keys[:]
371class _TimedCache(Cache):
372 """Base class for time aware cache implementations."""
374 class _Timer:
375 def __init__(self, timer):
376 self.__timer = timer
377 self.__nesting = 0
379 def __call__(self):
380 if self.__nesting == 0:
381 return self.__timer()
382 else:
383 return self.__time
385 def __enter__(self):
386 if self.__nesting == 0:
387 self.__time = time = self.__timer()
388 else:
389 time = self.__time
390 self.__nesting += 1
391 return time
393 def __exit__(self, *exc):
394 self.__nesting -= 1
396 def __reduce__(self):
397 return _TimedCache._Timer, (self.__timer,)
399 def __getattr__(self, name):
400 return getattr(self.__timer, name)
402 def __init__(self, maxsize, timer, getsizeof=None):
403 Cache.__init__(self, maxsize, getsizeof)
404 self.__timer = _TimedCache._Timer(timer)
406 def __repr__(self, cache_repr=Cache.__repr__):
407 with self.__timer as time:
408 self.expire(time)
409 return cache_repr(self)
411 def __len__(self, cache_len=Cache.__len__):
412 with self.__timer as time:
413 self.expire(time)
414 return cache_len(self)
416 @property
417 def currsize(self):
418 with self.__timer as time:
419 self.expire(time)
420 return super().currsize
422 @property
423 def timer(self):
424 """The timer function used by the cache."""
425 return self.__timer
427 def get(self, *args, **kwargs):
428 with self.__timer:
429 return Cache.get(self, *args, **kwargs)
431 def pop(self, *args, **kwargs):
432 with self.__timer:
433 return Cache.pop(self, *args, **kwargs)
435 def setdefault(self, *args, **kwargs):
436 with self.__timer:
437 return Cache.setdefault(self, *args, **kwargs)
439 def clear(self):
440 # Subclasses must override to also reset their own time-tracking
441 # structures; we do not call expire() here since clear() should
442 # be O(1) regardless of cache contents.
443 Cache.clear(self)
445 def expire(self, time=None): # pragma: no cover
446 raise NotImplementedError
449class TTLCache(_TimedCache):
450 """LRU Cache implementation with per-item time-to-live (TTL) value."""
452 class _Link:
453 __slots__ = ("expires", "key", "next", "prev")
455 def __init__(self, key=None, expires=None):
456 self.key = key
457 self.expires = expires
459 def __reduce__(self):
460 return TTLCache._Link, (self.key, self.expires)
462 def unlink(self):
463 next = self.next
464 prev = self.prev
465 prev.next = next
466 next.prev = prev
468 def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None):
469 _TimedCache.__init__(self, maxsize, timer, getsizeof)
470 self.__root = root = TTLCache._Link()
471 root.prev = root.next = root
472 self.__links = collections.OrderedDict()
473 self.__ttl = ttl
475 def __contains__(self, key):
476 try:
477 link = self.__links[key] # no reordering
478 except KeyError:
479 return False
480 else:
481 return self.timer() < link.expires
483 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
484 try:
485 link = self.__getlink(key)
486 except KeyError:
487 expired = False
488 else:
489 expired = not (self.timer() < link.expires)
490 if expired:
491 return self.__missing__(key)
492 else:
493 return cache_getitem(self, key)
495 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
496 with self.timer as time:
497 self.expire(time)
498 cache_setitem(self, key, value)
499 try:
500 link = self.__getlink(key)
501 except KeyError:
502 self.__links[key] = link = TTLCache._Link(key)
503 else:
504 link.unlink()
505 link.expires = time + self.__ttl
506 link.next = root = self.__root
507 link.prev = prev = root.prev
508 prev.next = root.prev = link
510 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
511 cache_delitem(self, key)
512 link = self.__links.pop(key)
513 link.unlink()
514 if not (self.timer() < link.expires):
515 raise KeyError(key)
517 def __iter__(self):
518 root = self.__root
519 curr = root.next
520 while curr is not root:
521 # "freeze" time for iterator access
522 with self.timer as time:
523 if time < curr.expires:
524 yield curr.key
525 curr = curr.next
527 def __setstate__(self, state):
528 self.__dict__.update(state)
529 root = self.__root
530 root.prev = root.next = root
531 for link in sorted(self.__links.values(), key=lambda obj: obj.expires):
532 link.next = root
533 link.prev = prev = root.prev
534 prev.next = root.prev = link
535 self.expire(self.timer())
537 @property
538 def ttl(self):
539 """The time-to-live value of the cache's items."""
540 return self.__ttl
542 def expire(self, time=None):
543 """Remove expired items from the cache and return an iterable of the
544 expired `(key, value)` pairs.
546 """
547 if time is None:
548 time = self.timer()
549 root = self.__root
550 curr = root.next
551 links = self.__links
552 expired = []
553 cache_delitem = Cache.__delitem__
554 cache_getitem = Cache.__getitem__
555 while curr is not root and not (time < curr.expires):
556 expired.append((curr.key, cache_getitem(self, curr.key)))
557 cache_delitem(self, curr.key)
558 del links[curr.key]
559 next = curr.next
560 curr.unlink()
561 curr = next
562 return expired
564 def popitem(self):
565 """Remove and return the `(key, value)` pair least recently used that
566 has not already expired.
568 """
569 with self.timer as time:
570 self.expire(time)
571 try:
572 key = next(iter(self.__links))
573 except StopIteration:
574 raise KeyError("%s is empty" % type(self).__name__) from None
575 else:
576 return (key, self.pop(key))
578 def clear(self):
579 _TimedCache.clear(self)
580 root = self.__root
581 root.prev = root.next = root
582 self.__links.clear()
584 def __getlink(self, key):
585 value = self.__links[key]
586 self.__links.move_to_end(key)
587 return value
590class TLRUCache(_TimedCache):
591 """Time aware Least Recently Used (TLRU) cache implementation."""
593 __HEAP_CLEANUP_FACTOR = 2 # clean up the heap if size > N * len(items)
595 @functools.total_ordering
596 class _Item:
597 __slots__ = ("expires", "key", "removed")
599 def __init__(self, key=None, expires=None):
600 self.key = key
601 self.expires = expires
602 self.removed = False
604 def __lt__(self, other):
605 return self.expires < other.expires
607 def __init__(self, maxsize, ttu, timer=time.monotonic, getsizeof=None):
608 _TimedCache.__init__(self, maxsize, timer, getsizeof)
609 self.__items = collections.OrderedDict()
610 self.__order = []
611 self.__ttu = ttu
613 def __contains__(self, key):
614 try:
615 item = self.__items[key] # no reordering
616 except KeyError:
617 return False
618 else:
619 return self.timer() < item.expires
621 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
622 try:
623 item = self.__getitem(key)
624 except KeyError:
625 expired = False
626 else:
627 expired = not (self.timer() < item.expires)
628 if expired:
629 return self.__missing__(key)
630 else:
631 return cache_getitem(self, key)
633 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
634 with self.timer as time:
635 self.expire(time)
636 expires = self.__ttu(key, value, time)
637 if not (time < expires):
638 # updating an existing item with an already expired
639 # one should remove the existing item
640 return self.__delitem(key)
641 cache_setitem(self, key, value)
642 # removing an existing item would break the heap structure, so
643 # only mark it as removed for now
644 try:
645 self.__getitem(key).removed = True
646 except KeyError:
647 pass
648 self.__items[key] = item = TLRUCache._Item(key, expires)
649 heapq.heappush(self.__order, item)
651 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
652 with self.timer as time:
653 # no self.expire() for performance reasons, e.g. self.clear() [#67]
654 cache_delitem(self, key)
655 item = self.__items.pop(key)
656 item.removed = True
657 if not (time < item.expires):
658 raise KeyError(key)
660 def __iter__(self):
661 for curr in self.__order:
662 # "freeze" time for iterator access
663 with self.timer as time:
664 if time < curr.expires and not curr.removed:
665 yield curr.key
667 @property
668 def ttu(self):
669 """The local time-to-use function used by the cache."""
670 return self.__ttu
672 def expire(self, time=None):
673 """Remove expired items from the cache and return an iterable of the
674 expired `(key, value)` pairs.
676 """
677 if time is None:
678 time = self.timer()
679 items = self.__items
680 order = self.__order
681 # clean up the heap if too many items are marked as removed
682 if len(order) > len(items) * self.__HEAP_CLEANUP_FACTOR:
683 self.__order = order = [item for item in order if not item.removed]
684 heapq.heapify(order)
685 expired = []
686 cache_delitem = Cache.__delitem__
687 cache_getitem = Cache.__getitem__
688 while order and (order[0].removed or not (time < order[0].expires)):
689 item = heapq.heappop(order)
690 if not item.removed:
691 expired.append((item.key, cache_getitem(self, item.key)))
692 cache_delitem(self, item.key)
693 del items[item.key]
694 return expired
696 def popitem(self):
697 """Remove and return the `(key, value)` pair least recently used that
698 has not already expired.
700 """
701 with self.timer as time:
702 self.expire(time)
703 try:
704 key = next(iter(self.__items))
705 except StopIteration:
706 raise KeyError("%s is empty" % type(self).__name__) from None
707 else:
708 return (key, self.pop(key))
710 def clear(self):
711 _TimedCache.clear(self)
712 self.__items.clear()
713 del self.__order[:]
715 def __getitem(self, key):
716 value = self.__items[key]
717 self.__items.move_to_end(key)
718 return value
720 def __delitem(self, key, cache_delitem=Cache.__delitem__):
721 try:
722 self.__items.pop(key).removed = True
723 except KeyError:
724 pass
725 else:
726 cache_delitem(self, key)
729# note that the runtime __name__ is "CacheInfo", as in stdlib:
730# https://github.com/python/cpython/blob/3.14/Lib/functools.py#L520
731_CacheInfo = collections.namedtuple(
732 "CacheInfo", ["hits", "misses", "maxsize", "currsize"]
733)
736def cached(cache, key=keys.hashkey, lock=None, condition=None, info=False):
737 """Decorator to wrap a function with a memoizing callable that saves
738 results in a cache.
740 """
741 from ._cached import _wrapper
743 def decorator(func):
744 if info:
745 if isinstance(cache, Cache):
747 def make_info(hits, misses):
748 return _CacheInfo(hits, misses, cache.maxsize, cache.currsize)
750 elif isinstance(cache, collections.abc.Mapping):
752 def make_info(hits, misses):
753 return _CacheInfo(hits, misses, None, len(cache))
755 else:
757 def make_info(hits, misses):
758 return _CacheInfo(hits, misses, 0, 0)
760 return _wrapper(func, cache, key, lock, condition, info=make_info)
761 else:
762 return _wrapper(func, cache, key, lock, condition)
764 return decorator
767def cachedmethod(cache, key=keys.methodkey, lock=None, condition=None, info=False):
768 """Decorator to wrap a method with a memoizing callable that saves
769 results in a cache.
771 """
772 from ._cachedmethod import _wrapper
774 def decorator(method):
775 if info:
777 def make_info(cache, hits, misses):
778 if isinstance(cache, Cache):
779 return _CacheInfo(hits, misses, cache.maxsize, cache.currsize)
780 elif isinstance(cache, collections.abc.Mapping):
781 return _CacheInfo(hits, misses, None, len(cache))
782 else:
783 raise TypeError("cache(self) must return a mutable mapping")
785 return _wrapper(method, cache, key, lock, condition, info=make_info)
786 else:
787 return _wrapper(method, cache, key, lock, condition)
789 return decorator