Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decorator namespace key #627

Merged
merged 49 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
d1290cb
test cached decorator with namespace
padraic-shafer Jan 3, 2023
8afb9e4
Use namespace in 'cached' decorator
padraic-shafer Jan 4, 2023
c0e0cc0
Clean-up for flake8
padraic-shafer Jan 4, 2023
e8b5b4c
Simplify namespace and tests for cached decorator
padraic-shafer Jan 4, 2023
c52fc61
Remove excess whitespace from tests
padraic-shafer Jan 4, 2023
eff89ac
Add whitespace around operators
padraic-shafer Jan 4, 2023
3d9abb1
Simplify tests for cached decorator with namespace
padraic-shafer Jan 4, 2023
bacfef3
Use namespace with cache.exists in cached decorator tests
padraic-shafer Jan 4, 2023
0e9e546
exists(
padraic-shafer Jan 4, 2023
0031377
Use cache namespace from fixture in tests
padraic-shafer Jan 4, 2023
9e31edc
Merge branch 'test-namespace-key' into decorator-namespace-key
padraic-shafer Jan 4, 2023
b8e7d9f
cached decorator uses namespace param
padraic-shafer Jan 4, 2023
cb0d527
Mocker cache.get/set call signature updated with namespace
padraic-shafer Jan 4, 2023
54e0142
Fixed indentation for flake8
padraic-shafer Jan 4, 2023
4002562
Update RedLock to use namespace
padraic-shafer Jan 4, 2023
f6855cf
multi_cached decorator uses namespace
padraic-shafer Jan 4, 2023
249a856
Update tests.ut.mock_cache and decorator tests to use namespace
padraic-shafer Jan 4, 2023
204183c
Namespace is implicit in get/set call signatures
padraic-shafer Jan 6, 2023
5c5a946
test for namespace as initialized member of decorators
padraic-shafer Jan 6, 2023
d1c2f47
Test with namespace='test' consistently
padraic-shafer Jan 6, 2023
861c9b5
Merge branch 'test-namespace-key' into decorator-namespace-key
padraic-shafer Jan 6, 2023
5d2bba0
Use decorator object in tests of decorator with namespace
padraic-shafer Jan 8, 2023
5d9e913
Merge branch 'test-namespace-key' into decorator-namespace-key
padraic-shafer Jan 8, 2023
46f539c
Test custom key_builder for cache with namespace unspecified
padraic-shafer Jan 8, 2023
d1b9cc4
Add examples of key_builder usage
padraic-shafer Jan 8, 2023
dbff91d
Merge branch 'test-namespace-key' into decorator-namespace-key
padraic-shafer Jan 9, 2023
711e2b5
Cache uses self.namespace to build key in member functions with unspe…
padraic-shafer Jan 9, 2023
639a026
Add example of custom cache key_builder that uses cache.namespace whe…
padraic-shafer Jan 9, 2023
b95c91a
Add example of decorator with an aliased cache that uses a namespace
padraic-shafer Jan 9, 2023
a7ede81
Test locks with cache having custom key_builder
padraic-shafer Jan 9, 2023
1ad4695
Merge branch 'test-namespace-key' into decorator-namespace-key
padraic-shafer Jan 9, 2023
bac288e
Use public build_key() in locks
padraic-shafer Jan 9, 2023
27486ca
Refactor alt_build_key tests into helper members
padraic-shafer Jan 9, 2023
1642a4a
Ensure _kwargs is populated in decorator init tests
padraic-shafer Jan 9, 2023
b7b50c3
Merge branch 'test-namespace-key' into decorator-namespace-key
padraic-shafer Jan 9, 2023
099f1ce
Warn when decorator parameters are unused because alias takes precedence
padraic-shafer Jan 9, 2023
07fbe05
Update CHANGELOG
padraic-shafer Jan 9, 2023
433b255
Flake8 indentation of module comment in examples/alt_key_builder.py
padraic-shafer Jan 9, 2023
c6d6744
Flake8 indentation of continuation lines
padraic-shafer Jan 9, 2023
4297efb
Flake8 121, 123, 125 resolved
padraic-shafer Jan 9, 2023
f3397a7
Flake8 trailing whitespace in module docstring for examples/alt_key_b…
padraic-shafer Jan 9, 2023
0f86a9e
Apply suggestions from code review of f3397a7
padraic-shafer Jan 10, 2023
9539fef
Revert cached_decorator example (drop aliased cache example)
padraic-shafer Jan 10, 2023
a5b0c04
Acceptance lock tests with custom cache use mocker.patch
padraic-shafer Jan 10, 2023
266a8e3
Move custom cache fixtures to module scope
padraic-shafer Jan 10, 2023
6abc74b
Refactor build_key out of custom cache fixtures
padraic-shafer Jan 10, 2023
23301ee
Pretty formatting for CHANGELOG
padraic-shafer Jan 10, 2023
8f33f9e
Merge branch 'master' into decorator-namespace-key
padraic-shafer Jan 10, 2023
4a57e8b
Warn if initialization arguments are discarded by multi_cached becaus…
padraic-shafer Jan 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions aiocache/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ class cached:
:param key: str value to set as key for the function return. Takes precedence over
key_builder param. If key and key_builder are not passed, it will use module_name
+ function_name + args + kwargs
:param namespace: string to use as default prefix for the key used in all operations of
the backend. Default is None
:param key_builder: Callable that allows to build the function dynamically. It receives
the function plus same args and kwargs passed to the function.
This behavior is necessarily different than ``BaseCache.build_key()``
Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved
:param cache: cache class to use when calling the ``set``/``get`` operations.
Default is :class:`aiocache.SimpleMemoryCache`.
:param serializer: serializer instance to use when calling the ``dumps``/``loads``.
Expand All @@ -58,6 +61,7 @@ def __init__(
self,
ttl=SENTINEL,
key=None,
namespace=None,
key_builder=None,
cache=Cache.MEMORY,
serializer=None,
Expand All @@ -75,6 +79,7 @@ def __init__(

self._cache = cache
self._serializer = serializer
self._namespace = namespace
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
self._plugins = plugins
self._kwargs = kwargs

Expand All @@ -85,6 +90,7 @@ def __call__(self, f):
self.cache = _get_cache(
cache=self._cache,
serializer=self._serializer,
namespace=self._namespace,
plugins=self._plugins,
**self._kwargs,
)
Expand Down Expand Up @@ -134,18 +140,21 @@ def _key_from_args(self, func, args, kwargs):
+ str(ordered_kwargs)
)

async def get_from_cache(self, key: str):
async def get_from_cache(self, key):
namespace = self.cache.namespace
try:
return await self.cache.get(key)
return await self.cache.get(key, namespace=namespace)
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
except Exception:
logger.exception("Couldn't retrieve %s, unexpected error", key)
return None

async def set_in_cache(self, key, value):
namespace = self.cache.namespace
try:
await self.cache.set(key, value, ttl=self.ttl)
await self.cache.set(key, value, namespace=namespace, ttl=self.ttl)
except Exception:
logger.exception("Couldn't set %s in key %s, unexpected error", value, key)
logger.exception(
"Couldn't set %s in key %s, unexpected error", value, key)


class cached_stampede(cached):
Expand All @@ -168,6 +177,11 @@ class cached_stampede(cached):
key_from_attr param. If key and key_from_attr are not passed, it will use module_name
+ function_name + args + kwargs
:param key_from_attr: str arg or kwarg name from the function to use as a key.
:param namespace: string to use as default prefix for the key used in all operations of
the backend. Default is None
:param key_builder: Callable that allows to build the function dynamically. It receives
the function plus same args and kwargs passed to the function.
This behavior is necessarily different than ``BaseCache.build_key()``
:param cache: cache class to use when calling the ``set``/``get`` operations.
Default is :class:`aiocache.SimpleMemoryCache`.
:param serializer: serializer instance to use when calling the ``dumps``/``loads``.
Expand All @@ -192,7 +206,8 @@ async def decorator(self, f, *args, **kwargs):
if value is not None:
return value

async with RedLock(self.cache, key, self.lease):
async with RedLock(
self.cache, key, self.lease, namespace=self._namespace):
value = await self.get_from_cache(key)
if value is not None:
return value
Expand All @@ -205,7 +220,12 @@ async def decorator(self, f, *args, **kwargs):


def _get_cache(cache=Cache.MEMORY, serializer=None, plugins=None, **cache_kwargs):
return Cache(cache, serializer=serializer, plugins=plugins, **cache_kwargs)
return Cache(
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
cache,
serializer=serializer,
plugins=plugins,
**cache_kwargs,
)


def _get_args_dict(func, args, kwargs):
Expand Down Expand Up @@ -250,8 +270,11 @@ class multi_cached:

:param keys_from_attr: arg or kwarg name from the function containing an iterable to use
as keys to index in the cache.
:param key_builder: Callable that allows to change the format of the keys before storing.
Receives the key the function and same args and kwargs as the called function.
:param namespace: string to use as default prefix for the key used in all operations of
the backend. Default is None
:param key_builder: Callable that allows to build the function dynamically. It receives
the function plus same args and kwargs passed to the function.
This behavior is necessarily different than ``BaseCache.build_key()``
:param ttl: int seconds to store the keys. Default is 0 which means no expiration.
:param cache: cache class to use when calling the ``multi_set``/``multi_get`` operations.
Default is :class:`aiocache.SimpleMemoryCache`.
Expand All @@ -268,6 +291,7 @@ class multi_cached:
def __init__(
self,
keys_from_attr,
namespace=None,
key_builder=None,
ttl=SENTINEL,
cache=Cache.MEMORY,
Expand All @@ -284,6 +308,7 @@ def __init__(

self._cache = cache
self._serializer = serializer
self._namespace = namespace
self._plugins = plugins
self._kwargs = kwargs

Expand All @@ -294,6 +319,7 @@ def __call__(self, f):
self.cache = _get_cache(
cache=self._cache,
serializer=self._serializer,
namespace=self._namespace,
plugins=self._plugins,
**self._kwargs,
)
Expand Down Expand Up @@ -356,20 +382,22 @@ def get_cache_keys(self, f, args, kwargs):
return keys, new_args, keys_index

async def get_from_cache(self, *keys):
namespace = self.cache.namespace
if not keys:
return []
try:
values = await self.cache.multi_get(keys)
values = await self.cache.multi_get(keys, namespace=namespace)
return values
except Exception:
logger.exception("Couldn't retrieve %s, unexpected error", keys)
return [None] * len(keys)

async def set_in_cache(self, result, fn, fn_args, fn_kwargs):
namespace = self.cache.namespace
try:
await self.cache.multi_set(
[(self.key_builder(k, fn, *fn_args, **fn_kwargs), v) for k, v in result.items()],
ttl=self.ttl,
namespace=namespace, ttl=self.ttl,
)
except Exception:
logger.exception("Couldn't set %s, unexpected error", result)
5 changes: 3 additions & 2 deletions aiocache/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ class RedLock:

_EVENTS: Dict[str, asyncio.Event] = {}

def __init__(self, client: BaseCache, key: str, lease: Union[int, float]):
def __init__(self, client: BaseCache, key: str, lease: Union[int, float],
namespace=None):
self.client = client
self.key = self.client._build_key(key + "-lock")
self.key = self.client._build_key(key + "-lock", namespace=namespace)
self.lease = lease
self._value = ""

Expand Down
30 changes: 28 additions & 2 deletions tests/acceptance/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ async def fn(self, a, b=2):
await fn("self", 1, 3)
assert await cache.exists(build_key(fn, "self", 1, 3)) is True

async def test_cached_without_namespace(self, cache):
"""Default cache key is created when no namespace is provided"""
@cached(namespace=None)
async def fn():
return "1"
decorated_fn = cached(fn, namespace=None)

await fn()
key = decorated_fn.get_cache_key(fn, args=(), kwargs={})
assert await cache.exists(key, namespace=None) is True

async def test_cached_with_namespace(self, cache):
"""Cache key is prefixed with provided namespace"""
key_prefix = cache.namespace

@cached(namespace=key_prefix)
async def ns_fn():
return "1"
decorated_ns_fn = cached(ns_fn, namespace=key_prefix)

await ns_fn()
key = decorated_ns_fn.get_cache_key(ns_fn, args=(), kwargs={})
assert await cache.exists(key, namespace=key_prefix) is True


class TestCachedStampede:
@pytest.fixture(autouse=True)
Expand All @@ -61,10 +85,12 @@ async def test_cached_stampede(self, mocker, cache):

await asyncio.gather(decorator(stub)(0.5), decorator(stub)(0.5))

cache.get.assert_called_with("tests.acceptance.test_decoratorsstub(0.5,)[]")
cache.get.assert_called_with("tests.acceptance.test_decoratorsstub(0.5,)[]",
namespace=cache.namespace)
assert cache.get.call_count == 4
cache.set.assert_called_with("tests.acceptance.test_decoratorsstub(0.5,)[]",
mock.ANY, ttl=10)
mock.ANY, ttl=10,
namespace=cache.namespace)
assert cache.set.call_count == 1, cache.set.call_args_list

async def test_locking_dogpile_lease_expiration(self, mocker, cache):
Expand Down
2 changes: 1 addition & 1 deletion tests/ut/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def reset_caches():

@pytest.fixture
def mock_cache(mocker):
return create_autospec(BaseCache, instance=True)
return create_autospec(BaseCache, instance=True, namespace="test")


@pytest.fixture
Expand Down
44 changes: 27 additions & 17 deletions tests/ut/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ def test_init(self):
assert c.cache is None
assert c._cache == SimpleMemoryCache
assert c._serializer is None
assert c._kwargs == {"namespace": "test"}
assert c._namespace == "test"
assert c._kwargs == {}

def test_fails_at_instantiation(self):
with pytest.raises(TypeError):
Expand Down Expand Up @@ -100,7 +101,7 @@ async def test_calls_get_and_returns(self, decorator, decorator_call):

await decorator_call()

decorator.cache.get.assert_called_with("stub()[]")
decorator.cache.get.assert_called_with("stub()[]", namespace="test")
assert decorator.cache.set.call_count == 0
assert stub.call_count == 0

Expand Down Expand Up @@ -173,12 +174,14 @@ async def test_cache_write_doesnt_wait_for_future(self, mocker, decorator, decor

async def test_set_calls_set(self, decorator, decorator_call):
await decorator.set_in_cache("key", "value")
decorator.cache.set.assert_called_with("key", "value", ttl=SENTINEL)
decorator.cache.set.assert_called_with(
"key", "value", namespace="test", ttl=SENTINEL)

async def test_set_calls_set_ttl(self, decorator, decorator_call):
decorator.ttl = 10
await decorator.set_in_cache("key", "value")
decorator.cache.set.assert_called_with("key", "value", ttl=decorator.ttl)
decorator.cache.set.assert_called_with(
"key", "value", namespace="test", ttl=decorator.ttl)

async def test_set_catches_exception(self, decorator, decorator_call):
decorator.cache.set.side_effect = Exception
Expand Down Expand Up @@ -209,7 +212,7 @@ async def what(self, a, b):

async def test_reuses_cache_instance(self):
with patch("aiocache.decorators._get_cache", autospec=True) as get_c:
cache = create_autospec(BaseCache, instance=True)
cache = create_autospec(BaseCache, instance=True, namespace="test")
get_c.side_effect = [cache, None]

@cached()
Expand Down Expand Up @@ -272,14 +275,15 @@ def test_init(self):
assert c._cache == SimpleMemoryCache
assert c._serializer is None
assert c.lease == 3
assert c._kwargs == {"namespace": "test"}
assert c._namespace == "test"
assert c._kwargs == {}

async def test_calls_get_and_returns(self, decorator, decorator_call):
decorator.cache.get.return_value = 1

await decorator_call()

decorator.cache.get.assert_called_with("stub()[]")
decorator.cache.get.assert_called_with("stub()[]", namespace="test")
assert decorator.cache.set.call_count == 0
assert stub.call_count == 0

Expand All @@ -291,7 +295,7 @@ async def test_calls_fn_raises_exception(self, decorator, decorator_call):

async def test_calls_redlock(self, decorator, decorator_call):
decorator.cache.get.return_value = None
lock = create_autospec(RedLock, instance=True)
lock = create_autospec(RedLock, instance=True, namespace="test")

with patch("aiocache.decorators.RedLock", autospec=True, return_value=lock):
await decorator_call(value="value")
Expand All @@ -300,15 +304,16 @@ async def test_calls_redlock(self, decorator, decorator_call):
assert lock.__aenter__.call_count == 1
assert lock.__aexit__.call_count == 1
decorator.cache.set.assert_called_with(
"stub()[('value', 'value')]", "value", ttl=SENTINEL
"stub()[('value', 'value')]", "value",
namespace="test", ttl=SENTINEL
)
stub.assert_called_once_with(value="value")

async def test_calls_locked_client(self, decorator, decorator_call):
decorator.cache.get.side_effect = [None, None, None, "value"]
decorator.cache._add.side_effect = [True, ValueError]
lock1 = create_autospec(RedLock, instance=True)
lock2 = create_autospec(RedLock, instance=True)
lock1 = create_autospec(RedLock, instance=True, namespace="test")
lock2 = create_autospec(RedLock, instance=True, namespace="test")

with patch("aiocache.decorators.RedLock", autospec=True, side_effect=[lock1, lock2]):
await asyncio.gather(decorator_call(value="value"), decorator_call(value="value"))
Expand All @@ -319,7 +324,8 @@ async def test_calls_locked_client(self, decorator, decorator_call):
assert lock2.__aenter__.call_count == 1
assert lock2.__aexit__.call_count == 1
decorator.cache.set.assert_called_with(
"stub()[('value', 'value')]", "value", ttl=SENTINEL
"stub()[('value', 'value')]", "value",
namespace="test", ttl=SENTINEL
)
assert stub.call_count == 1

Expand Down Expand Up @@ -366,7 +372,8 @@ def f():
assert mc.cache is None
assert mc._cache == SimpleMemoryCache
assert mc._serializer is None
assert mc._kwargs == {"namespace": "test"}
assert mc._namespace == "test"
assert mc._kwargs == {}

def test_fails_at_instantiation(self):
with pytest.raises(TypeError):
Expand Down Expand Up @@ -418,7 +425,8 @@ async def test_get_from_cache(self, decorator, decorator_call):
decorator.cache.multi_get.return_value = [1, 2, 3]

assert await decorator.get_from_cache("a", "b", "c") == [1, 2, 3]
decorator.cache.multi_get.assert_called_with(("a", "b", "c"))
decorator.cache.multi_get.assert_called_with(("a", "b", "c"),
namespace="test")

async def test_get_from_cache_no_keys(self, decorator, decorator_call):
assert await decorator.get_from_cache() == []
Expand All @@ -428,13 +436,15 @@ async def test_get_from_cache_exception(self, decorator, decorator_call):
decorator.cache.multi_get.side_effect = Exception

assert await decorator.get_from_cache("a", "b", "c") == [None, None, None]
decorator.cache.multi_get.assert_called_with(("a", "b", "c"))
decorator.cache.multi_get.assert_called_with(("a", "b", "c"),
namespace="test")

async def test_get_from_cache_conn(self, decorator, decorator_call):
decorator.cache.multi_get.return_value = [1, 2, 3]

assert await decorator.get_from_cache("a", "b", "c") == [1, 2, 3]
decorator.cache.multi_get.assert_called_with(("a", "b", "c"))
decorator.cache.multi_get.assert_called_with(("a", "b", "c"),
namespace="test")

async def test_calls_no_keys(self, decorator, decorator_call):
await decorator_call(keys=[])
Expand Down Expand Up @@ -559,7 +569,7 @@ async def what(self, keys=None, what=1):

async def test_reuses_cache_instance(self):
with patch("aiocache.decorators._get_cache", autospec=True) as get_c:
cache = create_autospec(BaseCache, instance=True)
cache = create_autospec(BaseCache, instance=True, namespace="test")
cache.multi_get.return_value = [None]
get_c.side_effect = [cache, None]

Expand Down