From 7df11d0fef77e803c11236e2fbcc5ae3828f139e Mon Sep 17 00:00:00 2001 From: Matteo Morlacchi Date: Wed, 8 Jul 2026 00:14:09 +0200 Subject: [PATCH 1/6] feat: add global key prefixes --- src/cachier/config.py | 2 ++ src/cachier/core.py | 3 +++ src/cachier/cores/base.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) 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..698006e2 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["RedisClient"] = None, s3_bucket: Optional[str] = None, s3_prefix: str = "cachier", s3_client: Optional["S3Client"] = None, @@ -357,6 +358,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 +409,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..599275ff 100644 --- a/src/cachier/cores/base.py +++ b/src/cachier/cores/base.py @@ -37,7 +37,7 @@ def _get_func_str(func: Callable) -> str: __name__, but at runtime the decorated functions always do. """ - return f".{func.__module__}.{func.__name__}" + return f"{_update_with_defaults('.', 'func_prefix')}{func.__module__}.{func.__name__}" class _BaseCore(metaclass=abc.ABCMeta): From 24805dac02c9550a64c44a49144939c4fe8c090e Mon Sep 17 00:00:00 2001 From: Matteo Morlacchi Date: Wed, 8 Jul 2026 00:35:34 +0200 Subject: [PATCH 2/6] feat: add global key prefixes --- tests/test_core_lookup.py | 2 ++ 1 file changed, 2 insertions(+) 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", From a1fccd008ceae46ae6879233ae2b6d2da567d5dd Mon Sep 17 00:00:00 2001 From: Matteo Morlacchi Date: Wed, 8 Jul 2026 00:50:03 +0200 Subject: [PATCH 3/6] fix: annotation --- src/cachier/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cachier/core.py b/src/cachier/core.py index 698006e2..48f2a6ae 100644 --- a/src/cachier/core.py +++ b/src/cachier/core.py @@ -226,7 +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["RedisClient"] = None, + key_prefix: Optional[str] = None, s3_bucket: Optional[str] = None, s3_prefix: str = "cachier", s3_client: Optional["S3Client"] = None, From 6cd8e5deef0261dd1ce5cdea6a316f738855ad61 Mon Sep 17 00:00:00 2001 From: Matteo Morlacchi Date: Fri, 10 Jul 2026 22:34:32 +0200 Subject: [PATCH 4/6] feat: add documentation --- src/cachier/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cachier/core.py b/src/cachier/core.py index 48f2a6ae..d7b00233 100644 --- a/src/cachier/core.py +++ b/src/cachier/core.py @@ -281,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 From be1faaf8ba5d9826a8e318f7d7af896aaead0253 Mon Sep 17 00:00:00 2001 From: Matteo Morlacchi Date: Fri, 10 Jul 2026 22:36:00 +0200 Subject: [PATCH 5/6] feat: make global func prefix work with defaults --- src/cachier/cores/base.py | 3 ++- tests/test_base_core.py | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/cachier/cores/base.py b/src/cachier/cores/base.py index 599275ff..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"{_update_with_defaults('.', 'func_prefix')}{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/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.""" From 11083564c6e34eb924d1124a7fb677e867355795 Mon Sep 17 00:00:00 2001 From: Matteo Morlacchi Date: Fri, 10 Jul 2026 22:36:11 +0200 Subject: [PATCH 6/6] feat: add tests for redis --- tests/redis_tests/test_async_redis_core.py | 30 ++++++++++++++++++++++ tests/redis_tests/test_redis_core.py | 25 ++++++++++++++++++ 2 files changed, 55 insertions(+) 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."""