Skip to content

Commit 7c80a0f

Browse files
VitalyPetrovVitaly PetrovDreamsorcerer
authored
Introduce skip_cache_func (#652)
Co-authored-by: Vitaly Petrov <[email protected]> Co-authored-by: Sam Bull <[email protected]>
1 parent f9ec685 commit 7c80a0f

File tree

2 files changed

+66
-2
lines changed

2 files changed

+66
-2
lines changed

aiocache/decorators.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ class cached:
3939
:param key_builder: Callable that allows to build the function dynamically. It receives
4040
the function plus same args and kwargs passed to the function.
4141
This behavior is necessarily different than ``BaseCache.build_key()``
42+
:param skip_cache_func: Callable that receives the result after calling the
43+
wrapped function and should return `True` if the value should skip the
44+
cache (or `False` to store in the cache).
45+
e.g. to avoid caching `None` results: `lambda r: r is None`
4246
:param cache: cache class to use when calling the ``set``/``get`` operations.
4347
Default is :class:`aiocache.SimpleMemoryCache`.
4448
:param serializer: serializer instance to use when calling the ``dumps``/``loads``.
@@ -58,6 +62,7 @@ def __init__(
5862
ttl=SENTINEL,
5963
namespace=None,
6064
key_builder=None,
65+
skip_cache_func=lambda x: False,
6166
cache=Cache.MEMORY,
6267
serializer=None,
6368
plugins=None,
@@ -67,6 +72,7 @@ def __init__(
6772
):
6873
self.ttl = ttl
6974
self.key_builder = key_builder
75+
self.skip_cache_func = skip_cache_func
7076
self.noself = noself
7177
self.alias = alias
7278
self.cache = None
@@ -111,6 +117,9 @@ async def decorator(
111117

112118
result = await f(*args, **kwargs)
113119

120+
if self.skip_cache_func(result):
121+
return result
122+
114123
if cache_write:
115124
if aiocache_wait_for_write:
116125
await self.set_in_cache(key, result)
@@ -171,6 +180,10 @@ class cached_stampede(cached):
171180
:param key_builder: Callable that allows to build the function dynamically. It receives
172181
the function plus same args and kwargs passed to the function.
173182
This behavior is necessarily different than ``BaseCache.build_key()``
183+
:param skip_cache_func: Callable that receives the result after calling the
184+
wrapped function and should return `True` if the value should skip the
185+
cache (or `False` to store in the cache).
186+
e.g. to avoid caching `None` results: `lambda r: r is None`
174187
:param cache: cache class to use when calling the ``set``/``get`` operations.
175188
Default is :class:`aiocache.SimpleMemoryCache`.
176189
:param serializer: serializer instance to use when calling the ``dumps``/``loads``.
@@ -202,6 +215,9 @@ async def decorator(self, f, *args, **kwargs):
202215

203216
result = await f(*args, **kwargs)
204217

218+
if self.skip_cache_func(result):
219+
return result
220+
205221
await self.set_in_cache(key, result)
206222

207223
return result
@@ -268,6 +284,9 @@ class multi_cached:
268284
``keys_from_attr``, the decorated callable, and the positional and keyword arguments
269285
that were passed to the decorated callable. This behavior is necessarily different than
270286
``BaseCache.build_key()`` and the call signature differs from ``cached.key_builder``.
287+
:param skip_cache_keys: Callable that receives both key and value and returns True
288+
if that key-value pair should not be cached (or False to store in cache).
289+
The keys and values to be passed are taken from the wrapped function result.
271290
:param ttl: int seconds to store the keys. Default is 0 which means no expiration.
272291
:param cache: cache class to use when calling the ``multi_set``/``multi_get`` operations.
273292
Default is :class:`aiocache.SimpleMemoryCache`.
@@ -286,6 +305,7 @@ def __init__(
286305
keys_from_attr,
287306
namespace=None,
288307
key_builder=None,
308+
skip_cache_func=lambda k, v: False,
289309
ttl=SENTINEL,
290310
cache=Cache.MEMORY,
291311
serializer=None,
@@ -295,6 +315,7 @@ def __init__(
295315
):
296316
self.keys_from_attr = keys_from_attr
297317
self.key_builder = key_builder or (lambda key, f, *args, **kwargs: key)
318+
self.skip_cache_func = skip_cache_func
298319
self.ttl = ttl
299320
self.alias = alias
300321
self.cache = None
@@ -354,12 +375,17 @@ async def decorator(
354375
result = await f(*new_args, **kwargs)
355376
result.update(partial)
356377

378+
to_cache = {k: v for k, v in result.items() if not self.skip_cache_func(k, v)}
379+
380+
if not to_cache:
381+
return result
382+
357383
if cache_write:
358384
if aiocache_wait_for_write:
359-
await self.set_in_cache(result, f, args, kwargs)
385+
await self.set_in_cache(to_cache, f, args, kwargs)
360386
else:
361387
# TODO: Use aiojobs to avoid warnings.
362-
asyncio.create_task(self.set_in_cache(result, f, args, kwargs))
388+
asyncio.create_task(self.set_in_cache(to_cache, f, args, kwargs))
363389

364390
return result
365391

tests/acceptance/test_decorators.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,31 @@ async def fn(self, a, b=2):
4949
await fn("self", 1, 3)
5050
assert await cache.exists(build_key(fn, "self", 1, 3)) is True
5151

52+
@pytest.mark.parametrize("decorator", (cached, cached_stampede))
53+
async def test_cached_skip_cache_func(self, cache, decorator):
54+
@decorator(skip_cache_func=lambda r: r is None)
55+
async def sk_func(x):
56+
return x if x > 0 else None
57+
58+
arg = 1
59+
res = await sk_func(arg)
60+
assert res
61+
62+
key = decorator().get_cache_key(sk_func, args=(1,), kwargs={})
63+
64+
assert key
65+
assert await cache.exists(key)
66+
assert await cache.get(key) == res
67+
68+
arg = -1
69+
70+
await sk_func(arg)
71+
72+
key = decorator().get_cache_key(sk_func, args=(-1,), kwargs={})
73+
74+
assert key
75+
assert not await cache.exists(key)
76+
5277
async def test_cached_without_namespace(self, cache):
5378
"""Default cache key is created when no namespace is provided"""
5479
@cached(namespace=None)
@@ -149,6 +174,19 @@ async def fn(self, keys, market="ES"):
149174
assert await cache.exists("fn_" + _ensure_key(Keys.KEY) + "_ES") is True
150175
assert await cache.exists("fn_" + _ensure_key(Keys.KEY_1) + "_ES") is True
151176

177+
async def test_multi_cached_skip_keys(self, cache):
178+
@multi_cached(keys_from_attr="keys", skip_cache_func=lambda _, v: v is None)
179+
async def multi_sk_fn(keys, values):
180+
return {k: v for k, v in zip(keys, values)}
181+
182+
res = await multi_sk_fn(keys=[Keys.KEY, Keys.KEY_1], values=[42, None])
183+
assert res
184+
assert Keys.KEY in res and Keys.KEY_1 in res
185+
186+
assert await cache.exists(Keys.KEY)
187+
assert await cache.get(Keys.KEY) == res[Keys.KEY]
188+
assert not await cache.exists(Keys.KEY_1)
189+
152190
async def test_fn_with_args(self, cache):
153191
@multi_cached("keys")
154192
async def fn(keys, *args):

0 commit comments

Comments
 (0)