Skip to content

Commit 99ea39d

Browse files
committed
func: Add lru_cache(), a coroutine version of functools.lru_cache
1 parent 3414a2c commit 99ea39d

File tree

2 files changed

+106
-1
lines changed

2 files changed

+106
-1
lines changed

aiotools/func.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import collections
12
import functools
23

34
__all__ = [
45
'apartial',
6+
'lru_cache',
57
]
68

79

@@ -14,3 +16,64 @@ def apartial(coro, *args, **kwargs):
1416
async def wrapped(*cargs, **ckwargs):
1517
return await coro(*args, *cargs, **kwargs, **ckwargs)
1618
return wrapped
19+
20+
21+
def lru_cache(maxsize=128, typed=False):
22+
'''
23+
A simple LRU cache just like functools.lru_cache, but it works for
24+
coroutines. This is not as heavily optimized as functools.lru_cache
25+
which uses an internal C implementation, as it targets async operations
26+
that take a long time.
27+
28+
It follows the same API that the standard functools provides. The wrapped
29+
function has ``cache_clear()`` method to flush the cache manually, but
30+
leaves ``cache_info()`` for statistics unimplemented.
31+
32+
Note that calling the coroutine multiple times with the same arguments
33+
before the first call returns may incur duplicate exectuions.
34+
35+
This function is not thread-safe.
36+
'''
37+
38+
if maxsize is not None and not isinstance(maxsize, int):
39+
raise TypeError('Expected maxsize to be an integer or None')
40+
41+
def wrapper(coro):
42+
43+
sentinel = object() # unique object to distinguish None as result
44+
cache = collections.OrderedDict()
45+
cache_get = cache.get
46+
cache_set = cache.__setitem__
47+
cache_len = cache.__len__
48+
cache_move = cache.move_to_end
49+
make_key = functools._make_key
50+
51+
# We don't use explicit locks like the standard functools,
52+
# because this lru_cache is intended for use in asyncio coroutines.
53+
# The only context interleaving happens when calling the user-defined
54+
# coroutine, so there is no need to add extra synchronization guards.
55+
56+
@functools.wraps(coro)
57+
async def wrapped(*args, **kwargs):
58+
k = make_key(args, kwargs, typed)
59+
result = cache_get(k, sentinel)
60+
if result is not sentinel:
61+
return result
62+
result = await coro(*args, **kwargs)
63+
if maxsize is not None and cache_len() >= maxsize:
64+
cache.popitem(last=False)
65+
cache_set(k, result)
66+
cache_move(k, last=True)
67+
return result
68+
69+
def cache_clear():
70+
cache.clear()
71+
72+
def cache_info():
73+
raise NotImplementedError
74+
75+
wrapped.cache_clear = cache_clear
76+
wrapped.cache_info = cache_info
77+
return wrapped
78+
79+
return wrapper

tests/test_func.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import asyncio
2+
13
import pytest
24

3-
from aiotools.func import apartial
5+
from aiotools.func import apartial, lru_cache
46

57

68
async def do(a, b, *, c=1, d=2):
@@ -43,3 +45,43 @@ async def test_apartial_wraps():
4345
assert do2.__doc__ == do.__doc__
4446
assert do.__name__ == 'do'
4547
assert do2.__name__ == 'do'
48+
49+
50+
@pytest.mark.asyncio
51+
async def test_lru_cache():
52+
53+
calc_count = 0
54+
55+
@lru_cache(maxsize=2)
56+
async def calc(n):
57+
'''testing'''
58+
nonlocal calc_count
59+
await asyncio.sleep(0)
60+
calc_count += 1
61+
return n * n
62+
63+
assert calc.__name__ == 'calc'
64+
assert calc.__doc__ == 'testing'
65+
66+
assert (await calc(1)) == 1
67+
assert calc_count == 1
68+
assert (await calc(2)) == 4
69+
assert calc_count == 2
70+
assert (await calc(1)) == 1
71+
assert calc_count == 2
72+
assert (await calc(3)) == 9
73+
assert calc_count == 3
74+
assert (await calc(1)) == 1 # evicted and re-executed
75+
assert calc_count == 4
76+
assert (await calc(1)) == 1 # cached again
77+
assert calc_count == 4
78+
79+
with pytest.raises(NotImplementedError):
80+
calc.cache_info()
81+
82+
calc.cache_clear()
83+
84+
assert (await calc(1)) == 1
85+
assert calc_count == 5
86+
assert (await calc(3)) == 9
87+
assert calc_count == 6

0 commit comments

Comments
 (0)