Hide keyboard shortcuts

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: 

5 

6from __future__ import absolute_import 

7 

8import collections 

9import time 

10 

11# CHANGE: import from cachetools instead of .cache 

12from cachetools import Cache 

13 

14# CHANGE: MAXTTL is the default TTL = 10 years 

15MAXTTL = 86400 * 365 * 10 # noqa: ignore well known magic constants 

16 

17 

18class _Link(object): 

19 

20 __slots__ = ('key', 'expire', 'next', 'prev') 

21 

22 def __init__(self, key=None, expire=None): 

23 self.key = key 

24 self.expire = expire 

25 

26 def __reduce__(self): 

27 return _Link, (self.key, self.expire) 

28 

29 def unlink(self): 

30 next = self.next 

31 prev = self.prev 

32 prev.next = next 

33 next.prev = prev 

34 

35 

36class _Timer(object): 

37 

38 def __init__(self, timer): 

39 self.__timer = timer 

40 self.__nesting = 0 

41 

42 def __call__(self): 

43 if self.__nesting == 0: 

44 return self.__timer() 

45 else: 

46 return self.__time 

47 

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 

55 

56 def __exit__(self, *exc): 

57 self.__nesting -= 1 

58 

59 def __reduce__(self): 

60 return _Timer, (self.__timer,) 

61 

62 def __getattr__(self, name): 

63 return getattr(self.__timer, name) 

64 

65 

66class TTLCache(Cache): 

67 """LRU Cache implementation with per-item time-to-live (TTL) value.""" 

68 

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__ 

79 

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()) 

87 

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) 

99 

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 

116 

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) 

123 

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 

133 

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 

143 

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()) 

153 

154 def __repr__(self, cache_repr=Cache.__repr__): 

155 with self.__timer as time: 

156 self.expire(time) 

157 return cache_repr(self) 

158 

159 @property 

160 def currsize(self): 

161 with self.__timer as time: 

162 self.expire(time) 

163 return super(TTLCache, self).currsize 

164 

165 @property 

166 def timer(self): 

167 """The timer function used by the cache.""" 

168 return self.__timer 

169 

170 @property 

171 def ttl(self): 

172 """The time-to-live value of the cache's items.""" 

173 return self.__ttl 

174 

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 

189 

190 def clear(self): 

191 with self.__timer as time: 

192 self.expire(time) 

193 Cache.clear(self) 

194 

195 def get(self, *args, **kwargs): 

196 with self.__timer: 

197 return Cache.get(self, *args, **kwargs) 

198 

199 def pop(self, *args, **kwargs): 

200 with self.__timer: 

201 return Cache.pop(self, *args, **kwargs) 

202 

203 def setdefault(self, *args, **kwargs): 

204 with self.__timer: 

205 return Cache.setdefault(self, *args, **kwargs) 

206 

207 def popitem(self): 

208 """Remove and return the `(key, value)` pair least recently used that 

209 has not already expired. 

210 

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)) 

220 

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