Skip to content

Commit 94f29e8

Browse files
committed
feat(cache): add exclude arguments feature for cache key generation
- Introduce `exclude_positional_args` and `exclude_keyword_args` parameters to the `exec`, `aexec`, and decorator methods - Implement logic to filter out specified positional and keyword arguments before cache key generation - Update the `_before_get` method to handle the new exclude parameters - Add unit tests to verify the functionality of excluding arguments
1 parent 0bf0428 commit 94f29e8

File tree

2 files changed

+66
-21
lines changed

2 files changed

+66
-21
lines changed

src/redis_func_cache/cache.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -441,10 +441,19 @@ async def aput(
441441
ext_args = ext_args or ()
442442
await script(keys=key_pair, args=chain((maxsize, ttl, hash_, value, encoded_options), ext_args))
443443

444-
def _before_get(self, user_function, user_args, user_kwds):
445-
keys = self.policy.calc_keys(user_function, user_args, user_kwds)
446-
hash_value = self.policy.calc_hash(user_function, user_args, user_kwds)
447-
ext_args = self.policy.calc_ext_args(user_function, user_args, user_kwds) or ()
444+
def _before_get(
445+
self,
446+
user_function,
447+
user_args,
448+
user_kwds,
449+
exclude_positional_args: Optional[Sequence[int]] = None,
450+
exclude_keyword_args: Optional[Sequence[str]] = None,
451+
):
452+
args = [x for i, x in enumerate(user_args) if i not in (exclude_positional_args or [])]
453+
kwds = {k: v for k, v in user_kwds.items() if k not in (exclude_keyword_args or {})}
454+
keys = self.policy.calc_keys(user_function, args, kwds)
455+
hash_value = self.policy.calc_hash(user_function, args, kwds)
456+
ext_args = self.policy.calc_ext_args(user_function, args, kwds) or ()
448457
return keys, hash_value, ext_args
449458

450459
def exec(
@@ -454,6 +463,8 @@ def exec(
454463
user_kwds: Mapping[str, Any],
455464
serialize_func: Optional[SerializerT] = None,
456465
deserialize_func: Optional[DeserializerT] = None,
466+
exclude_positional_args: Optional[Sequence[int]] = None,
467+
exclude_keyword_args: Optional[Sequence[str]] = None,
457468
**options,
458469
):
459470
"""Execute the given user function with the provided arguments.
@@ -476,7 +487,9 @@ def exec(
476487
script_0, script_1 = self.policy.lua_scripts
477488
if not (isinstance(script_0, redis.commands.core.Script) and isinstance(script_1, redis.commands.core.Script)):
478489
raise RuntimeError("Can not eval redis lua script in asynchronous mode on a synchronous redis client")
479-
keys, hash_value, ext_args = self._before_get(user_function, user_args, user_kwds)
490+
keys, hash_value, ext_args = self._before_get(
491+
user_function, user_args, user_kwds, exclude_positional_args, exclude_keyword_args
492+
)
480493
cached_return_value = self.get(script_0, keys, hash_value, self.ttl, options, ext_args)
481494
if cached_return_value is not None:
482495
return self.deserialize(cached_return_value, deserialize_func)
@@ -492,6 +505,8 @@ async def aexec(
492505
user_kwds: Mapping[str, Any],
493506
serialize_func: Optional[SerializerT] = None,
494507
deserialize_func: Optional[DeserializerT] = None,
508+
exclude_positional_args: Optional[Sequence[int]] = None,
509+
exclude_keyword_args: Optional[Sequence[str]] = None,
495510
**options,
496511
):
497512
"""Asynchronous version of :meth:`.exec`"""
@@ -501,7 +516,9 @@ async def aexec(
501516
and isinstance(script_1, redis.commands.core.AsyncScript)
502517
):
503518
raise RuntimeError("Can not eval redis lua script in synchronous mode on an asynchronous redis client")
504-
keys, hash_value, ext_args = self._before_get(user_function, user_args, user_kwds)
519+
keys, hash_value, ext_args = self._before_get(
520+
user_function, user_args, user_kwds, exclude_positional_args, exclude_keyword_args
521+
)
505522
cached = await self.aget(script_0, keys, hash_value, self.ttl, options, ext_args)
506523
if cached is not None:
507524
return self.deserialize(cached, deserialize_func)
@@ -515,8 +532,8 @@ def decorate(
515532
user_function: Optional[CallableTV] = None,
516533
/,
517534
serializer: Optional[SerializerSetterValueT] = None,
518-
ignore_positional_args: Optional[Sequence[int]] = None,
519-
ignore_keyword_args: Optional[Sequence[str]] = None,
535+
exclude_positional_args: Optional[Sequence[int]] = None,
536+
exclude_keyword_args: Optional[Sequence[str]] = None,
520537
**options,
521538
) -> CallableTV:
522539
"""Decorate the given function with caching.
@@ -530,14 +547,13 @@ def decorate(
530547
- A string key mapping to predefined serializers (like "yaml" or "json")
531548
- A tuple of (`serialize_func`, `deserialize_func`) functions
532549
533-
If defined, it overrides the :attr:`serializer` property of the cache instance.
534-
550+
If assigned, it overwrite the :attr:`serializer` property of the cache instance on the decorated function.
535551
536-
ignore_positional_args: A list of positional argument indices to exclude from cache key generation.
552+
exclude_positional_args: A list of positional argument indices to exclude from cache key generation.
537553
538554
These arguments will be filtered out before cache operations.
539555
540-
ignore_keyword_args: A list of keyword argument names to exclude from cache key generation.
556+
exclude_keyword_args: A list of keyword argument names to exclude from cache key generation.
541557
542558
These parameters will be filtered out before cache operations.
543559
@@ -600,31 +616,30 @@ def my_func(a, b):
600616
elif serializer is not None:
601617
serialize_func, deserialize_func = serializer
602618

603-
if ignore_positional_args is None:
604-
ignore_positional_args = []
605-
if ignore_keyword_args is None:
606-
ignore_keyword_args = []
607-
608619
def decorator(user_func: CallableTV):
609620
@wraps(user_func)
610621
def wrapper(*user_args, **user_kwargs):
611622
return self.exec(
612623
user_func,
613-
[x for i, x in enumerate(user_args) if i not in ignore_positional_args],
614-
{k: v for k, v in user_kwargs.items() if k not in ignore_keyword_args},
624+
user_args,
625+
user_kwargs,
615626
serialize_func,
616627
deserialize_func,
628+
exclude_positional_args,
629+
exclude_keyword_args,
617630
**options,
618631
)
619632

620633
@wraps(user_func)
621634
async def awrapper(*user_args, **user_kwargs):
622635
return await self.aexec(
623636
user_func,
624-
[x for i, x in enumerate(user_args) if i not in ignore_positional_args],
625-
{k: v for k, v in user_kwargs.items() if k not in ignore_keyword_args},
637+
user_args,
638+
user_kwargs,
626639
serialize_func,
627640
deserialize_func,
641+
exclude_positional_args,
642+
exclude_keyword_args,
628643
**options,
629644
)
630645

tests/test_basic.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,33 @@ def test_lambda(self):
356356
for _ in range(cache.maxsize * 2 + 1):
357357
v = uuid4().hex
358358
self.assertEqual(v, f(v))
359+
360+
361+
class ExcludeArgsTestCase(TestCase):
362+
def setUp(self):
363+
for cache in CACHES.values():
364+
cache.policy.purge()
365+
366+
def test_exclude_positional_args(self):
367+
def user_func(func, value):
368+
return func(value)
369+
370+
unpickable_func = lambda x: x # noqa: E731
371+
372+
for cache in CACHES.values():
373+
f = cache(user_func, exclude_positional_args=[0])
374+
for _ in range(cache.maxsize * 2 + 1):
375+
v = uuid4().hex
376+
self.assertEqual(f(unpickable_func, v), v)
377+
378+
def test_exclude_keyword_args(self):
379+
def user_func(func, value):
380+
return func(value)
381+
382+
unpickable_func = lambda x: x # noqa: E731
383+
384+
for cache in CACHES.values():
385+
f = cache(user_func, exclude_keyword_args=["func"])
386+
for _ in range(cache.maxsize * 2 + 1):
387+
v = uuid4().hex
388+
self.assertEqual(f(func=unpickable_func, value=v), v)

0 commit comments

Comments
 (0)