diff --git a/src/cachier/config.py b/src/cachier/config.py index 6b1652e4..7b5ad157 100644 --- a/src/cachier/config.py +++ b/src/cachier/config.py @@ -67,6 +67,7 @@ class Params: cleanup_interval: timedelta = timedelta(days=1) entry_size_limit: Optional[int] = None allow_non_static_methods: bool = False + key_prefix: str = "cachier" _global_params = Params() diff --git a/src/cachier/core.py b/src/cachier/core.py index 5a4bc4aa..d7b00233 100644 --- a/src/cachier/core.py +++ b/src/cachier/core.py @@ -226,6 +226,7 @@ def cachier( mongetter: Optional[Mongetter] = None, sql_engine: Optional[Union[str, Any, Callable[[], Any]]] = None, redis_client: Optional["RedisClient"] = None, + key_prefix: Optional[str] = None, s3_bucket: Optional[str] = None, s3_prefix: str = "cachier", s3_client: Optional["S3Client"] = None, @@ -280,6 +281,8 @@ def cachier( redis_client : redis.Redis or callable, optional Redis client instance or callable returning a Redis client. Used for the Redis backend. + key_prefix : str, optional + Key prefix applied to all redis keys. Defaults to ``"cachier"``. s3_bucket : str, optional The S3 bucket name for cache storage. Required when using the S3 backend. s3_prefix : str, optional @@ -357,6 +360,7 @@ def cachier( backend = _update_with_defaults(backend, "backend") mongetter = _update_with_defaults(mongetter, "mongetter") size_limit_bytes = parse_bytes(_update_with_defaults(entry_size_limit, "entry_size_limit")) + key_prefix = _update_with_defaults(key_prefix, "key_prefix") # Create metrics object if enabled cache_metrics = None @@ -407,6 +411,7 @@ def cachier( wait_for_calc_timeout=wait_for_calc_timeout, entry_size_limit=size_limit_bytes, metrics=cache_metrics, + key_prefix=key_prefix, ) elif backend == "s3": core = _S3Core( diff --git a/tests/redis_tests/test_async_redis_core.py b/tests/redis_tests/test_async_redis_core.py index e5dec8f7..46f12559 100644 --- a/tests/redis_tests/test_async_redis_core.py +++ b/tests/redis_tests/test_async_redis_core.py @@ -109,6 +109,36 @@ def _func(x: int) -> int: return core +@pytest.mark.redis +@pytest.mark.asyncio +async def test_async_redis_key_prefix_passed_to_client(): + pytest.importorskip("redis") + + class PrefixCapturingRedis(_AsyncInMemoryRedis): + def __init__(self): + super().__init__() + self.keys_used: list[str] = [] + + async def hset(self, key: str, field=None, value=None, mapping=None, **kwargs): + self.keys_used.append(key) + await super().hset(key, field=field, value=value, mapping=mapping, **kwargs) + + client = PrefixCapturingRedis() + + async def get_redis_client(): + return client + + @cachier(backend="redis", redis_client=get_redis_client, key_prefix="custom-prefix") + async def async_cached_value(x: int) -> int: + return x + 1 + + result = await async_cached_value(1) + assert result == 2 + assert client.keys_used, "Redis client was not called" + assert all(key.startswith("custom-prefix:") for key in client.keys_used) + assert all(stored_key.startswith("custom-prefix:") for stored_key in client._data) + + @pytest.mark.redis @pytest.mark.asyncio async def test_async_redis_core_helpers_and_client_resolution(): diff --git a/tests/redis_tests/test_redis_core.py b/tests/redis_tests/test_redis_core.py index 6b2d9f39..e8cbace3 100644 --- a/tests/redis_tests/test_redis_core.py +++ b/tests/redis_tests/test_redis_core.py @@ -4,6 +4,7 @@ import queue import threading import warnings +from dataclasses import replace from datetime import datetime, timedelta from random import random from time import sleep @@ -12,7 +13,8 @@ import pandas as pd import pytest -from cachier import cachier +import cachier.config as cachier_config +from cachier import cachier, set_global_params from cachier.core import _is_async_redis_client from cachier.cores.redis import MissingRedisClient, _RedisCore from tests.redis_tests.clients import _SyncInMemoryRedis @@ -130,6 +132,43 @@ def _test_redis_caching(arg_1, arg_2): assert val6 == val5 +@pytest.mark.redis +def test_redis_key_prefix_uses_global_default_and_decorator_override(): + _copied_defaults = replace(cachier_config.get_global_params()) + + set_global_params(key_prefix="global-prefix") + + class PrefixCapturingRedis(_SyncInMemoryRedis): + def __init__(self): + super().__init__() + self.keys_used: list[str] = [] + + def hset(self, key: str, field=None, value=None, mapping=None, **kwargs): + self.keys_used.append(key) + return super().hset(key, field=field, value=value, mapping=mapping, **kwargs) + + global_client = PrefixCapturingRedis() + explicit_client = PrefixCapturingRedis() + + @cachier(backend="redis", redis_client=global_client) + def cached_with_global_prefix(x: int) -> int: + return x + 1 + + @cachier(backend="redis", redis_client=explicit_client, key_prefix="explicit-prefix") + def cached_with_explicit_prefix(x: int) -> int: + return x + 1 + + assert cached_with_global_prefix(1) == 2 + assert cached_with_explicit_prefix(1) == 2 + + cachier_config.set_global_params(**vars(_copied_defaults)) + + assert global_client.keys_used, "Redis client was not called" + assert explicit_client.keys_used, "Redis client was not called" + assert all(key.startswith("global-prefix:") for key in global_client.keys_used) + assert all(key.startswith("explicit-prefix:") for key in explicit_client.keys_used) + + @pytest.mark.redis def test_redis_stale_after(): """Testing Redis core stale_after functionality.""" diff --git a/tests/test_core_lookup.py b/tests/test_core_lookup.py index 599aa194..96a6ce2b 100644 --- a/tests/test_core_lookup.py +++ b/tests/test_core_lookup.py @@ -17,6 +17,7 @@ def test_get_default_params(): "cleanup_stale", "entry_size_limit", "hash_func", + "key_prefix", "mongetter", "next_time", "pickle_reload",