Skip to content

Commit a3c6b58

Browse files
committed
fix timed-sized cache
- add cache_info and cache_access attributes - add and update tests
1 parent 8e57f9b commit a3c6b58

File tree

5 files changed

+115
-25
lines changed

5 files changed

+115
-25
lines changed

README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,20 @@
1010

1111
The `cached` decorator requires the arguments of the wrapped function to be hashable.
1212
`cached` accepts arguments `capacity` and `seconds` to limit the size of the cache and limit age of cached items.
13+
`cached` functions similarly to `functools.lru_cache` with the additional of the optional time constraint.
1314

1415
```python
16+
import time
1517
from cashed import cached
1618

17-
@cached(capacity=100)
19+
@cached(capacity=100, seconds=5)
1820
def fib(n):
1921
if n == 0 or n == 1:
2022
return n
2123
return fib(n-1) + fib(n-2)
24+
25+
fib(100) # -> 354224848179261915075L
26+
fib.cache_info() # -> {'seconds': 5, 'hits': 98, 'capacity': 100, 'misses': 101, 'size': 100}
27+
time.sleep(5)
28+
fib.cache_info() # -> {'hits': 98, 'capacity': 100, 'seconds': 5, 'misses': 101, 'size': 0}
2229
```

cashed/cache.py

+72-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections import OrderedDict
2+
from collections import namedtuple
23
from functools import wraps
34
import time
45

@@ -61,7 +62,7 @@ def timed_get(self, key):
6162
now = time.time()
6263
then, val = self.store[key]
6364
if (now - then) > self.limit:
64-
del self.store[key]
65+
self.__delitem__(key)
6566
raise KeyError
6667
return val
6768

@@ -73,7 +74,7 @@ def timed_set(self, key, value, return_dont_set=False):
7374
self.store[key] = stamped_val
7475

7576
def timed_del(self, key):
76-
del self[key]
77+
del self.store[key]
7778

7879

7980
class TimedCache(Timed):
@@ -89,21 +90,36 @@ def __init__(self, seconds=None):
8990
raise CacheError("TimedCache expects a single argument: `seconds`")
9091
self.store = {}
9192
self.limit = seconds
93+
self.size = 0
9294

9395
def __getitem__(self, key):
9496
return self.timed_get(key)
9597

9698
def __setitem__(self, key, value):
99+
if key not in self.store:
100+
self.size += 1
97101
return self.timed_set(key, value)
98102

99-
def __delitem(self, key):
100-
del self.store[key]
103+
def __delitem__(self, key):
104+
self.size -= 1
105+
return self.timed_del(key)
101106

102107
def __repr__(self):
103-
return '<TimedCache[{}s]: {}>'.format(self.limit, [(k, self.store[k]) for k in self.store])
108+
return '<TimedCache[{}s]>'.format(self.limit)
109+
110+
def _safe_get(self, k):
111+
try:
112+
return self[k]
113+
except KeyError:
114+
return None
104115

105116
def items(self):
106-
return self.store.items()
117+
keys = list(self.store.keys())
118+
return (pair for pair in ((k, self._safe_get(k)) for k in keys)
119+
if pair[-1] is not None)
120+
121+
def clean(self):
122+
[_ for _ in self.items()]
107123

108124

109125
class SizedCache(Sized):
@@ -132,11 +148,11 @@ def __delitem__(self, key):
132148
return self.sized_del(key)
133149

134150
def __repr__(self):
135-
return '<SizedCache[{}cap : {}size]: {}>'\
136-
.format(self.capacity, self.size, [(k, self.store[k]) for k in self.store])
151+
return '<SizedCache[{}cap : {}size]>'\
152+
.format(self.capacity, self.size)
137153

138154
def items(self):
139-
return self.store.items()
155+
return ((k, self[k]) for k in self.store)
140156

141157

142158
class TimedSizedCache(Timed, Sized):
@@ -157,7 +173,8 @@ def __init__(self, capacity=None, seconds=None):
157173

158174
def __getitem__(self, key):
159175
_ = self.timed_get(key)
160-
return self.sized_get(key)
176+
t, value = self.sized_get(key)
177+
return value
161178

162179
def __setitem__(self, key, value):
163180
stamped_val = self.timed_set(key, value, return_dont_set=True)
@@ -167,23 +184,54 @@ def __delitem__(self, key):
167184
return self.sized_del(key)
168185

169186
def __repr__(self):
170-
return '<TimedSizedCache[{}cap, {}size, {}s]: {}>'\
171-
.format(self.capacity, self.size, self.limit, [(k, self.store[k]) for k in self.store])
187+
return '<TimedSizedCache[{}cap, {}size, {}s]>'\
188+
.format(self.capacity, self.size, self.limit)
189+
190+
def _safe_get(self, k):
191+
try:
192+
return self[k]
193+
except KeyError:
194+
return None
172195

173196
def items(self):
174-
return self.store.items()
197+
keys = list(self.store.keys())
198+
return (pair for pair in ((k, self._safe_get(k)) for k in keys)
199+
if pair[-1] is not None)
200+
201+
def clean(self):
202+
[_ for _ in self.items()]
203+
204+
205+
class CountDict(dict):
206+
def __init__(self):
207+
self.size = 0
208+
super(CountDict, self).__init__()
209+
210+
def __setitem__(self, key, value):
211+
if key not in self:
212+
self.size += 1
213+
super(CountDict, self).__setitem__(key, value)
214+
215+
def __delitem__(self, key):
216+
self.size -= 1
217+
super(CountDict, self).__delitem__(key, value)
175218

176219

177220
def cached(capacity=None, seconds=None, debug=False):
178221
""" Decorator to wrap a function with an optionally sized or timed cache. """
222+
info = {'hits': 0, 'misses': 0}
179223
if capacity is not None and seconds is not None:
180224
cache = TimedSizedCache(capacity=capacity, seconds=seconds)
225+
info['capacity'] = capacity
226+
info['seconds'] = seconds
181227
elif capacity is not None:
182228
cache = SizedCache(capacity=capacity)
229+
info['capacity'] = capacity
183230
elif seconds is not None:
184231
cache = TimedCache(seconds=seconds)
232+
info['seconds'] = seconds
185233
else:
186-
cache = {}
234+
cache = CountDict()
187235

188236
def _cached(func):
189237
@wraps(func)
@@ -192,16 +240,26 @@ def wrapper(*args, **kwargs):
192240
try:
193241
v = cache[key]
194242
was_cached = True
243+
info['hits'] += 1
195244
except TypeError:
196245
raise CacheError('Inputs args: {}, kwargs: {} are not cacheable. All arguments must be hashable'\
197246
.format(args, kwargs))
198247
except KeyError:
199248
v = func(*args, **kwargs)
200249
cache[key] = v
201250
was_cached = False
251+
info['misses'] += 1
202252
if debug:
203253
return v, was_cached
204254
return v
255+
256+
def _cache_info():
257+
if hasattr(cache, 'clean'):
258+
cache.clean()
259+
info.update({'size': cache.size})
260+
return info
261+
wrapper.cache_info = _cache_info
262+
wrapper.cache_access = cache
205263
return wrapper
206264
return _cached
207265

examples/simple.py

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def slow_op(name, pet_name=None):
2222
start = time.time()
2323
ans = slow_op(name, pet_name=pet)
2424
print('{} -> {}'.format(time.time()-start, ans))
25+
print(slow_op.cache_info())
2526

2627

2728
######
@@ -46,4 +47,5 @@ def fib_fast(n):
4647
for n in range(30):
4748
v = func(n)
4849
print(' ** {} -> {}s **'.format(_type, time.time()-start))
50+
print(fib_fast.cache_info())
4951

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name="cashed",
5-
version="0.1.2",
5+
version="0.2.0",
66
description="Simple caching using decorators",
77
author="James Kominick",
88
author_email="[email protected]",

tests/test_simple.py

+32-9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def test_basic_cache(self):
2626
elap2 = time.time() - start
2727

2828
self.assertTrue(elap1 > elap2, "cached op is faster")
29+
self.assertTrue(self.cached_op.cache_info()['size'] == 1)
2930

3031

3132
class SizedCacheTest(unittest.TestCase):
@@ -37,13 +38,21 @@ def cached_op(k, value=None):
3738
self.cached_op = cached_op
3839

3940
def test_sized_cache(self):
40-
[self.cached_op(n) for n in range(3)]
41-
_, was_cached = self.cached_op(2)
41+
[self.cached_op(n, value=n) for n in range(3)]
42+
ans, was_cached = self.cached_op(2, value=2)
4243
self.assertTrue(was_cached)
43-
_, was_cached = self.cached_op(20)
44+
self.assertTrue(ans == 2)
45+
ans, was_cached = self.cached_op(20, value=30)
4446
self.assertFalse(was_cached)
45-
_, was_cached = self.cached_op(0)
47+
self.assertTrue(ans == 30)
48+
ans, was_cached = self.cached_op(0)
4649
self.assertFalse(was_cached)
50+
self.assertTrue(ans is None)
51+
52+
info = self.cached_op.cache_info()
53+
self.assertTrue(info['hits'] == 1)
54+
self.assertTrue(info['misses'] == 5)
55+
self.assertTrue(info['size'] == 3)
4756

4857

4958
class TimedCacheTest(unittest.TestCase):
@@ -56,11 +65,22 @@ def cached_op(n):
5665

5766
def test_timed_cache(self):
5867
self.cached_op(1)
59-
_, was_cached = self.cached_op(1)
68+
ans, was_cached = self.cached_op(1)
6069
self.assertTrue(was_cached)
70+
self.assertTrue(ans == 1)
6171
time.sleep(3)
62-
_, was_cached = self.cached_op(1)
72+
ans, was_cached = self.cached_op(1)
6373
self.assertFalse(was_cached)
74+
self.assertTrue(ans == 1)
75+
76+
info = self.cached_op.cache_info()
77+
self.assertTrue(info['hits'] == 1)
78+
self.assertTrue(info['misses'] == 2)
79+
self.assertTrue(info['size'] == 1)
80+
81+
time.sleep(3)
82+
info = self.cached_op.cache_info()
83+
self.assertTrue(info['size'] == 0)
6484

6585

6686
class TimedSizedCacheTest(unittest.TestCase):
@@ -73,12 +93,15 @@ def cached_op(n):
7393

7494
def test_timedsized_cache(self):
7595
[self.cached_op(n) for n in range(2)]
76-
_, was_cached = self.cached_op(2)
96+
ans, was_cached = self.cached_op(2)
7797
self.assertFalse(was_cached)
78-
_, was_cached = self.cached_op(0)
98+
self.assertTrue(ans == 2)
99+
ans, was_cached = self.cached_op(0)
79100
self.assertFalse(was_cached)
101+
self.assertTrue(ans == 0)
80102
time.sleep(3)
81-
_, was_cached = self.cached_op(2)
103+
ans, was_cached = self.cached_op(2)
104+
self.assertTrue(ans == 2)
82105
self.assertFalse(was_cached)
83106

84107

0 commit comments

Comments
 (0)