Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/cachetools/__init__.py: 25%
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__ = "6.2.2"
17import collections
18import collections.abc
19import functools
20import heapq
21import random
22import time
24from . import keys
27class _DefaultSize:
28 __slots__ = ()
30 def __getitem__(self, _key):
31 return 1
33 def __setitem__(self, _key, _value):
34 pass
36 def pop(self, _key):
37 return 1
40class Cache(collections.abc.MutableMapping):
41 """Mutable mapping to serve as a simple cache or cache base class."""
43 __marker = object()
45 __size = _DefaultSize()
47 def __init__(self, maxsize, getsizeof=None):
48 if getsizeof:
49 self.getsizeof = getsizeof
50 if self.getsizeof is not Cache.getsizeof:
51 self.__size = dict()
52 self.__data = dict()
53 self.__currsize = 0
54 self.__maxsize = maxsize
56 def __repr__(self):
57 return "%s(%s, maxsize=%r, currsize=%r)" % (
58 type(self).__name__,
59 repr(self.__data),
60 self.__maxsize,
61 self.__currsize,
62 )
64 def __getitem__(self, key):
65 try:
66 return self.__data[key]
67 except KeyError:
68 return self.__missing__(key)
70 def __setitem__(self, key, value):
71 maxsize = self.__maxsize
72 size = self.getsizeof(value)
73 if size > maxsize:
74 raise ValueError("value too large")
75 if key not in self.__data or self.__size[key] < size:
76 while self.__currsize + size > maxsize:
77 self.popitem()
78 if key in self.__data:
79 diffsize = size - self.__size[key]
80 else:
81 diffsize = size
82 self.__data[key] = value
83 self.__size[key] = size
84 self.__currsize += diffsize
86 def __delitem__(self, key):
87 size = self.__size.pop(key)
88 del self.__data[key]
89 self.__currsize -= size
91 def __contains__(self, key):
92 return key in self.__data
94 def __missing__(self, key):
95 raise KeyError(key)
97 def __iter__(self):
98 return iter(self.__data)
100 def __len__(self):
101 return len(self.__data)
103 # Note that we cannot simply inherit get(), pop() and setdefault()
104 # from MutableMapping, since these rely on __getitem__ throwing a
105 # KeyError on cache miss. This is not the case if __missing__ is
106 # implemented for a Cache subclass, so we have to roll our own,
107 # somewhat less elegant versions.
109 def get(self, key, default=None):
110 if key in self:
111 return self[key]
112 else:
113 return default
115 def pop(self, key, default=__marker):
116 if key in self:
117 value = self[key]
118 del self[key]
119 elif default is self.__marker:
120 raise KeyError(key)
121 else:
122 value = default
123 return value
125 def setdefault(self, key, default=None):
126 if key in self:
127 value = self[key]
128 else:
129 self[key] = value = default
130 return value
132 @property
133 def maxsize(self):
134 """The maximum size of the cache."""
135 return self.__maxsize
137 @property
138 def currsize(self):
139 """The current size of the cache."""
140 return self.__currsize
142 @staticmethod
143 def getsizeof(value):
144 """Return the size of a cache element's value."""
145 return 1
148class FIFOCache(Cache):
149 """First In First Out (FIFO) cache implementation."""
151 def __init__(self, maxsize, getsizeof=None):
152 Cache.__init__(self, maxsize, getsizeof)
153 self.__order = collections.OrderedDict()
155 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
156 cache_setitem(self, key, value)
157 try:
158 self.__order.move_to_end(key)
159 except KeyError:
160 self.__order[key] = None
162 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
163 cache_delitem(self, key)
164 del self.__order[key]
166 def popitem(self):
167 """Remove and return the `(key, value)` pair first inserted."""
168 try:
169 key = next(iter(self.__order))
170 except StopIteration:
171 raise KeyError("%s is empty" % type(self).__name__) from None
172 else:
173 return (key, self.pop(key))
176class LFUCache(Cache):
177 """Least Frequently Used (LFU) cache implementation."""
179 class _Link:
180 __slots__ = ("count", "keys", "next", "prev")
182 def __init__(self, count):
183 self.count = count
184 self.keys = set()
186 def unlink(self):
187 next = self.next
188 prev = self.prev
189 prev.next = next
190 next.prev = prev
192 def __init__(self, maxsize, getsizeof=None):
193 Cache.__init__(self, maxsize, getsizeof)
194 self.__root = root = LFUCache._Link(0) # sentinel
195 root.prev = root.next = root
196 self.__links = {}
198 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
199 value = cache_getitem(self, key)
200 if key in self: # __missing__ may not store item
201 self.__touch(key)
202 return value
204 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
205 cache_setitem(self, key, value)
206 if key in self.__links:
207 return self.__touch(key)
208 root = self.__root
209 link = root.next
210 if link.count != 1:
211 link = LFUCache._Link(1)
212 link.next = root.next
213 root.next = link.next.prev = link
214 link.prev = root
215 link.keys.add(key)
216 self.__links[key] = link
218 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
219 cache_delitem(self, key)
220 link = self.__links.pop(key)
221 link.keys.remove(key)
222 if not link.keys:
223 link.unlink()
225 def popitem(self):
226 """Remove and return the `(key, value)` pair least frequently used."""
227 root = self.__root
228 curr = root.next
229 if curr is root:
230 raise KeyError("%s is empty" % type(self).__name__) from None
231 key = next(iter(curr.keys)) # remove an arbitrary element
232 return (key, self.pop(key))
234 def __touch(self, key):
235 """Increment use count"""
236 link = self.__links[key]
237 curr = link.next
238 if curr.count != link.count + 1:
239 if len(link.keys) == 1:
240 link.count += 1
241 return
242 curr = LFUCache._Link(link.count + 1)
243 curr.next = link.next
244 link.next = curr.next.prev = curr
245 curr.prev = link
246 curr.keys.add(key)
247 link.keys.remove(key)
248 if not link.keys:
249 link.unlink()
250 self.__links[key] = curr
253class LRUCache(Cache):
254 """Least Recently Used (LRU) cache implementation."""
256 def __init__(self, maxsize, getsizeof=None):
257 Cache.__init__(self, maxsize, getsizeof)
258 self.__order = collections.OrderedDict()
260 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
261 value = cache_getitem(self, key)
262 if key in self: # __missing__ may not store item
263 self.__touch(key)
264 return value
266 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
267 cache_setitem(self, key, value)
268 self.__touch(key)
270 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
271 cache_delitem(self, key)
272 del self.__order[key]
274 def popitem(self):
275 """Remove and return the `(key, value)` pair least recently used."""
276 try:
277 key = next(iter(self.__order))
278 except StopIteration:
279 raise KeyError("%s is empty" % type(self).__name__) from None
280 else:
281 return (key, self.pop(key))
283 def __touch(self, key):
284 """Mark as recently used"""
285 try:
286 self.__order.move_to_end(key)
287 except KeyError:
288 self.__order[key] = None
291class RRCache(Cache):
292 """Random Replacement (RR) cache implementation."""
294 def __init__(self, maxsize, choice=random.choice, getsizeof=None):
295 Cache.__init__(self, maxsize, getsizeof)
296 self.__choice = choice
297 self.__index = {}
298 self.__keys = []
300 @property
301 def choice(self):
302 """The `choice` function used by the cache."""
303 return self.__choice
305 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
306 cache_setitem(self, key, value)
307 if key not in self.__index:
308 self.__index[key] = len(self.__keys)
309 self.__keys.append(key)
311 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
312 cache_delitem(self, key)
313 index = self.__index.pop(key)
314 if index != len(self.__keys) - 1:
315 last = self.__keys[-1]
316 self.__keys[index] = last
317 self.__index[last] = index
318 self.__keys.pop()
320 def popitem(self):
321 """Remove and return a random `(key, value)` pair."""
322 try:
323 key = self.__choice(self.__keys)
324 except IndexError:
325 raise KeyError("%s is empty" % type(self).__name__) from None
326 else:
327 return (key, self.pop(key))
330class _TimedCache(Cache):
331 """Base class for time aware cache implementations."""
333 class _Timer:
334 def __init__(self, timer):
335 self.__timer = timer
336 self.__nesting = 0
338 def __call__(self):
339 if self.__nesting == 0:
340 return self.__timer()
341 else:
342 return self.__time
344 def __enter__(self):
345 if self.__nesting == 0:
346 self.__time = time = self.__timer()
347 else:
348 time = self.__time
349 self.__nesting += 1
350 return time
352 def __exit__(self, *exc):
353 self.__nesting -= 1
355 def __reduce__(self):
356 return _TimedCache._Timer, (self.__timer,)
358 def __getattr__(self, name):
359 return getattr(self.__timer, name)
361 def __init__(self, maxsize, timer=time.monotonic, getsizeof=None):
362 Cache.__init__(self, maxsize, getsizeof)
363 self.__timer = _TimedCache._Timer(timer)
365 def __repr__(self, cache_repr=Cache.__repr__):
366 with self.__timer as time:
367 self.expire(time)
368 return cache_repr(self)
370 def __len__(self, cache_len=Cache.__len__):
371 with self.__timer as time:
372 self.expire(time)
373 return cache_len(self)
375 @property
376 def currsize(self):
377 with self.__timer as time:
378 self.expire(time)
379 return super().currsize
381 @property
382 def timer(self):
383 """The timer function used by the cache."""
384 return self.__timer
386 def clear(self):
387 with self.__timer as time:
388 self.expire(time)
389 Cache.clear(self)
391 def get(self, *args, **kwargs):
392 with self.__timer:
393 return Cache.get(self, *args, **kwargs)
395 def pop(self, *args, **kwargs):
396 with self.__timer:
397 return Cache.pop(self, *args, **kwargs)
399 def setdefault(self, *args, **kwargs):
400 with self.__timer:
401 return Cache.setdefault(self, *args, **kwargs)
404class TTLCache(_TimedCache):
405 """LRU Cache implementation with per-item time-to-live (TTL) value."""
407 class _Link:
408 __slots__ = ("key", "expires", "next", "prev")
410 def __init__(self, key=None, expires=None):
411 self.key = key
412 self.expires = expires
414 def __reduce__(self):
415 return TTLCache._Link, (self.key, self.expires)
417 def unlink(self):
418 next = self.next
419 prev = self.prev
420 prev.next = next
421 next.prev = prev
423 def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None):
424 _TimedCache.__init__(self, maxsize, timer, getsizeof)
425 self.__root = root = TTLCache._Link()
426 root.prev = root.next = root
427 self.__links = collections.OrderedDict()
428 self.__ttl = ttl
430 def __contains__(self, key):
431 try:
432 link = self.__links[key] # no reordering
433 except KeyError:
434 return False
435 else:
436 return self.timer() < link.expires
438 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
439 try:
440 link = self.__getlink(key)
441 except KeyError:
442 expired = False
443 else:
444 expired = not (self.timer() < link.expires)
445 if expired:
446 return self.__missing__(key)
447 else:
448 return cache_getitem(self, key)
450 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
451 with self.timer as time:
452 self.expire(time)
453 cache_setitem(self, key, value)
454 try:
455 link = self.__getlink(key)
456 except KeyError:
457 self.__links[key] = link = TTLCache._Link(key)
458 else:
459 link.unlink()
460 link.expires = time + self.__ttl
461 link.next = root = self.__root
462 link.prev = prev = root.prev
463 prev.next = root.prev = link
465 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
466 cache_delitem(self, key)
467 link = self.__links.pop(key)
468 link.unlink()
469 if not (self.timer() < link.expires):
470 raise KeyError(key)
472 def __iter__(self):
473 root = self.__root
474 curr = root.next
475 while curr is not root:
476 # "freeze" time for iterator access
477 with self.timer as time:
478 if time < curr.expires:
479 yield curr.key
480 curr = curr.next
482 def __setstate__(self, state):
483 self.__dict__.update(state)
484 root = self.__root
485 root.prev = root.next = root
486 for link in sorted(self.__links.values(), key=lambda obj: obj.expires):
487 link.next = root
488 link.prev = prev = root.prev
489 prev.next = root.prev = link
490 self.expire(self.timer())
492 @property
493 def ttl(self):
494 """The time-to-live value of the cache's items."""
495 return self.__ttl
497 def expire(self, time=None):
498 """Remove expired items from the cache and return an iterable of the
499 expired `(key, value)` pairs.
501 """
502 if time is None:
503 time = self.timer()
504 root = self.__root
505 curr = root.next
506 links = self.__links
507 expired = []
508 cache_delitem = Cache.__delitem__
509 cache_getitem = Cache.__getitem__
510 while curr is not root and not (time < curr.expires):
511 expired.append((curr.key, cache_getitem(self, curr.key)))
512 cache_delitem(self, curr.key)
513 del links[curr.key]
514 next = curr.next
515 curr.unlink()
516 curr = next
517 return expired
519 def popitem(self):
520 """Remove and return the `(key, value)` pair least recently used that
521 has not already expired.
523 """
524 with self.timer as time:
525 self.expire(time)
526 try:
527 key = next(iter(self.__links))
528 except StopIteration:
529 raise KeyError("%s is empty" % type(self).__name__) from None
530 else:
531 return (key, self.pop(key))
533 def __getlink(self, key):
534 value = self.__links[key]
535 self.__links.move_to_end(key)
536 return value
539class TLRUCache(_TimedCache):
540 """Time aware Least Recently Used (TLRU) cache implementation."""
542 @functools.total_ordering
543 class _Item:
544 __slots__ = ("key", "expires", "removed")
546 def __init__(self, key=None, expires=None):
547 self.key = key
548 self.expires = expires
549 self.removed = False
551 def __lt__(self, other):
552 return self.expires < other.expires
554 def __init__(self, maxsize, ttu, timer=time.monotonic, getsizeof=None):
555 _TimedCache.__init__(self, maxsize, timer, getsizeof)
556 self.__items = collections.OrderedDict()
557 self.__order = []
558 self.__ttu = ttu
560 def __contains__(self, key):
561 try:
562 item = self.__items[key] # no reordering
563 except KeyError:
564 return False
565 else:
566 return self.timer() < item.expires
568 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
569 try:
570 item = self.__getitem(key)
571 except KeyError:
572 expired = False
573 else:
574 expired = not (self.timer() < item.expires)
575 if expired:
576 return self.__missing__(key)
577 else:
578 return cache_getitem(self, key)
580 def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
581 with self.timer as time:
582 expires = self.__ttu(key, value, time)
583 if not (time < expires):
584 return # skip expired items
585 self.expire(time)
586 cache_setitem(self, key, value)
587 # removing an existing item would break the heap structure, so
588 # only mark it as removed for now
589 try:
590 self.__getitem(key).removed = True
591 except KeyError:
592 pass
593 self.__items[key] = item = TLRUCache._Item(key, expires)
594 heapq.heappush(self.__order, item)
596 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
597 with self.timer as time:
598 # no self.expire() for performance reasons, e.g. self.clear() [#67]
599 cache_delitem(self, key)
600 item = self.__items.pop(key)
601 item.removed = True
602 if not (time < item.expires):
603 raise KeyError(key)
605 def __iter__(self):
606 for curr in self.__order:
607 # "freeze" time for iterator access
608 with self.timer as time:
609 if time < curr.expires and not curr.removed:
610 yield curr.key
612 @property
613 def ttu(self):
614 """The local time-to-use function used by the cache."""
615 return self.__ttu
617 def expire(self, time=None):
618 """Remove expired items from the cache and return an iterable of the
619 expired `(key, value)` pairs.
621 """
622 if time is None:
623 time = self.timer()
624 items = self.__items
625 order = self.__order
626 # clean up the heap if too many items are marked as removed
627 if len(order) > len(items) * 2:
628 self.__order = order = [item for item in order if not item.removed]
629 heapq.heapify(order)
630 expired = []
631 cache_delitem = Cache.__delitem__
632 cache_getitem = Cache.__getitem__
633 while order and (order[0].removed or not (time < order[0].expires)):
634 item = heapq.heappop(order)
635 if not item.removed:
636 expired.append((item.key, cache_getitem(self, item.key)))
637 cache_delitem(self, item.key)
638 del items[item.key]
639 return expired
641 def popitem(self):
642 """Remove and return the `(key, value)` pair least recently used that
643 has not already expired.
645 """
646 with self.timer as time:
647 self.expire(time)
648 try:
649 key = next(iter(self.__items))
650 except StopIteration:
651 raise KeyError("%s is empty" % type(self).__name__) from None
652 else:
653 return (key, self.pop(key))
655 def __getitem(self, key):
656 value = self.__items[key]
657 self.__items.move_to_end(key)
658 return value
661_CacheInfo = collections.namedtuple(
662 "CacheInfo", ["hits", "misses", "maxsize", "currsize"]
663)
666def cached(cache, key=keys.hashkey, lock=None, condition=None, info=False):
667 """Decorator to wrap a function with a memoizing callable that saves
668 results in a cache.
670 """
671 from ._cached import _wrapper
673 if isinstance(condition, bool):
674 from warnings import warn
676 warn(
677 "passing `info` as positional parameter is deprecated",
678 DeprecationWarning,
679 stacklevel=2,
680 )
681 info = condition
682 condition = None
684 def decorator(func):
685 if info:
686 if isinstance(cache, Cache):
688 def make_info(hits, misses):
689 return _CacheInfo(hits, misses, cache.maxsize, cache.currsize)
691 elif isinstance(cache, collections.abc.Mapping):
693 def make_info(hits, misses):
694 return _CacheInfo(hits, misses, None, len(cache))
696 else:
698 def make_info(hits, misses):
699 return _CacheInfo(hits, misses, 0, 0)
701 return _wrapper(func, cache, key, lock, condition, info=make_info)
702 else:
703 return _wrapper(func, cache, key, lock, condition)
705 return decorator
708def cachedmethod(cache, key=keys.methodkey, lock=None, condition=None):
709 """Decorator to wrap a class or instance method with a memoizing
710 callable that saves results in a cache.
712 """
713 from ._cachedmethod import _wrapper
715 def decorator(method):
716 return _wrapper(method, cache, key, lock, condition)
718 return decorator