Skip to content

Commit 08f54ed

Browse files
feat: add cache_contains method to check cache presence without affecting counters
1 parent e2ddf7a commit 08f54ed

2 files changed

Lines changed: 139 additions & 0 deletions

File tree

async_lru/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ def _check_loop(self, loop: asyncio.AbstractEventLoop) -> None:
150150
self.cache_clear()
151151
self.__first_loop = loop
152152

153+
def cache_contains(self, /, *args: Hashable, **kwargs: Any) -> bool:
154+
"""Check if the given arguments are in the cache.
155+
156+
Does not affect hit/miss counters or LRU ordering.
157+
"""
158+
key = _make_key(args, kwargs, self.__typed)
159+
return key in self.__cache
160+
153161
def cache_invalidate(self, /, *args: Hashable, **kwargs: Any) -> bool:
154162
key = _make_key(args, kwargs, self.__typed)
155163

@@ -326,6 +334,9 @@ def __init__(
326334
self.__instance = instance
327335
self.__wrapper = wrapper
328336

337+
def cache_contains(self, /, *args: Hashable, **kwargs: Any) -> bool:
338+
return self.__wrapper.cache_contains(self.__instance, *args, **kwargs)
339+
329340
def cache_invalidate(self, /, *args: Hashable, **kwargs: Any) -> bool:
330341
return self.__wrapper.cache_invalidate(self.__instance, *args, **kwargs)
331342

tests/test_cache_contains.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import asyncio
2+
from async_lru import alru_cache
3+
4+
5+
async def test_cache_contains_basic(check_lru):
6+
@alru_cache(maxsize=4)
7+
async def coro(val):
8+
return val
9+
10+
assert coro.cache_contains(1) is False
11+
await coro(1)
12+
assert coro.cache_contains(1) is True
13+
assert coro.cache_contains(2) is False
14+
check_lru(coro, hits=0, misses=1, cache=1, tasks=0, maxsize=4)
15+
16+
17+
async def test_cache_contains_does_not_affect_counters(check_lru):
18+
@alru_cache(maxsize=4)
19+
async def coro(val):
20+
return val
21+
22+
await coro(1)
23+
for _ in range(10):
24+
coro.cache_contains(1)
25+
coro.cache_contains(99)
26+
27+
# hits/misses must stay unchanged after cache_contains calls
28+
check_lru(coro, hits=0, misses=1, cache=1, tasks=0, maxsize=4)
29+
30+
31+
async def test_cache_contains_does_not_change_lru_order():
32+
@alru_cache(maxsize=2)
33+
async def coro(val):
34+
return val
35+
36+
await coro(1)
37+
await coro(2)
38+
39+
# Peek at key 1 without refreshing its LRU position
40+
assert coro.cache_contains(1) is True
41+
42+
# Adding a third entry must evict key 1 (oldest), not key 2
43+
await coro(3)
44+
45+
assert coro.cache_contains(1) is False
46+
assert coro.cache_contains(2) is True
47+
assert coro.cache_contains(3) is True
48+
49+
50+
async def test_cache_contains_after_invalidate_and_clear():
51+
@alru_cache(maxsize=4)
52+
async def coro(val):
53+
return val
54+
55+
await coro(1)
56+
await coro(2)
57+
58+
coro.cache_invalidate(1)
59+
assert coro.cache_contains(1) is False
60+
assert coro.cache_contains(2) is True # unaffected
61+
62+
coro.cache_clear()
63+
assert coro.cache_contains(2) is False
64+
65+
66+
async def test_cache_contains_with_kwargs():
67+
@alru_cache(maxsize=4)
68+
async def coro(a, b=10):
69+
return a + b
70+
71+
await coro(1, b=20)
72+
assert coro.cache_contains(1, b=20) is True
73+
assert coro.cache_contains(1, b=30) is False
74+
assert coro.cache_contains(1) is False
75+
76+
77+
async def test_cache_contains_respects_typed_flag():
78+
79+
@alru_cache(maxsize=4, typed=True)
80+
async def coro(val):
81+
return val
82+
83+
await coro(1)
84+
assert coro.cache_contains(1) is True
85+
assert coro.cache_contains(1.0) is False
86+
87+
88+
async def test_cache_contains_pending_task():
89+
event = asyncio.Event()
90+
91+
@alru_cache(maxsize=4)
92+
async def coro(val):
93+
await event.wait()
94+
return val
95+
96+
task = asyncio.ensure_future(coro(1))
97+
await asyncio.sleep(0)
98+
99+
# Key must be present even while the underlying task is still running
100+
assert coro.cache_contains(1) is True
101+
102+
event.set()
103+
await task
104+
105+
106+
async def test_cache_contains_after_ttl_expiry():
107+
@alru_cache(maxsize=4, ttl=0.05)
108+
async def coro(val):
109+
return val
110+
111+
await coro(1)
112+
assert coro.cache_contains(1) is True
113+
114+
await asyncio.sleep(0.1)
115+
assert coro.cache_contains(1) is False
116+
117+
118+
async def test_cache_contains_on_method():
119+
class MyService:
120+
@alru_cache(maxsize=4)
121+
async def fetch(self, key):
122+
return key * 2
123+
124+
svc = MyService()
125+
await svc.fetch(5)
126+
127+
assert svc.fetch.cache_contains(5) is True
128+
assert svc.fetch.cache_contains(6) is False

0 commit comments

Comments
 (0)