diff --git a/README.rst b/README.rst index 5b82b931..555c5f51 100644 --- a/README.rst +++ b/README.rst @@ -62,8 +62,8 @@ Using a cache is as simple as >>> import asyncio >>> loop = asyncio.get_event_loop() - >>> from aiocache import SimpleMemoryCache # Here you can also use RedisCache and MemcachedCache - >>> cache = SimpleMemoryCache() + >>> from aiocache import Cache + >>> cache = Cache(Cache.MEMORY) # Here you can also use Cache.REDIS and Cache.MEMCACHED, default is Cache.MEMORY >>> loop.run_until_complete(cache.set('key', 'value')) True >>> loop.run_until_complete(cache.get('key')) @@ -77,7 +77,7 @@ Or as a decorator from collections import namedtuple - from aiocache import cached, RedisCache + from aiocache import cached, Cache from aiocache.serializers import PickleSerializer # With this we can store python objects in backends like Redis! @@ -85,7 +85,7 @@ Or as a decorator @cached( - ttl=10, cache=RedisCache, key="key", serializer=PickleSerializer(), port=6379, namespace="main") + ttl=10, cache=Cache.REDIS, key="key", serializer=PickleSerializer(), port=6379, namespace="main") async def cached_call(): print("Sleeping for three seconds zzzz.....") await asyncio.sleep(3) @@ -97,12 +97,14 @@ Or as a decorator loop.run_until_complete(cached_call()) loop.run_until_complete(cached_call()) loop.run_until_complete(cached_call()) - cache = RedisCache(endpoint="127.0.0.1", port=6379, namespace="main") + cache = Cache(Cache.REDIS, endpoint="127.0.0.1", port=6379, namespace="main") loop.run_until_complete(cache.delete("key")) if __name__ == "__main__": run() +The recommended approach to instantiate a new cache is using the `Cache` constructor. However you can also instantiate directly using `aiocache.RedisCache`, `aiocache.SimpleMemoryCache` or `aiocache.MemcachedCache`. + You can also setup cache aliases so its easy to reuse configurations @@ -110,8 +112,7 @@ You can also setup cache aliases so its easy to reuse configurations import asyncio - from aiocache import caches, SimpleMemoryCache, RedisCache - from aiocache.serializers import StringSerializer, PickleSerializer + from aiocache import caches # You can use either classes or strings for referencing classes caches.set_config({ diff --git a/aiocache/__init__.py b/aiocache/__init__.py index 1f5fed35..49d0cf61 100644 --- a/aiocache/__init__.py +++ b/aiocache/__init__.py @@ -1,8 +1,6 @@ import logging from .backends.memory import SimpleMemoryCache -from .factory import caches -from .decorators import cached, cached_stampede, multi_cached from ._version import __version__ @@ -30,4 +28,16 @@ del aiomcache -__all__ = ("caches", "cached", "cached_stampede", "multi_cached", *__cache_types, "__version__") +from .factory import caches, Cache # noqa: E402 +from .decorators import cached, cached_stampede, multi_cached # noqa: E402 + + +__all__ = ( + "caches", + "Cache", + "cached", + "cached_stampede", + "multi_cached", + *__cache_types, + "__version__", +) diff --git a/aiocache/exceptions.py b/aiocache/exceptions.py new file mode 100644 index 00000000..e5db3b4d --- /dev/null +++ b/aiocache/exceptions.py @@ -0,0 +1,2 @@ +class InvalidCacheType(Exception): + pass diff --git a/aiocache/factory.py b/aiocache/factory.py index 7c4b5e57..2d711e77 100644 --- a/aiocache/factory.py +++ b/aiocache/factory.py @@ -1,4 +1,12 @@ from copy import deepcopy +import logging + + +from aiocache import SimpleMemoryCache, RedisCache, MemcachedCache +from aiocache.exceptions import InvalidCacheType + + +logger = logging.getLogger(__name__) def _class_from_string(class_path): @@ -26,6 +34,35 @@ def _create_cache(cache, serializer=None, plugins=None, **kwargs): return instance +class Cache: + + MEMORY = "memory" + REDIS = "redis" + MEMCACHED = "memcached" + + _PROTOCOL_MAPPING = { + "memory": SimpleMemoryCache, + "redis": RedisCache, + "memcached": MemcachedCache, + } + + def __new__(cls, cache_type=MEMORY, **kwargs): + try: + cache_class = cls.get_protocol_class(cache_type) + except KeyError as e: + raise InvalidCacheType( + "Invalid cache type, you can only use {}".format(list(cls._PROTOCOL_MAPPING.keys())) + ) from e + + instance = cache_class.__new__(cache_class, **kwargs) + instance.__init__(**kwargs) + return instance + + @classmethod + def get_protocol_class(cls, protocol): + return cls._PROTOCOL_MAPPING[protocol] + + class CacheHandler: _config = { diff --git a/aiocache/lock.py b/aiocache/lock.py index 20ee58db..627a95bc 100644 --- a/aiocache/lock.py +++ b/aiocache/lock.py @@ -44,10 +44,10 @@ class RedLock: Example usage:: - from aiocache import RedisCache + from aiocache import Cache from aiocache.lock import RedLock - cache = RedisCache() + cache = Cache(Cache.REDIS) async with RedLock(cache, 'key', lease=1): # Calls will wait here result = await cache.get('key') if result is not None: @@ -112,7 +112,7 @@ class OptimisticLock: Example usage:: - cache = RedisCache() + cache = Cache(Cache.REDIS) # The value stored in 'key' will be checked here async with OptimisticLock(cache, 'key') as lock: @@ -123,7 +123,7 @@ class OptimisticLock: an :class:`aiocache.lock.OptimisticLockError` will be raised. A way to make the same call crash would be to change the value inside the lock like:: - cache = RedisCache() + cache = Cache(Cache.REDIS) # The value stored in 'key' will be checked here async with OptimisticLock(cache, 'key') as lock: diff --git a/aiocache/serializers.py b/aiocache/serializers.py index c487a76f..836d670e 100644 --- a/aiocache/serializers.py +++ b/aiocache/serializers.py @@ -42,7 +42,7 @@ class NullSerializer(BaseSerializer): DISCLAIMER: Be careful with mutable types and memory storage. The following behavior is considered normal (same as ``functools.lru_cache``):: - cache = SimpleMemoryCache() + cache = Cache() my_list = [1] await cache.set("key", my_list) my_list.append(2) diff --git a/docs/index.rst b/docs/index.rst index 5b359fc5..3da30b56 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,8 +25,8 @@ Using a cache is as simple as >>> import asyncio >>> loop = asyncio.get_event_loop() - >>> from aiocache import SimpleMemoryCache - >>> cache = SimpleMemoryCache() + >>> from aiocache import Cache + >>> cache = Cache() >>> loop.run_until_complete(cache.set('key', 'value')) True >>> loop.run_until_complete(cache.get('key')) diff --git a/docs/plugins.rst b/docs/plugins.rst index 3bdf95fc..9b21731c 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -5,9 +5,9 @@ Plugins Plugins can be used to enrich the behavior of the cache. By default all caches are configured without any plugin but can add new ones in the constructor or after initializing the cache class:: - >>> from aiocache import SimpleMemoryCache + >>> from aiocache import Cache >>> from aiocache.plugins import TimingPlugin - cache = SimpleMemoryCache(plugins=[HitMissRatioPlugin()]) + cache = Cache(plugins=[HitMissRatioPlugin()]) cache.plugins += [TimingPlugin()] You can define your custom plugin by inheriting from `BasePlugin`_ and overriding the needed methods (the overrides NEED to be async). All commands have ``pre_`` and ``post_`` hooks. diff --git a/examples/cached_decorator.py b/examples/cached_decorator.py index c1a43f14..70a351bd 100644 --- a/examples/cached_decorator.py +++ b/examples/cached_decorator.py @@ -9,7 +9,8 @@ @cached( - ttl=10, cache=RedisCache, key="key", serializer=PickleSerializer(), port=6379, namespace="main") + ttl=10, cache=RedisCache, key="key", serializer=PickleSerializer(), + port=6379, namespace="main") async def cached_call(): return Result("content", 200) diff --git a/examples/frameworks/sanic_example.py b/examples/frameworks/sanic_example.py index 56a1afdd..56eb5da7 100644 --- a/examples/frameworks/sanic_example.py +++ b/examples/frameworks/sanic_example.py @@ -10,7 +10,7 @@ from sanic import Sanic from sanic.response import json from sanic.log import log -from aiocache import cached, SimpleMemoryCache +from aiocache import cached, Cache from aiocache.serializers import JsonSerializer app = Sanic(__name__) @@ -24,7 +24,7 @@ async def expensive_call(): async def reuse_data(): - cache = SimpleMemoryCache(serializer=JsonSerializer()) # Not ideal to define here + cache = Cache(serializer=JsonSerializer()) # Not ideal to define here data = await cache.get("my_custom_key") # Note the key is defined in `cached` decorator return data diff --git a/examples/marshmallow_serializer_class.py b/examples/marshmallow_serializer_class.py index d2ff676b..63a3e16b 100644 --- a/examples/marshmallow_serializer_class.py +++ b/examples/marshmallow_serializer_class.py @@ -4,7 +4,7 @@ from marshmallow import fields, Schema, post_load -from aiocache import SimpleMemoryCache +from aiocache import Cache from aiocache.serializers import BaseSerializer @@ -47,7 +47,7 @@ class Meta: strict = True -cache = SimpleMemoryCache(serializer=MarshmallowSerializer(), namespace="main") +cache = Cache(serializer=MarshmallowSerializer(), namespace="main") async def serializer(): diff --git a/examples/optimistic_lock.py b/examples/optimistic_lock.py index 7339d22f..f4417f53 100644 --- a/examples/optimistic_lock.py +++ b/examples/optimistic_lock.py @@ -2,12 +2,12 @@ import logging import random -from aiocache import RedisCache +from aiocache import Cache from aiocache.lock import OptimisticLock, OptimisticLockError logger = logging.getLogger(__name__) -cache = RedisCache(endpoint='127.0.0.1', port=6379, namespace='main') +cache = Cache(Cache.REDIS, endpoint='127.0.0.1', port=6379, namespace='main') async def expensive_function(): diff --git a/examples/plugins.py b/examples/plugins.py index 683bccca..c5731ca2 100644 --- a/examples/plugins.py +++ b/examples/plugins.py @@ -2,7 +2,7 @@ import random import logging -from aiocache import SimpleMemoryCache +from aiocache import Cache from aiocache.plugins import HitMissRatioPlugin, TimingPlugin, BasePlugin @@ -18,7 +18,7 @@ async def post_set(self, *args, **kwargs): logger.info("I'm the post_set hook being called with %s %s" % (args, kwargs)) -cache = SimpleMemoryCache( +cache = Cache( plugins=[HitMissRatioPlugin(), TimingPlugin(), MyCustomPlugin()], namespace="main") diff --git a/examples/python_object.py b/examples/python_object.py index 5439f546..a0cc8f11 100644 --- a/examples/python_object.py +++ b/examples/python_object.py @@ -1,12 +1,12 @@ import asyncio from collections import namedtuple -from aiocache import RedisCache +from aiocache import Cache from aiocache.serializers import PickleSerializer MyObject = namedtuple("MyObject", ["x", "y"]) -cache = RedisCache(serializer=PickleSerializer(), namespace="main") +cache = Cache(Cache.REDIS, serializer=PickleSerializer(), namespace="main") async def complex_object(): diff --git a/examples/redlock.py b/examples/redlock.py index 8f6c2983..a3f37c70 100644 --- a/examples/redlock.py +++ b/examples/redlock.py @@ -1,12 +1,12 @@ import asyncio import logging -from aiocache import RedisCache +from aiocache import Cache from aiocache.lock import RedLock logger = logging.getLogger(__name__) -cache = RedisCache(endpoint='127.0.0.1', port=6379, namespace='main') +cache = Cache(Cache.REDIS, endpoint='127.0.0.1', port=6379, namespace='main') async def expensive_function(): diff --git a/examples/serializer_class.py b/examples/serializer_class.py index 25f731de..4d1c05b2 100644 --- a/examples/serializer_class.py +++ b/examples/serializer_class.py @@ -1,7 +1,7 @@ import asyncio import zlib -from aiocache import RedisCache +from aiocache import Cache from aiocache.serializers import BaseSerializer @@ -25,7 +25,7 @@ def loads(self, value): return decompressed -cache = RedisCache(serializer=CompressionSerializer(), namespace="main") +cache = Cache(Cache.REDIS, serializer=CompressionSerializer(), namespace="main") async def serializer(): diff --git a/examples/serializer_function.py b/examples/serializer_function.py index b4d06b10..934d3420 100644 --- a/examples/serializer_function.py +++ b/examples/serializer_function.py @@ -2,7 +2,7 @@ import json from marshmallow import Schema, fields, post_load -from aiocache import RedisCache +from aiocache import Cache class MyType: @@ -28,7 +28,7 @@ def loads(value): return MyTypeSchema().loads(value).data -cache = RedisCache(namespace="main") +cache = Cache(Cache.REDIS, namespace="main") async def serializer_function(): diff --git a/examples/simple_redis.py b/examples/simple_redis.py index 86a13d7a..7d1b7449 100644 --- a/examples/simple_redis.py +++ b/examples/simple_redis.py @@ -1,9 +1,9 @@ import asyncio -from aiocache import RedisCache +from aiocache import Cache -cache = RedisCache(endpoint="127.0.0.1", port=6379, namespace="main") +cache = Cache(Cache.REDIS, endpoint="127.0.0.1", port=6379, namespace="main") async def redis(): diff --git a/tests/acceptance/conftest.py b/tests/acceptance/conftest.py index 7ba00e96..0751da04 100644 --- a/tests/acceptance/conftest.py +++ b/tests/acceptance/conftest.py @@ -1,6 +1,6 @@ import pytest -from aiocache import SimpleMemoryCache, RedisCache, MemcachedCache, caches +from aiocache import Cache, caches from aiocache.backends.redis import RedisBackend @@ -28,7 +28,7 @@ def reset_redis_pools(): @pytest.fixture def redis_cache(event_loop): - cache = RedisCache(namespace="test") + cache = Cache(Cache.REDIS, namespace="test") yield cache event_loop.run_until_complete(cache.delete(pytest.KEY)) @@ -39,7 +39,7 @@ def redis_cache(event_loop): @pytest.fixture def memory_cache(event_loop): - cache = SimpleMemoryCache(namespace="test") + cache = Cache(namespace="test") yield cache event_loop.run_until_complete(cache.delete(pytest.KEY)) @@ -50,7 +50,7 @@ def memory_cache(event_loop): @pytest.fixture def memcached_cache(event_loop): - cache = MemcachedCache(namespace="test") + cache = Cache(Cache.MEMCACHED, namespace="test") yield cache event_loop.run_until_complete(cache.delete(pytest.KEY)) diff --git a/tests/performance/conftest.py b/tests/performance/conftest.py index 50ed5784..1c4a610a 100644 --- a/tests/performance/conftest.py +++ b/tests/performance/conftest.py @@ -1,12 +1,12 @@ import pytest -from aiocache import MemcachedCache, RedisCache +from aiocache import Cache from aiocache.backends.redis import RedisBackend @pytest.fixture def redis_cache(event_loop): - cache = RedisCache(namespace="test", pool_max_size=1) + cache = Cache(Cache.REDIS, namespace="test", pool_max_size=1) yield cache for _, pool in RedisBackend.pools.items(): @@ -16,5 +16,5 @@ def redis_cache(event_loop): @pytest.fixture def memcached_cache(): - cache = MemcachedCache(namespace="test", pool_size=1) + cache = Cache(Cache.MEMCACHED, namespace="test", pool_size=1) yield cache diff --git a/tests/performance/server.py b/tests/performance/server.py index 511406b4..55b1a11b 100644 --- a/tests/performance/server.py +++ b/tests/performance/server.py @@ -2,7 +2,7 @@ import argparse import logging import uuid -import aiocache +from aiocache import Cache from aiohttp import web @@ -11,9 +11,9 @@ AIOCACHE_BACKENDS = { - "memory": aiocache.SimpleMemoryCache(), - "redis": aiocache.RedisCache(pool_max_size=1), - "memcached": aiocache.MemcachedCache(pool_size=1), + "memory": Cache(Cache.MEMORY), + "redis": Cache(Cache.REDIS), + "memcached": Cache(Cache.MEMCACHED), } diff --git a/tests/ut/test_exceptions.py b/tests/ut/test_exceptions.py new file mode 100644 index 00000000..1ed377e5 --- /dev/null +++ b/tests/ut/test_exceptions.py @@ -0,0 +1,5 @@ +from aiocache.exceptions import InvalidCacheType + + +def test_inherit_from_exception(): + assert isinstance(InvalidCacheType(), Exception) diff --git a/tests/ut/test_factory.py b/tests/ut/test_factory.py index 9dc70228..bad0e866 100644 --- a/tests/ut/test_factory.py +++ b/tests/ut/test_factory.py @@ -1,7 +1,9 @@ import pytest +from unittest.mock import patch -from aiocache import SimpleMemoryCache, RedisCache, caches +from aiocache import SimpleMemoryCache, RedisCache, caches, Cache from aiocache.factory import _class_from_string, _create_cache +from aiocache.exceptions import InvalidCacheType from aiocache.serializers import JsonSerializer, PickleSerializer from aiocache.plugins import TimingPlugin, HitMissRatioPlugin @@ -30,6 +32,41 @@ def test_create_cache_with_everything(): assert isinstance(redis.plugins[0], TimingPlugin) +class TestCache: + def test_cache_types(self): + assert Cache.MEMORY == "memory" + assert Cache.REDIS == "redis" + assert Cache.MEMCACHED == "memcached" + + @pytest.mark.parametrize("cache_type", [Cache.MEMORY, Cache.REDIS, Cache.MEMCACHED]) + def test_new(self, cache_type): + kwargs = {"a": 1, "b": 2} + cache_class = Cache.get_protocol_class(cache_type) + + with patch("aiocache.{}.__init__".format(cache_class.__name__)) as init: + cache = Cache(cache_type, **kwargs) + assert isinstance(cache, cache_class) + init.assert_called_once_with(**kwargs) + + def test_new_defaults_to_memory(self): + assert isinstance(Cache(), Cache.get_protocol_class(Cache.MEMORY)) + + def test_new_invalid_cache_raises(self): + with pytest.raises(InvalidCacheType) as e: + Cache("file") + assert str(e.value) == "Invalid cache type, you can only use {}".format( + list(Cache._PROTOCOL_MAPPING.keys()) + ) + + @pytest.mark.parametrize("protocol", [Cache.MEMORY, Cache.REDIS, Cache.MEMCACHED]) + def test_get_protocol_class(self, protocol): + assert Cache.get_protocol_class(protocol) == Cache._PROTOCOL_MAPPING[protocol] + + def test_get_protocol_class_invalid(self): + with pytest.raises(KeyError): + Cache.get_protocol_class("http") + + class TestCacheHandler: @pytest.fixture(autouse=True) def remove_caches(self):