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

Simplify cache namespace and key encoding logic for v1 #670

Merged
merged 36 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7bd719a
Move namespace logic to BaseCache.build_key()
padraic-shafer Feb 18, 2023
0eeab3e
BaseCache._build_key() needs keyword param
padraic-shafer Feb 18, 2023
25943bd
ut/test_base/alt_base_cache tests use clearer default namespace
padraic-shafer Feb 19, 2023
fcd7afc
Update ut/test_base/alt_base_cache tests with new build_key() namespa…
padraic-shafer Feb 19, 2023
139e0a4
Update redis namespaced key for new build_key() compatibility
padraic-shafer Feb 19, 2023
eebe299
Update memcached key encoding for new build_key() compatibility
padraic-shafer Feb 19, 2023
6622f57
test/ut/test_base.py now calls build_key(), rather than _build_key()
padraic-shafer Feb 19, 2023
54ce48d
Move Enum _ensure_key() logic into BaseCache:build_key()
padraic-shafer Feb 19, 2023
b2e58d7
Update changelog with revisied cache build_key() scheme
padraic-shafer Feb 19, 2023
223b537
Clarify that output of BaseCache key_builder arg should be a string
padraic-shafer Feb 19, 2023
e8d6037
Clarify 'key_builder' parameter docstring in 'BaseCache.__init__()'
padraic-shafer Feb 20, 2023
9da3608
Merge branch 'master' into simplify-cache-namespace
padraic-shafer Feb 21, 2023
db94261
Ensure that cache namespace is a string
padraic-shafer Feb 21, 2023
84ce34c
mypy annotations for build_key() with namespace
padraic-shafer Feb 22, 2023
8392b15
Merge branch 'master' into simplify-cache-namespace
padraic-shafer Feb 22, 2023
16db995
Define BaseCache as generic, typed by backend cache key
padraic-shafer Feb 23, 2023
9dc7c77
Update tests for BaseCache with TypeVar
padraic-shafer Feb 24, 2023
a757e9a
Propagate type annotations for mypy compliance
padraic-shafer Feb 24, 2023
54093a1
Resolve mypy type annotations in tests
padraic-shafer Feb 24, 2023
d00a18b
Default namespace is empty string for decorators
padraic-shafer Feb 24, 2023
7239f16
Call build_key() with namespace as positional argument
padraic-shafer Feb 24, 2023
5995d64
redis.asyncio.Redis is generic for type checking purposes, but not at…
padraic-shafer Feb 24, 2023
e8c2306
Update alt_key_builder example with revised build_key() logickey_builder
padraic-shafer Feb 24, 2023
4126d60
Remove unneeded typing in decorator tests [mypy]
padraic-shafer Feb 24, 2023
3563def
BaseCache key_builder param defaults to lambda
padraic-shafer Feb 24, 2023
07e1919
Remove extraneous comments
padraic-shafer Feb 24, 2023
589d2b8
Remove bound on CacheKeyType
padraic-shafer Feb 24, 2023
08d8bf9
Correct the return annotation for redis_client fixture
padraic-shafer Feb 24, 2023
08b70d7
Test for abstract BaseCache
padraic-shafer Feb 24, 2023
64436a0
Revert extraneous mypy typing fixes
padraic-shafer Feb 24, 2023
bc385f7
Revise the changelog for PR #670
padraic-shafer Feb 24, 2023
396952f
Update redis.py
Dreamsorcerer Feb 27, 2023
6c4780e
Update base.py
Dreamsorcerer Feb 27, 2023
ce2f26a
Update alt_key_builder.py
Dreamsorcerer Feb 27, 2023
3d50611
Update CHANGES.rst
Dreamsorcerer Feb 27, 2023
23cd481
Update CHANGES.rst
Dreamsorcerer Feb 27, 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
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ There are a number of backwards-incompatible changes. These points should help w
* The ``key`` parameter has been removed from the ``cached`` decorator. The behaviour can be easily reimplemented with ``key_builder=lambda *a, **kw: "foo"``
* When using the ``key_builder`` parameter in ``@multicached``, the function will now return the original, unmodified keys, only using the transformed keys in the cache (this has always been the documented behaviour, but not the implemented behaviour).
* ``BaseSerializer`` is now an ``ABC``, so cannot be instantiated directly.
* The logic for encoding a cache key and selecting the key's namespace is now encapsulated in
the ``build_key(key, namespace)`` member of BaseCache (and its backend subclasses). Now
creating a cache with a custom ``key_builder`` argument simply requires that function to
return any string mapping from the ``key`` and optional ``namespace`` parameters.


0.12.0 (2023-01-13)
Expand Down
7 changes: 4 additions & 3 deletions aiocache/backends/memcached.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
from typing import Optional

import aiomcache

Expand Down Expand Up @@ -130,7 +131,7 @@ class MemcachedCache(MemcachedBackend):
:param serializer: obj derived from :class:`aiocache.serializers.BaseSerializer`.
:param plugins: list of :class:`aiocache.plugins.BasePlugin` derived classes.
:param namespace: string to use as default prefix for the key used in all operations of
the backend. Default is None
the backend. Default is an empty string, "".
:param timeout: int or float in seconds specifying maximum timeout for the operations to last.
By default its 5.
:param endpoint: str with the endpoint to connect to. Default is 127.0.0.1.
Expand All @@ -147,8 +148,8 @@ def __init__(self, serializer=None, **kwargs):
def parse_uri_path(cls, path):
return {}

def _build_key(self, key, namespace=None):
ns_key = super()._build_key(key, namespace=namespace).replace(" ", "_")
def build_key(self, key: str, namespace: Optional[str] = None) -> str:
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
ns_key = super().build_key(key, namespace=namespace).replace(" ", "_")
return str.encode(ns_key)

def __repr__(self): # pragma: no cover
Expand Down
2 changes: 1 addition & 1 deletion aiocache/backends/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class SimpleMemoryCache(SimpleMemoryBackend):
:param serializer: obj derived from :class:`aiocache.serializers.BaseSerializer`.
:param plugins: list of :class:`aiocache.plugins.BasePlugin` derived classes.
:param namespace: string to use as default prefix for the key used in all operations of
the backend. Default is None.
the backend. Default is an empty string, "".
:param timeout: int or float in seconds specifying maximum timeout for the operations to last.
By default its 5.
"""
Expand Down
32 changes: 19 additions & 13 deletions aiocache/backends/redis.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import itertools
import warnings
from typing import Callable, Optional

import redis.asyncio as redis
from redis.exceptions import ResponseError as IncrbyException

from aiocache.base import BaseCache, _ensure_key
from aiocache.serializers import JsonSerializer
from aiocache.base import BaseCache
from aiocache.serializers import BaseSerializer, JsonSerializer


_NOT_SET = object()
Expand Down Expand Up @@ -186,7 +187,7 @@ class RedisCache(RedisBackend):
:param serializer: obj derived from :class:`aiocache.serializers.BaseSerializer`.
:param plugins: list of :class:`aiocache.plugins.BasePlugin` derived classes.
:param namespace: string to use as default prefix for the key used in all operations of
the backend. Default is None.
the backend. Default is an empty string, "".
:param timeout: int or float in seconds specifying maximum timeout for the operations to last.
By default its 5.
:param endpoint: str with the endpoint to connect to. Default is "127.0.0.1".
Expand All @@ -199,8 +200,19 @@ class RedisCache(RedisBackend):

NAME = "redis"

def __init__(self, serializer=None, **kwargs):
super().__init__(serializer=serializer or JsonSerializer(), **kwargs)
def __init__(
self,
serializer: Optional[BaseSerializer] = None,
namespace: str = "",
key_builder: Optional[Callable[[str, str], str]] = None,
**kwargs,
):
super().__init__(
serializer=serializer or JsonSerializer(),
namespace=namespace,
key_builder=key_builder or (
lambda key, namespace="": f"{namespace}:{key}" if namespace else key),
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
**kwargs)

@classmethod
def parse_uri_path(cls, path):
Expand All @@ -218,14 +230,8 @@ def parse_uri_path(cls, path):
options["db"] = db
return options

def _build_key(self, key, namespace=None):
if namespace is not None:
return "{}{}{}".format(
namespace, ":" if namespace else "", _ensure_key(key))
if self.namespace is not None:
return "{}{}{}".format(
self.namespace, ":" if self.namespace else "", _ensure_key(key))
return key
def _build_key_default(self, key: str, namespace: str = "") -> str:
return f"{namespace}:{key}" if namespace else key
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved

def __repr__(self): # pragma: no cover
return "RedisCache ({}:{})".format(self.endpoint, self.port)
82 changes: 38 additions & 44 deletions aiocache/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
import time
from enum import Enum
from types import TracebackType
from typing import Callable, Optional, Set, Type
from typing import Any, Callable, List, Optional, Set, Type

from aiocache import serializers
# from aiocache.plugins import BasePlugin
from aiocache.serializers import BaseSerializer, StringSerializer


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -97,9 +98,9 @@ class BaseCache:
:param plugins: list of :class:`aiocache.plugins.BasePlugin` derived classes. Default is empty
list.
:param namespace: string to use as default prefix for the key used in all operations of
the backend. Default is None
the backend. Default is an empty string, "".
:param key_builder: alternative callable to build the key. Receives the key and the namespace
as params and should return something that can be used as key by the underlying backend.
as params and should return a string that can be used as a key by the underlying backend.
:param timeout: int or float in seconds specifying maximum timeout for the operations to last.
By default its 5. Use 0 or None if you want to disable it.
:param ttl: int the expiration time in seconds to use as a default in all operations of
Expand All @@ -109,18 +110,26 @@ class BaseCache:
NAME: str

def __init__(
self, serializer=None, plugins=None, namespace=None, key_builder=None, timeout=5, ttl=None
self,
serializer: Optional[BaseSerializer] = None,
plugins: Optional[List[Any]] = None, # aiocache.plugins depends on aiocache.base
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
namespace: str = "",
Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved
key_builder: Optional[Callable[[str, str], str]] = None,
timeout: Optional[float] = 5,
ttl: Optional[float] = None,
):
self.timeout = float(timeout) if timeout is not None else timeout
self.namespace = namespace
self.ttl = float(ttl) if ttl is not None else ttl
self.build_key = key_builder or self._build_key
self.timeout: Optional[float] = float(timeout) if timeout is not None else None
self.ttl: Optional[float] = float(ttl) if ttl is not None else None
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved

self._serializer = None
self.serializer = serializer or serializers.StringSerializer()
self.namespace: str = namespace
self._build_key: Callable[[str, str], str] = key_builder or (
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
lambda key, namespace="": f"{namespace}{key}")
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved

self._plugins = None
self.plugins = plugins or []
self._serializer: Optional[BaseSerializer] = None
self.serializer: BaseSerializer = serializer or StringSerializer()
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved

self._plugins: List[Any] = None
self.plugins: List[Any] = plugins or []
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved

@property
def serializer(self):
Expand Down Expand Up @@ -163,8 +172,7 @@ async def add(self, key, value, ttl=SENTINEL, dumps_fn=None, namespace=None, _co
"""
start = time.monotonic()
dumps = dumps_fn or self._serializer.dumps
ns = namespace if namespace is not None else self.namespace
ns_key = self.build_key(key, namespace=ns)
ns_key = self.build_key(key, namespace=namespace)

await self._add(ns_key, dumps(value), ttl=self._get_ttl(ttl), _conn=_conn)

Expand Down Expand Up @@ -193,8 +201,7 @@ async def get(self, key, default=None, loads_fn=None, namespace=None, _conn=None
"""
start = time.monotonic()
loads = loads_fn or self._serializer.loads
ns = namespace if namespace is not None else self.namespace
ns_key = self.build_key(key, namespace=ns)
ns_key = self.build_key(key, namespace=namespace)

value = loads(await self._get(ns_key, encoding=self.serializer.encoding, _conn=_conn))

Expand Down Expand Up @@ -225,9 +232,8 @@ async def multi_get(self, keys, loads_fn=None, namespace=None, _conn=None):
"""
start = time.monotonic()
loads = loads_fn or self._serializer.loads
ns = namespace if namespace is not None else self.namespace

ns_keys = [self.build_key(key, namespace=ns) for key in keys]
ns_keys = [self.build_key(key, namespace=namespace) for key in keys]
values = [
loads(value)
for value in await self._multi_get(
Expand Down Expand Up @@ -270,8 +276,7 @@ async def set(
"""
start = time.monotonic()
dumps = dumps_fn or self._serializer.dumps
ns = namespace if namespace is not None else self.namespace
ns_key = self.build_key(key, namespace=ns)
ns_key = self.build_key(key, namespace=namespace)

res = await self._set(
ns_key, dumps(value), ttl=self._get_ttl(ttl), _cas_token=_cas_token, _conn=_conn
Expand Down Expand Up @@ -304,11 +309,10 @@ async def multi_set(self, pairs, ttl=SENTINEL, dumps_fn=None, namespace=None, _c
"""
start = time.monotonic()
dumps = dumps_fn or self._serializer.dumps
ns = namespace if namespace is not None else self.namespace

tmp_pairs = []
for key, value in pairs:
tmp_pairs.append((self.build_key(key, namespace=ns), dumps(value)))
tmp_pairs.append((self.build_key(key, namespace=namespace), dumps(value)))

await self._multi_set(tmp_pairs, ttl=self._get_ttl(ttl), _conn=_conn)

Expand Down Expand Up @@ -339,8 +343,7 @@ async def delete(self, key, namespace=None, _conn=None):
:raises: :class:`asyncio.TimeoutError` if it lasts more than self.timeout
"""
start = time.monotonic()
ns = namespace if namespace is not None else self.namespace
ns_key = self.build_key(key, namespace=ns)
ns_key = self.build_key(key, namespace=namespace)
ret = await self._delete(ns_key, _conn=_conn)
logger.debug("DELETE %s %d (%.4f)s", ns_key, ret, time.monotonic() - start)
return ret
Expand All @@ -364,8 +367,7 @@ async def exists(self, key, namespace=None, _conn=None):
:raises: :class:`asyncio.TimeoutError` if it lasts more than self.timeout
"""
start = time.monotonic()
ns = namespace if namespace is not None else self.namespace
ns_key = self.build_key(key, namespace=ns)
ns_key = self.build_key(key, namespace=namespace)
ret = await self._exists(ns_key, _conn=_conn)
logger.debug("EXISTS %s %d (%.4f)s", ns_key, ret, time.monotonic() - start)
return ret
Expand All @@ -392,8 +394,7 @@ async def increment(self, key, delta=1, namespace=None, _conn=None):
:raises: :class:`TypeError` if value is not incrementable
"""
start = time.monotonic()
ns = namespace if namespace is not None else self.namespace
ns_key = self.build_key(key, namespace=ns)
ns_key = self.build_key(key, namespace=namespace)
ret = await self._increment(ns_key, delta, _conn=_conn)
logger.debug("INCREMENT %s %d (%.4f)s", ns_key, ret, time.monotonic() - start)
return ret
Expand All @@ -418,8 +419,7 @@ async def expire(self, key, ttl, namespace=None, _conn=None):
:raises: :class:`asyncio.TimeoutError` if it lasts more than self.timeout
"""
start = time.monotonic()
ns = namespace if namespace is not None else self.namespace
ns_key = self.build_key(key, namespace=ns)
ns_key = self.build_key(key, namespace=namespace)
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
ret = await self._expire(ns_key, ttl, _conn=_conn)
logger.debug("EXPIRE %s %d (%.4f)s", ns_key, ret, time.monotonic() - start)
return ret
Expand Down Expand Up @@ -498,12 +498,13 @@ async def close(self, *args, _conn=None, **kwargs):
async def _close(self, *args, **kwargs):
pass

def _build_key(self, key, namespace=None):
if namespace is not None:
return "{}{}".format(namespace, _ensure_key(key))
if self.namespace is not None:
return "{}{}".format(self.namespace, _ensure_key(key))
return key
def build_key(self, key: str, namespace: Optional[str] = None) -> str:
key_name = key.value if isinstance(key, Enum) else key
ns = namespace if namespace is not None else self.namespace
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved
return self._build_key(key_name, namespace=ns)
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved

def _build_key_default(self, key: str, namespace: str = "") -> str:
return f"{namespace}{key}"
padraic-shafer marked this conversation as resolved.
Show resolved Hide resolved

def _get_ttl(self, ttl):
return ttl if ttl is not SENTINEL else self.ttl
Expand Down Expand Up @@ -550,12 +551,5 @@ async def _do_inject_conn(self, *args, **kwargs):
return _do_inject_conn


def _ensure_key(key):
if isinstance(key, Enum):
return key.value
else:
return key


for cmd in API.CMDS:
setattr(_Conn, cmd.__name__, _Conn._inject_conn(cmd.__name__))
6 changes: 3 additions & 3 deletions aiocache/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class cached:

:param ttl: int seconds to store the function call. Default is None which means no expiration.
:param namespace: string to use as default prefix for the key used in all operations of
the backend. Default is None
the backend. Default is an empty string, "".
: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()``
Expand Down Expand Up @@ -176,7 +176,7 @@ class cached_stampede(cached):
:param ttl: int seconds to store the function call. Default is None which means no expiration.
: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
the backend. Default is an empty string, "".
: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()``
Expand Down Expand Up @@ -278,7 +278,7 @@ class multi_cached:
:param keys_from_attr: name of the arg or kwarg in the decorated callable that contains
an iterable that yields the keys returned by the decorated callable.
:param namespace: string to use as default prefix for the key used in all operations of
the backend. Default is None
the backend. Default is an empty string, "".
:param key_builder: Callable that enables mapping the decorated function's keys to the keys
used by the cache. Receives a key from the iterable corresponding to
``keys_from_attr``, the decorated callable, and the positional and keyword arguments
Expand Down
9 changes: 4 additions & 5 deletions tests/acceptance/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import pytest

from aiocache import cached, cached_stampede, multi_cached
from aiocache.base import _ensure_key
from ..utils import Keys
from ..utils import Keys, ensure_key


async def return_dict(keys=None):
Expand Down Expand Up @@ -164,15 +163,15 @@ async def fn(keys):

async def test_multi_cached_key_builder(self, cache):
def build_key(key, f, self, keys, market="ES"):
return "{}_{}_{}".format(f.__name__, _ensure_key(key), market)
return "{}_{}_{}".format(f.__name__, ensure_key(key), market)

@multi_cached(keys_from_attr="keys", key_builder=build_key)
async def fn(self, keys, market="ES"):
return {Keys.KEY: 1, Keys.KEY_1: 2}

await fn("self", keys=[Keys.KEY, Keys.KEY_1])
assert await cache.exists("fn_" + _ensure_key(Keys.KEY) + "_ES") is True
assert await cache.exists("fn_" + _ensure_key(Keys.KEY_1) + "_ES") is True
assert await cache.exists("fn_" + ensure_key(Keys.KEY) + "_ES") is True
assert await cache.exists("fn_" + ensure_key(Keys.KEY_1) + "_ES") is True

async def test_multi_cached_skip_keys(self, cache):
@multi_cached(keys_from_attr="keys", skip_cache_func=lambda _, v: v is None)
Expand Down
2 changes: 1 addition & 1 deletion tests/acceptance/test_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def lock(self, cache):
async def test_acquire(self, cache, lock):
await cache.set(Keys.KEY, "value")
async with lock:
assert lock._token == await cache._gets(cache._build_key(Keys.KEY))
assert lock._token == await cache._gets(cache.build_key(Keys.KEY))

async def test_release_does_nothing(self, lock):
assert await lock.__aexit__("exc_type", "exc_value", "traceback") is None
Expand Down
6 changes: 3 additions & 3 deletions tests/ut/backends/test_memcached.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import pytest

from aiocache.backends.memcached import MemcachedBackend, MemcachedCache
from aiocache.base import BaseCache, _ensure_key
from aiocache.base import BaseCache
from aiocache.serializers import JsonSerializer
from ...utils import Keys
from ...utils import Keys, ensure_key


@pytest.fixture
Expand Down Expand Up @@ -249,7 +249,7 @@ def test_parse_uri_path(self):

@pytest.mark.parametrize(
"namespace, expected",
([None, "test" + _ensure_key(Keys.KEY)], ["", _ensure_key(Keys.KEY)], ["my_ns", "my_ns" + _ensure_key(Keys.KEY)]), # type: ignore[attr-defined] # noqa: B950
([None, "test" + ensure_key(Keys.KEY)], ["", ensure_key(Keys.KEY)], ["my_ns", "my_ns" + ensure_key(Keys.KEY)]), # type: ignore[attr-defined] # noqa: B950
)
def test_build_key_bytes(self, set_test_namespace, memcached_cache, namespace, expected):
assert memcached_cache.build_key(Keys.KEY, namespace=namespace) == expected.encode()
Expand Down
Loading