Coverage for gramex\services\ttlcache.py : 74%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1'''
2Same as cachetools/ttl.py 3.0.0, but with option to specify an expiry for EACH key.
3'''
4# Modifications are marked with CHANGE:
6from __future__ import absolute_import
8import collections
9import time
11# CHANGE: import from cachetools instead of .cache
12from cachetools import Cache
14# CHANGE: MAXTTL is the default TTL = 10 years
15MAXTTL = 86400 * 365 * 10 # noqa: ignore well known magic constants
18class _Link(object):
20 __slots__ = ('key', 'expire', 'next', 'prev')
22 def __init__(self, key=None, expire=None):
23 self.key = key
24 self.expire = expire
26 def __reduce__(self):
27 return _Link, (self.key, self.expire)
29 def unlink(self):
30 next = self.next
31 prev = self.prev
32 prev.next = next
33 next.prev = prev
36class _Timer(object):
38 def __init__(self, timer):
39 self.__timer = timer
40 self.__nesting = 0
42 def __call__(self):
43 if self.__nesting == 0:
44 return self.__timer()
45 else:
46 return self.__time
48 def __enter__(self):
49 if self.__nesting == 0:
50 self.__time = time = self.__timer()
51 else:
52 time = self.__time
53 self.__nesting += 1
54 return time
56 def __exit__(self, *exc):
57 self.__nesting -= 1
59 def __reduce__(self):
60 return _Timer, (self.__timer,)
62 def __getattr__(self, name):
63 return getattr(self.__timer, name)
66class TTLCache(Cache):
67 """LRU Cache implementation with per-item time-to-live (TTL) value."""
69 # CHANGE: ttl parameter defaults to MAXTTL
70 def __init__(self, maxsize, ttl=MAXTTL, timer=time.time, getsizeof=None):
71 Cache.__init__(self, maxsize, getsizeof)
72 self.__root = root = _Link()
73 root.prev = root.next = root
74 self.__links = collections.OrderedDict()
75 self.__timer = _Timer(timer)
76 self.__ttl = ttl
77 # CHANGE: .set() is the same as setitem
78 self.set = self.__setitem__
80 def __contains__(self, key):
81 try:
82 link = self.__links[key] # no reordering
83 except KeyError:
84 return False
85 else:
86 return not (link.expire < self.__timer())
88 def __getitem__(self, key, cache_getitem=Cache.__getitem__):
89 try:
90 link = self.__getlink(key)
91 except KeyError:
92 expired = False
93 else:
94 expired = link.expire < self.__timer()
95 if expired: 95 ↛ 96line 95 didn't jump to line 96, because the condition on line 95 was never true
96 return self.__missing__(key)
97 else:
98 return cache_getitem(self, key)
100 # CHANGE: optional expire=None parameter added
101 def __setitem__(self, key, value, expire=None, cache_setitem=Cache.__setitem__):
102 with self.__timer as time:
103 self.expire(time)
104 cache_setitem(self, key, value)
105 try:
106 link = self.__getlink(key)
107 except KeyError:
108 self.__links[key] = link = _Link(key)
109 else:
110 link.unlink()
111 # CHANGE: Expire time based on expire parameter, or default TTL
112 link.expire = time + (self.__ttl if expire is None else expire)
113 link.next = root = self.__root
114 link.prev = prev = root.prev
115 prev.next = root.prev = link
117 def __delitem__(self, key, cache_delitem=Cache.__delitem__):
118 cache_delitem(self, key)
119 link = self.__links.pop(key)
120 link.unlink()
121 if link.expire < self.__timer(): 121 ↛ 122line 121 didn't jump to line 122, because the condition on line 121 was never true
122 raise KeyError(key)
124 def __iter__(self):
125 root = self.__root
126 curr = root.next
127 while curr is not root:
128 # "freeze" time for iterator access
129 with self.__timer as time:
130 if not (curr.expire < time): 130 ↛ 132line 130 didn't jump to line 132, because the condition on line 130 was never false
131 yield curr.key
132 curr = curr.next
134 def __len__(self):
135 root = self.__root
136 curr = root.next
137 time = self.__timer()
138 count = len(self.__links)
139 while curr is not root and curr.expire < time: 139 ↛ 140line 139 didn't jump to line 140, because the condition on line 139 was never true
140 count -= 1
141 curr = curr.next
142 return count
144 def __setstate__(self, state):
145 self.__dict__.update(state)
146 root = self.__root
147 root.prev = root.next = root
148 for link in sorted(self.__links.values(), key=lambda obj: obj.expire):
149 link.next = root
150 link.prev = prev = root.prev
151 prev.next = root.prev = link
152 self.expire(self.__timer())
154 def __repr__(self, cache_repr=Cache.__repr__):
155 with self.__timer as time:
156 self.expire(time)
157 return cache_repr(self)
159 @property
160 def currsize(self):
161 with self.__timer as time:
162 self.expire(time)
163 return super(TTLCache, self).currsize
165 @property
166 def timer(self):
167 """The timer function used by the cache."""
168 return self.__timer
170 @property
171 def ttl(self):
172 """The time-to-live value of the cache's items."""
173 return self.__ttl
175 def expire(self, time=None):
176 """Remove expired items from the cache."""
177 if time is None: 177 ↛ 178line 177 didn't jump to line 178, because the condition on line 177 was never true
178 time = self.__timer()
179 root = self.__root
180 curr = root.next
181 links = self.__links
182 cache_delitem = Cache.__delitem__
183 while curr is not root and curr.expire < time: 183 ↛ 184line 183 didn't jump to line 184, because the condition on line 183 was never true
184 cache_delitem(self, curr.key)
185 del links[curr.key]
186 next = curr.next
187 curr.unlink()
188 curr = next
190 def clear(self):
191 with self.__timer as time:
192 self.expire(time)
193 Cache.clear(self)
195 def get(self, *args, **kwargs):
196 with self.__timer:
197 return Cache.get(self, *args, **kwargs)
199 def pop(self, *args, **kwargs):
200 with self.__timer:
201 return Cache.pop(self, *args, **kwargs)
203 def setdefault(self, *args, **kwargs):
204 with self.__timer:
205 return Cache.setdefault(self, *args, **kwargs)
207 def popitem(self):
208 """Remove and return the `(key, value)` pair least recently used that
209 has not already expired.
211 """
212 with self.__timer as time:
213 self.expire(time)
214 try:
215 key = next(iter(self.__links))
216 except StopIteration:
217 raise KeyError('%s is empty' % self.__class__.__name__)
218 else:
219 return (key, self.pop(key))
221 if hasattr(collections.OrderedDict, 'move_to_end'): 221 ↛ 227line 221 didn't jump to line 227, because the condition on line 221 was never false
222 def __getlink(self, key): # noqa: ignore N802: function name should be in lowercase
223 value = self.__links[key]
224 self.__links.move_to_end(key)
225 return value
226 else:
227 def __getlink(self, key): # noqa: ignore N802: function name should be in lowercase
228 value = self.__links.pop(key)
229 self.__links[key] = value
230 return value