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

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

## 161 statements 124 run 37 missing 0 excluded 7 partial

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

2 Same 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

6 from __future__ import absolute_import

7

8 import collections

9 import time

10

11# CHANGE: import from cachetools instead of .cache

12 from cachetools import Cache

13

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

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

16

17

18 class _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

36 class _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

66 class 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