You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
5.7 KiB

  1. import collections
  2. import time
  3. from .cache import Cache
  4. class _Link(object):
  5. __slots__ = ('key', 'expire', 'next', 'prev')
  6. def __init__(self, key=None, expire=None):
  7. self.key = key
  8. self.expire = expire
  9. def __reduce__(self):
  10. return _Link, (self.key, self.expire)
  11. def unlink(self):
  12. next = self.next
  13. prev = self.prev
  14. prev.next = next
  15. next.prev = prev
  16. class _Timer(object):
  17. def __init__(self, timer):
  18. self.__timer = timer
  19. self.__nesting = 0
  20. def __call__(self):
  21. if self.__nesting == 0:
  22. return self.__timer()
  23. else:
  24. return self.__time
  25. def __enter__(self):
  26. if self.__nesting == 0:
  27. self.__time = time = self.__timer()
  28. else:
  29. time = self.__time
  30. self.__nesting += 1
  31. return time
  32. def __exit__(self, *exc):
  33. self.__nesting -= 1
  34. def __reduce__(self):
  35. return _Timer, (self.__timer,)
  36. def __getattr__(self, name):
  37. return getattr(self.__timer, name)
  38. class TTLCache(Cache):
  39. """LRU Cache implementation with per-item time-to-live (TTL) value."""
  40. def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None):
  41. Cache.__init__(self, maxsize, getsizeof)
  42. self.__root = root = _Link()
  43. root.prev = root.next = root
  44. self.__links = collections.OrderedDict()
  45. self.__timer = _Timer(timer)
  46. self.__ttl = ttl
  47. def __contains__(self, key):
  48. try:
  49. link = self.__links[key] # no reordering
  50. except KeyError:
  51. return False
  52. else:
  53. return not (link.expire < self.__timer())
  54. def __getitem__(self, key, cache_getitem=Cache.__getitem__):
  55. try:
  56. link = self.__getlink(key)
  57. except KeyError:
  58. expired = False
  59. else:
  60. expired = link.expire < self.__timer()
  61. if expired:
  62. return self.__missing__(key)
  63. else:
  64. return cache_getitem(self, key)
  65. def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
  66. with self.__timer as time:
  67. self.expire(time)
  68. cache_setitem(self, key, value)
  69. try:
  70. link = self.__getlink(key)
  71. except KeyError:
  72. self.__links[key] = link = _Link(key)
  73. else:
  74. link.unlink()
  75. link.expire = time + self.__ttl
  76. link.next = root = self.__root
  77. link.prev = prev = root.prev
  78. prev.next = root.prev = link
  79. def __delitem__(self, key, cache_delitem=Cache.__delitem__):
  80. cache_delitem(self, key)
  81. link = self.__links.pop(key)
  82. link.unlink()
  83. if link.expire < self.__timer():
  84. raise KeyError(key)
  85. def __iter__(self):
  86. root = self.__root
  87. curr = root.next
  88. while curr is not root:
  89. # "freeze" time for iterator access
  90. with self.__timer as time:
  91. if not (curr.expire < time):
  92. yield curr.key
  93. curr = curr.next
  94. def __len__(self):
  95. root = self.__root
  96. curr = root.next
  97. time = self.__timer()
  98. count = len(self.__links)
  99. while curr is not root and curr.expire < time:
  100. count -= 1
  101. curr = curr.next
  102. return count
  103. def __setstate__(self, state):
  104. self.__dict__.update(state)
  105. root = self.__root
  106. root.prev = root.next = root
  107. for link in sorted(self.__links.values(), key=lambda obj: obj.expire):
  108. link.next = root
  109. link.prev = prev = root.prev
  110. prev.next = root.prev = link
  111. self.expire(self.__timer())
  112. def __repr__(self, cache_repr=Cache.__repr__):
  113. with self.__timer as time:
  114. self.expire(time)
  115. return cache_repr(self)
  116. @property
  117. def currsize(self):
  118. with self.__timer as time:
  119. self.expire(time)
  120. return super(TTLCache, self).currsize
  121. @property
  122. def timer(self):
  123. """The timer function used by the cache."""
  124. return self.__timer
  125. @property
  126. def ttl(self):
  127. """The time-to-live value of the cache's items."""
  128. return self.__ttl
  129. def expire(self, time=None):
  130. """Remove expired items from the cache."""
  131. if time is None:
  132. time = self.__timer()
  133. root = self.__root
  134. curr = root.next
  135. links = self.__links
  136. cache_delitem = Cache.__delitem__
  137. while curr is not root and curr.expire < time:
  138. cache_delitem(self, curr.key)
  139. del links[curr.key]
  140. next = curr.next
  141. curr.unlink()
  142. curr = next
  143. def clear(self):
  144. with self.__timer as time:
  145. self.expire(time)
  146. Cache.clear(self)
  147. def get(self, *args, **kwargs):
  148. with self.__timer:
  149. return Cache.get(self, *args, **kwargs)
  150. def pop(self, *args, **kwargs):
  151. with self.__timer:
  152. return Cache.pop(self, *args, **kwargs)
  153. def setdefault(self, *args, **kwargs):
  154. with self.__timer:
  155. return Cache.setdefault(self, *args, **kwargs)
  156. def popitem(self):
  157. """Remove and return the `(key, value)` pair least recently used that
  158. has not already expired.
  159. """
  160. with self.__timer as time:
  161. self.expire(time)
  162. try:
  163. key = next(iter(self.__links))
  164. except StopIteration:
  165. msg = '%s is empty' % self.__class__.__name__
  166. raise KeyError(msg) from None
  167. else:
  168. return (key, self.pop(key))
  169. def __getlink(self, key):
  170. value = self.__links[key]
  171. self.__links.move_to_end(key)
  172. return value