|
"""Extensible memoizing collections and decorators.""" |
|
|
|
__all__ = ( |
|
"Cache", |
|
"FIFOCache", |
|
"LFUCache", |
|
"LRUCache", |
|
"MRUCache", |
|
"RRCache", |
|
"TLRUCache", |
|
"TTLCache", |
|
"cached", |
|
"cachedmethod", |
|
) |
|
|
|
__version__ = "5.3.1" |
|
|
|
import collections |
|
import collections.abc |
|
import functools |
|
import heapq |
|
import random |
|
import time |
|
|
|
from . import keys |
|
|
|
|
|
class _DefaultSize: |
|
|
|
__slots__ = () |
|
|
|
def __getitem__(self, _): |
|
return 1 |
|
|
|
def __setitem__(self, _, value): |
|
assert value == 1 |
|
|
|
def pop(self, _): |
|
return 1 |
|
|
|
|
|
class Cache(collections.abc.MutableMapping): |
|
"""Mutable mapping to serve as a simple cache or cache base class.""" |
|
|
|
__marker = object() |
|
|
|
__size = _DefaultSize() |
|
|
|
def __init__(self, maxsize, getsizeof=None): |
|
if getsizeof: |
|
self.getsizeof = getsizeof |
|
if self.getsizeof is not Cache.getsizeof: |
|
self.__size = dict() |
|
self.__data = dict() |
|
self.__currsize = 0 |
|
self.__maxsize = maxsize |
|
|
|
def __repr__(self): |
|
return "%s(%s, maxsize=%r, currsize=%r)" % ( |
|
self.__class__.__name__, |
|
repr(self.__data), |
|
self.__maxsize, |
|
self.__currsize, |
|
) |
|
|
|
def __getitem__(self, key): |
|
try: |
|
return self.__data[key] |
|
except KeyError: |
|
return self.__missing__(key) |
|
|
|
def __setitem__(self, key, value): |
|
maxsize = self.__maxsize |
|
size = self.getsizeof(value) |
|
if size > maxsize: |
|
raise ValueError("value too large") |
|
if key not in self.__data or self.__size[key] < size: |
|
while self.__currsize + size > maxsize: |
|
self.popitem() |
|
if key in self.__data: |
|
diffsize = size - self.__size[key] |
|
else: |
|
diffsize = size |
|
self.__data[key] = value |
|
self.__size[key] = size |
|
self.__currsize += diffsize |
|
|
|
def __delitem__(self, key): |
|
size = self.__size.pop(key) |
|
del self.__data[key] |
|
self.__currsize -= size |
|
|
|
def __contains__(self, key): |
|
return key in self.__data |
|
|
|
def __missing__(self, key): |
|
raise KeyError(key) |
|
|
|
def __iter__(self): |
|
return iter(self.__data) |
|
|
|
def __len__(self): |
|
return len(self.__data) |
|
|
|
def get(self, key, default=None): |
|
if key in self: |
|
return self[key] |
|
else: |
|
return default |
|
|
|
def pop(self, key, default=__marker): |
|
if key in self: |
|
value = self[key] |
|
del self[key] |
|
elif default is self.__marker: |
|
raise KeyError(key) |
|
else: |
|
value = default |
|
return value |
|
|
|
def setdefault(self, key, default=None): |
|
if key in self: |
|
value = self[key] |
|
else: |
|
self[key] = value = default |
|
return value |
|
|
|
@property |
|
def maxsize(self): |
|
"""The maximum size of the cache.""" |
|
return self.__maxsize |
|
|
|
@property |
|
def currsize(self): |
|
"""The current size of the cache.""" |
|
return self.__currsize |
|
|
|
@staticmethod |
|
def getsizeof(value): |
|
"""Return the size of a cache element's value.""" |
|
return 1 |
|
|
|
|
|
class FIFOCache(Cache): |
|
"""First In First Out (FIFO) cache implementation.""" |
|
|
|
def __init__(self, maxsize, getsizeof=None): |
|
Cache.__init__(self, maxsize, getsizeof) |
|
self.__order = collections.OrderedDict() |
|
|
|
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): |
|
cache_setitem(self, key, value) |
|
try: |
|
self.__order.move_to_end(key) |
|
except KeyError: |
|
self.__order[key] = None |
|
|
|
def __delitem__(self, key, cache_delitem=Cache.__delitem__): |
|
cache_delitem(self, key) |
|
del self.__order[key] |
|
|
|
def popitem(self): |
|
"""Remove and return the `(key, value)` pair first inserted.""" |
|
try: |
|
key = next(iter(self.__order)) |
|
except StopIteration: |
|
raise KeyError("%s is empty" % type(self).__name__) from None |
|
else: |
|
return (key, self.pop(key)) |
|
|
|
|
|
class LFUCache(Cache): |
|
"""Least Frequently Used (LFU) cache implementation.""" |
|
|
|
def __init__(self, maxsize, getsizeof=None): |
|
Cache.__init__(self, maxsize, getsizeof) |
|
self.__counter = collections.Counter() |
|
|
|
def __getitem__(self, key, cache_getitem=Cache.__getitem__): |
|
value = cache_getitem(self, key) |
|
if key in self: |
|
self.__counter[key] -= 1 |
|
return value |
|
|
|
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): |
|
cache_setitem(self, key, value) |
|
self.__counter[key] -= 1 |
|
|
|
def __delitem__(self, key, cache_delitem=Cache.__delitem__): |
|
cache_delitem(self, key) |
|
del self.__counter[key] |
|
|
|
def popitem(self): |
|
"""Remove and return the `(key, value)` pair least frequently used.""" |
|
try: |
|
((key, _),) = self.__counter.most_common(1) |
|
except ValueError: |
|
raise KeyError("%s is empty" % type(self).__name__) from None |
|
else: |
|
return (key, self.pop(key)) |
|
|
|
|
|
class LRUCache(Cache): |
|
"""Least Recently Used (LRU) cache implementation.""" |
|
|
|
def __init__(self, maxsize, getsizeof=None): |
|
Cache.__init__(self, maxsize, getsizeof) |
|
self.__order = collections.OrderedDict() |
|
|
|
def __getitem__(self, key, cache_getitem=Cache.__getitem__): |
|
value = cache_getitem(self, key) |
|
if key in self: |
|
self.__update(key) |
|
return value |
|
|
|
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): |
|
cache_setitem(self, key, value) |
|
self.__update(key) |
|
|
|
def __delitem__(self, key, cache_delitem=Cache.__delitem__): |
|
cache_delitem(self, key) |
|
del self.__order[key] |
|
|
|
def popitem(self): |
|
"""Remove and return the `(key, value)` pair least recently used.""" |
|
try: |
|
key = next(iter(self.__order)) |
|
except StopIteration: |
|
raise KeyError("%s is empty" % type(self).__name__) from None |
|
else: |
|
return (key, self.pop(key)) |
|
|
|
def __update(self, key): |
|
try: |
|
self.__order.move_to_end(key) |
|
except KeyError: |
|
self.__order[key] = None |
|
|
|
|
|
class MRUCache(Cache): |
|
"""Most Recently Used (MRU) cache implementation.""" |
|
|
|
def __init__(self, maxsize, getsizeof=None): |
|
Cache.__init__(self, maxsize, getsizeof) |
|
self.__order = collections.OrderedDict() |
|
|
|
def __getitem__(self, key, cache_getitem=Cache.__getitem__): |
|
value = cache_getitem(self, key) |
|
if key in self: |
|
self.__update(key) |
|
return value |
|
|
|
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): |
|
cache_setitem(self, key, value) |
|
self.__update(key) |
|
|
|
def __delitem__(self, key, cache_delitem=Cache.__delitem__): |
|
cache_delitem(self, key) |
|
del self.__order[key] |
|
|
|
def popitem(self): |
|
"""Remove and return the `(key, value)` pair most recently used.""" |
|
try: |
|
key = next(iter(self.__order)) |
|
except StopIteration: |
|
raise KeyError("%s is empty" % type(self).__name__) from None |
|
else: |
|
return (key, self.pop(key)) |
|
|
|
def __update(self, key): |
|
try: |
|
self.__order.move_to_end(key, last=False) |
|
except KeyError: |
|
self.__order[key] = None |
|
|
|
|
|
class RRCache(Cache): |
|
"""Random Replacement (RR) cache implementation.""" |
|
|
|
def __init__(self, maxsize, choice=random.choice, getsizeof=None): |
|
Cache.__init__(self, maxsize, getsizeof) |
|
self.__choice = choice |
|
|
|
@property |
|
def choice(self): |
|
"""The `choice` function used by the cache.""" |
|
return self.__choice |
|
|
|
def popitem(self): |
|
"""Remove and return a random `(key, value)` pair.""" |
|
try: |
|
key = self.__choice(list(self)) |
|
except IndexError: |
|
raise KeyError("%s is empty" % type(self).__name__) from None |
|
else: |
|
return (key, self.pop(key)) |
|
|
|
|
|
class _TimedCache(Cache): |
|
"""Base class for time aware cache implementations.""" |
|
|
|
class _Timer: |
|
def __init__(self, timer): |
|
self.__timer = timer |
|
self.__nesting = 0 |
|
|
|
def __call__(self): |
|
if self.__nesting == 0: |
|
return self.__timer() |
|
else: |
|
return self.__time |
|
|
|
def __enter__(self): |
|
if self.__nesting == 0: |
|
self.__time = time = self.__timer() |
|
else: |
|
time = self.__time |
|
self.__nesting += 1 |
|
return time |
|
|
|
def __exit__(self, *exc): |
|
self.__nesting -= 1 |
|
|
|
def __reduce__(self): |
|
return _TimedCache._Timer, (self.__timer,) |
|
|
|
def __getattr__(self, name): |
|
return getattr(self.__timer, name) |
|
|
|
def __init__(self, maxsize, timer=time.monotonic, getsizeof=None): |
|
Cache.__init__(self, maxsize, getsizeof) |
|
self.__timer = _TimedCache._Timer(timer) |
|
|
|
def __repr__(self, cache_repr=Cache.__repr__): |
|
with self.__timer as time: |
|
self.expire(time) |
|
return cache_repr(self) |
|
|
|
def __len__(self, cache_len=Cache.__len__): |
|
with self.__timer as time: |
|
self.expire(time) |
|
return cache_len(self) |
|
|
|
@property |
|
def currsize(self): |
|
with self.__timer as time: |
|
self.expire(time) |
|
return super().currsize |
|
|
|
@property |
|
def timer(self): |
|
"""The timer function used by the cache.""" |
|
return self.__timer |
|
|
|
def clear(self): |
|
with self.__timer as time: |
|
self.expire(time) |
|
Cache.clear(self) |
|
|
|
def get(self, *args, **kwargs): |
|
with self.__timer: |
|
return Cache.get(self, *args, **kwargs) |
|
|
|
def pop(self, *args, **kwargs): |
|
with self.__timer: |
|
return Cache.pop(self, *args, **kwargs) |
|
|
|
def setdefault(self, *args, **kwargs): |
|
with self.__timer: |
|
return Cache.setdefault(self, *args, **kwargs) |
|
|
|
|
|
class TTLCache(_TimedCache): |
|
"""LRU Cache implementation with per-item time-to-live (TTL) value.""" |
|
|
|
class _Link: |
|
|
|
__slots__ = ("key", "expires", "next", "prev") |
|
|
|
def __init__(self, key=None, expires=None): |
|
self.key = key |
|
self.expires = expires |
|
|
|
def __reduce__(self): |
|
return TTLCache._Link, (self.key, self.expires) |
|
|
|
def unlink(self): |
|
next = self.next |
|
prev = self.prev |
|
prev.next = next |
|
next.prev = prev |
|
|
|
def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None): |
|
_TimedCache.__init__(self, maxsize, timer, getsizeof) |
|
self.__root = root = TTLCache._Link() |
|
root.prev = root.next = root |
|
self.__links = collections.OrderedDict() |
|
self.__ttl = ttl |
|
|
|
def __contains__(self, key): |
|
try: |
|
link = self.__links[key] |
|
except KeyError: |
|
return False |
|
else: |
|
return self.timer() < link.expires |
|
|
|
def __getitem__(self, key, cache_getitem=Cache.__getitem__): |
|
try: |
|
link = self.__getlink(key) |
|
except KeyError: |
|
expired = False |
|
else: |
|
expired = not (self.timer() < link.expires) |
|
if expired: |
|
return self.__missing__(key) |
|
else: |
|
return cache_getitem(self, key) |
|
|
|
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): |
|
with self.timer as time: |
|
self.expire(time) |
|
cache_setitem(self, key, value) |
|
try: |
|
link = self.__getlink(key) |
|
except KeyError: |
|
self.__links[key] = link = TTLCache._Link(key) |
|
else: |
|
link.unlink() |
|
link.expires = time + self.__ttl |
|
link.next = root = self.__root |
|
link.prev = prev = root.prev |
|
prev.next = root.prev = link |
|
|
|
def __delitem__(self, key, cache_delitem=Cache.__delitem__): |
|
cache_delitem(self, key) |
|
link = self.__links.pop(key) |
|
link.unlink() |
|
if not (self.timer() < link.expires): |
|
raise KeyError(key) |
|
|
|
def __iter__(self): |
|
root = self.__root |
|
curr = root.next |
|
while curr is not root: |
|
|
|
with self.timer as time: |
|
if time < curr.expires: |
|
yield curr.key |
|
curr = curr.next |
|
|
|
def __setstate__(self, state): |
|
self.__dict__.update(state) |
|
root = self.__root |
|
root.prev = root.next = root |
|
for link in sorted(self.__links.values(), key=lambda obj: obj.expires): |
|
link.next = root |
|
link.prev = prev = root.prev |
|
prev.next = root.prev = link |
|
self.expire(self.timer()) |
|
|
|
@property |
|
def ttl(self): |
|
"""The time-to-live value of the cache's items.""" |
|
return self.__ttl |
|
|
|
def expire(self, time=None): |
|
"""Remove expired items from the cache.""" |
|
if time is None: |
|
time = self.timer() |
|
root = self.__root |
|
curr = root.next |
|
links = self.__links |
|
cache_delitem = Cache.__delitem__ |
|
while curr is not root and not (time < curr.expires): |
|
cache_delitem(self, curr.key) |
|
del links[curr.key] |
|
next = curr.next |
|
curr.unlink() |
|
curr = next |
|
|
|
def popitem(self): |
|
"""Remove and return the `(key, value)` pair least recently used that |
|
has not already expired. |
|
|
|
""" |
|
with self.timer as time: |
|
self.expire(time) |
|
try: |
|
key = next(iter(self.__links)) |
|
except StopIteration: |
|
raise KeyError("%s is empty" % type(self).__name__) from None |
|
else: |
|
return (key, self.pop(key)) |
|
|
|
def __getlink(self, key): |
|
value = self.__links[key] |
|
self.__links.move_to_end(key) |
|
return value |
|
|
|
|
|
class TLRUCache(_TimedCache): |
|
"""Time aware Least Recently Used (TLRU) cache implementation.""" |
|
|
|
@functools.total_ordering |
|
class _Item: |
|
|
|
__slots__ = ("key", "expires", "removed") |
|
|
|
def __init__(self, key=None, expires=None): |
|
self.key = key |
|
self.expires = expires |
|
self.removed = False |
|
|
|
def __lt__(self, other): |
|
return self.expires < other.expires |
|
|
|
def __init__(self, maxsize, ttu, timer=time.monotonic, getsizeof=None): |
|
_TimedCache.__init__(self, maxsize, timer, getsizeof) |
|
self.__items = collections.OrderedDict() |
|
self.__order = [] |
|
self.__ttu = ttu |
|
|
|
def __contains__(self, key): |
|
try: |
|
item = self.__items[key] |
|
except KeyError: |
|
return False |
|
else: |
|
return self.timer() < item.expires |
|
|
|
def __getitem__(self, key, cache_getitem=Cache.__getitem__): |
|
try: |
|
item = self.__getitem(key) |
|
except KeyError: |
|
expired = False |
|
else: |
|
expired = not (self.timer() < item.expires) |
|
if expired: |
|
return self.__missing__(key) |
|
else: |
|
return cache_getitem(self, key) |
|
|
|
def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): |
|
with self.timer as time: |
|
expires = self.__ttu(key, value, time) |
|
if not (time < expires): |
|
return |
|
self.expire(time) |
|
cache_setitem(self, key, value) |
|
|
|
|
|
try: |
|
self.__getitem(key).removed = True |
|
except KeyError: |
|
pass |
|
self.__items[key] = item = TLRUCache._Item(key, expires) |
|
heapq.heappush(self.__order, item) |
|
|
|
def __delitem__(self, key, cache_delitem=Cache.__delitem__): |
|
with self.timer as time: |
|
|
|
cache_delitem(self, key) |
|
item = self.__items.pop(key) |
|
item.removed = True |
|
if not (time < item.expires): |
|
raise KeyError(key) |
|
|
|
def __iter__(self): |
|
for curr in self.__order: |
|
|
|
with self.timer as time: |
|
if time < curr.expires and not curr.removed: |
|
yield curr.key |
|
|
|
@property |
|
def ttu(self): |
|
"""The local time-to-use function used by the cache.""" |
|
return self.__ttu |
|
|
|
def expire(self, time=None): |
|
"""Remove expired items from the cache.""" |
|
if time is None: |
|
time = self.timer() |
|
items = self.__items |
|
order = self.__order |
|
|
|
if len(order) > len(items) * 2: |
|
self.__order = order = [item for item in order if not item.removed] |
|
heapq.heapify(order) |
|
cache_delitem = Cache.__delitem__ |
|
while order and (order[0].removed or not (time < order[0].expires)): |
|
item = heapq.heappop(order) |
|
if not item.removed: |
|
cache_delitem(self, item.key) |
|
del items[item.key] |
|
|
|
def popitem(self): |
|
"""Remove and return the `(key, value)` pair least recently used that |
|
has not already expired. |
|
|
|
""" |
|
with self.timer as time: |
|
self.expire(time) |
|
try: |
|
key = next(iter(self.__items)) |
|
except StopIteration: |
|
raise KeyError("%s is empty" % self.__class__.__name__) from None |
|
else: |
|
return (key, self.pop(key)) |
|
|
|
def __getitem(self, key): |
|
value = self.__items[key] |
|
self.__items.move_to_end(key) |
|
return value |
|
|
|
|
|
_CacheInfo = collections.namedtuple( |
|
"CacheInfo", ["hits", "misses", "maxsize", "currsize"] |
|
) |
|
|
|
|
|
def cached(cache, key=keys.hashkey, lock=None, info=False): |
|
"""Decorator to wrap a function with a memoizing callable that saves |
|
results in a cache. |
|
|
|
""" |
|
|
|
def decorator(func): |
|
if info: |
|
hits = misses = 0 |
|
|
|
if isinstance(cache, Cache): |
|
|
|
def getinfo(): |
|
nonlocal hits, misses |
|
return _CacheInfo(hits, misses, cache.maxsize, cache.currsize) |
|
|
|
elif isinstance(cache, collections.abc.Mapping): |
|
|
|
def getinfo(): |
|
nonlocal hits, misses |
|
return _CacheInfo(hits, misses, None, len(cache)) |
|
|
|
else: |
|
|
|
def getinfo(): |
|
nonlocal hits, misses |
|
return _CacheInfo(hits, misses, 0, 0) |
|
|
|
if cache is None: |
|
|
|
def wrapper(*args, **kwargs): |
|
nonlocal misses |
|
misses += 1 |
|
return func(*args, **kwargs) |
|
|
|
def cache_clear(): |
|
nonlocal hits, misses |
|
hits = misses = 0 |
|
|
|
cache_info = getinfo |
|
|
|
elif lock is None: |
|
|
|
def wrapper(*args, **kwargs): |
|
nonlocal hits, misses |
|
k = key(*args, **kwargs) |
|
try: |
|
result = cache[k] |
|
hits += 1 |
|
return result |
|
except KeyError: |
|
misses += 1 |
|
v = func(*args, **kwargs) |
|
try: |
|
cache[k] = v |
|
except ValueError: |
|
pass |
|
return v |
|
|
|
def cache_clear(): |
|
nonlocal hits, misses |
|
cache.clear() |
|
hits = misses = 0 |
|
|
|
cache_info = getinfo |
|
|
|
else: |
|
|
|
def wrapper(*args, **kwargs): |
|
nonlocal hits, misses |
|
k = key(*args, **kwargs) |
|
try: |
|
with lock: |
|
result = cache[k] |
|
hits += 1 |
|
return result |
|
except KeyError: |
|
with lock: |
|
misses += 1 |
|
v = func(*args, **kwargs) |
|
|
|
try: |
|
with lock: |
|
return cache.setdefault(k, v) |
|
except ValueError: |
|
return v |
|
|
|
def cache_clear(): |
|
nonlocal hits, misses |
|
with lock: |
|
cache.clear() |
|
hits = misses = 0 |
|
|
|
def cache_info(): |
|
with lock: |
|
return getinfo() |
|
|
|
else: |
|
if cache is None: |
|
|
|
def wrapper(*args, **kwargs): |
|
return func(*args, **kwargs) |
|
|
|
def cache_clear(): |
|
pass |
|
|
|
elif lock is None: |
|
|
|
def wrapper(*args, **kwargs): |
|
k = key(*args, **kwargs) |
|
try: |
|
return cache[k] |
|
except KeyError: |
|
pass |
|
v = func(*args, **kwargs) |
|
try: |
|
cache[k] = v |
|
except ValueError: |
|
pass |
|
return v |
|
|
|
def cache_clear(): |
|
cache.clear() |
|
|
|
else: |
|
|
|
def wrapper(*args, **kwargs): |
|
k = key(*args, **kwargs) |
|
try: |
|
with lock: |
|
return cache[k] |
|
except KeyError: |
|
pass |
|
v = func(*args, **kwargs) |
|
|
|
try: |
|
with lock: |
|
return cache.setdefault(k, v) |
|
except ValueError: |
|
return v |
|
|
|
def cache_clear(): |
|
with lock: |
|
cache.clear() |
|
|
|
cache_info = None |
|
|
|
wrapper.cache = cache |
|
wrapper.cache_key = key |
|
wrapper.cache_lock = lock |
|
wrapper.cache_clear = cache_clear |
|
wrapper.cache_info = cache_info |
|
|
|
return functools.update_wrapper(wrapper, func) |
|
|
|
return decorator |
|
|
|
|
|
def cachedmethod(cache, key=keys.methodkey, lock=None): |
|
"""Decorator to wrap a class or instance method with a memoizing |
|
callable that saves results in a cache. |
|
|
|
""" |
|
|
|
def decorator(method): |
|
if lock is None: |
|
|
|
def wrapper(self, *args, **kwargs): |
|
c = cache(self) |
|
if c is None: |
|
return method(self, *args, **kwargs) |
|
k = key(self, *args, **kwargs) |
|
try: |
|
return c[k] |
|
except KeyError: |
|
pass |
|
v = method(self, *args, **kwargs) |
|
try: |
|
c[k] = v |
|
except ValueError: |
|
pass |
|
return v |
|
|
|
def clear(self): |
|
c = cache(self) |
|
if c is not None: |
|
c.clear() |
|
|
|
else: |
|
|
|
def wrapper(self, *args, **kwargs): |
|
c = cache(self) |
|
if c is None: |
|
return method(self, *args, **kwargs) |
|
k = key(self, *args, **kwargs) |
|
try: |
|
with lock(self): |
|
return c[k] |
|
except KeyError: |
|
pass |
|
v = method(self, *args, **kwargs) |
|
|
|
try: |
|
with lock(self): |
|
return c.setdefault(k, v) |
|
except ValueError: |
|
return v |
|
|
|
def clear(self): |
|
c = cache(self) |
|
if c is not None: |
|
with lock(self): |
|
c.clear() |
|
|
|
wrapper.cache = cache |
|
wrapper.cache_key = key |
|
wrapper.cache_lock = lock |
|
wrapper.cache_clear = clear |
|
|
|
return functools.update_wrapper(wrapper, method) |
|
|
|
return decorator |
|
|