diff --git a/src/cachier/config.py b/src/cachier/config.py index 6b1652e4..a250d813 100644 --- a/src/cachier/config.py +++ b/src/cachier/config.py @@ -67,6 +67,8 @@ class Params: cleanup_interval: timedelta = timedelta(days=1) entry_size_limit: Optional[int] = None allow_non_static_methods: bool = False + key_prefix: str = "cachier" + func_prefix: str = "." _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/src/cachier/cores/base.py b/src/cachier/cores/base.py index a62a0d28..64bb3d8c 100644 --- a/src/cachier/cores/base.py +++ b/src/cachier/cores/base.py @@ -37,7 +37,8 @@ def _get_func_str(func: Callable) -> str: __name__, but at runtime the decorated functions always do. """ - return f".{func.__module__}.{func.__name__}" + func_prefix = _update_with_defaults(None, "func_prefix") + return f"{func_prefix}{func.__module__}.{func.__name__}" class _BaseCore(metaclass=abc.ABCMeta): 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..e7a41101 100644 --- a/tests/redis_tests/test_redis_core.py +++ b/tests/redis_tests/test_redis_core.py @@ -130,6 +130,31 @@ def _test_redis_caching(arg_1, arg_2): assert val6 == val5 +@pytest.mark.redis +def test_redis_key_prefix_passed_to_client(): + pytest.importorskip("redis") + + 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) + + client = PrefixCapturingRedis() + + @cachier(backend="redis", redis_client=client, key_prefix="custom-prefix") + def cached_value(x: int) -> int: + return x + 1 + + result = 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) + + @pytest.mark.redis def test_redis_stale_after(): """Testing Redis core stale_after functionality.""" diff --git a/tests/test_base_core.py b/tests/test_base_core.py index 589eac66..37f41acd 100644 --- a/tests/test_base_core.py +++ b/tests/test_base_core.py @@ -5,7 +5,8 @@ import pytest -from cachier.cores.base import RecalculationNeeded, _BaseCore +from cachier.config import get_global_params, set_global_params +from cachier.cores.base import RecalculationNeeded, _BaseCore, _get_func_str class ConcreteCachingCore(_BaseCore): @@ -82,6 +83,21 @@ def test_should_store_exception(): assert core._should_store("test_value") is True +def test_get_func_str_honors_global_func_prefix(): + """Test _get_func_str prepends the configured func_prefix.""" + original_prefix = get_global_params().func_prefix + try: + set_global_params(func_prefix="custom-prefix-") + + def dummy_function(): + return None + + expected = f"custom-prefix-{dummy_function.__module__}.{dummy_function.__name__}" + assert _get_func_str(dummy_function) == expected + finally: + set_global_params(func_prefix=original_prefix) + + @pytest.mark.asyncio async def test_base_core_async_default_wrappers(): """Test async default wrappers delegate correctly to sync methods.""" diff --git a/tests/test_core_lookup.py b/tests/test_core_lookup.py index 599aa194..7b7e582d 100644 --- a/tests/test_core_lookup.py +++ b/tests/test_core_lookup.py @@ -16,7 +16,9 @@ def test_get_default_params(): "cleanup_interval", "cleanup_stale", "entry_size_limit", + "func_prefix", "hash_func", + "key_prefix", "mongetter", "next_time", "pickle_reload",