Skip to content

Commit

Permalink
Fix #157: Add TLRUCache implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
tkem committed Dec 18, 2021
1 parent f949504 commit 63ca17c
Show file tree
Hide file tree
Showing 2 changed files with 409 additions and 0 deletions.
159 changes: 159 additions & 0 deletions src/cachetools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"LRUCache",
"MRUCache",
"RRCache",
"TLRUCache",
"TTLCache",
"cached",
"cachedmethod",
Expand Down Expand Up @@ -497,6 +498,164 @@ def __getlink(self, key):
return value


class TLRUCache(Cache):
"""LRU Cache implementation with per-item time-to-use (TTU) value."""

def __init__(self, maxsize, ttu, timer=time.monotonic, getsizeof=None):
Cache.__init__(self, maxsize, getsizeof)
self.__root = root = _Link()
root.prev = root.next = root
self.__links = collections.OrderedDict()
self.__timer = _Timer(timer)
self.__ttu = ttu

def __contains__(self, key):
try:
link = self.__links[key] # no reordering
except KeyError:
return False
else:
return self.__timer() < link.expire

def __getitem__(self, key, cache_getitem=Cache.__getitem__):
try:
link = self.__getlink(key)
except KeyError:
expired = False
else:
expired = not (self.__timer() < link.expire)
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 = _Link(key)
else:
link.unlink()
link.expire = time + self.__ttu(value)
# FIXME: insert in sorted expiration order, start at the end
# of the linked list since we expect newer items to expire
# later; this is O(n) and should be replaced with e.g. RBTree
root = self.__root
prev = root.prev
while prev is not root and link.expire < prev.expire:
prev = prev.prev
link.next = next = prev.next
link.prev = prev
prev.next = next.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.expire):
raise KeyError(key)

def __iter__(self):
root = self.__root
curr = root.next
while curr is not root:
# "freeze" time for iterator access
with self.__timer as time:
if time < curr.expire:
yield curr.key
curr = curr.next

def __len__(self):
root = self.__root
curr = root.next
time = self.__timer()
count = len(self.__links)
while curr is not root and not (time < curr.expire):
count -= 1
curr = curr.next
return count

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.expire):
link.next = root
link.prev = prev = root.prev
prev.next = root.prev = link
self.expire(self.__timer())

def __repr__(self, cache_repr=Cache.__repr__):
with self.__timer as time:
self.expire(time)
return cache_repr(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 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.expire):
cache_delitem(self, curr.key)
del links[curr.key]
next = curr.next
curr.unlink()
curr = next

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)

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" % self.__class__.__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


def cached(cache, key=hashkey, lock=None):
"""Decorator to wrap a function with a memoizing callable that saves
results in a cache.
Expand Down
Loading

0 comments on commit 63ca17c

Please sign in to comment.