From ab2519ea719deca877e5ca5f260a43539d1f382b Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Mon, 11 Jun 2018 23:52:14 +0900 Subject: [PATCH] Separate Ring & Wire - Ring is now based on WireRope --- ring/callable.py | 10 +- ring/django.py | 16 +-- ring/func_asyncio.py | 23 ++-- ring/func_base.py | 213 ++++++++++++++++++------------------ ring/func_sync.py | 4 +- ring/wire.py | 189 +++++++++++++++++--------------- setup.py | 3 +- tests/_test_func_asyncio.py | 2 +- tests/test_func_sync.py | 2 +- tests/test_wire.py | 45 ++++---- 10 files changed, 261 insertions(+), 246 deletions(-) diff --git a/ring/callable.py b/ring/callable.py index b360114..8de98c1 100644 --- a/ring/callable.py +++ b/ring/callable.py @@ -14,7 +14,7 @@ class Callable(object): - """A wrapper of :class:`inspect.Signature` including more information of callable.""" + """A wrapper object including more information of callables.""" def __init__(self, f): self.wrapped_object = f @@ -72,8 +72,8 @@ def is_method(self): :param ring.callable.Callable c: A callable object. :rtype: bool - :note: The test is not based on python state but based on parameter name - `self`. The test result might be wrong. + :note: The test is not based on python state but based on parameter + name `self`. The test result might be wrong. """ if six.PY34: if self.is_barefunction: @@ -193,8 +193,8 @@ def kwargify(self, args, kwargs, bound_args=()): @cached_property def identifier(self): - return '.'.join( - (self.wrapped_callable.__module__, qualname(self.wrapped_callable))) + return '.'.join(( + self.wrapped_callable.__module__, qualname(self.wrapped_callable))) @cached_property def first_parameter(self): diff --git a/ring/django.py b/ring/django.py index 269b5c1..c2957f8 100644 --- a/ring/django.py +++ b/ring/django.py @@ -120,9 +120,9 @@ def get(self, wire, request, *args, **kwargs): return result # no 'precess_view' in CacheMiddleware # if hasattr(middleware, 'process_view'): - # result = middleware.process_view(request, view_func, args, kwargs) - # if result is not None: - # return result + # result = middleware.process_view(request, view_func, args, kwargs) + # if result is not None: + # return result return self.ring.miss_value @fbase.interface_attrs( @@ -204,7 +204,7 @@ def cache( .. _`Django's cache framework: Setting up the cache`: https://docs.djangoproject.com/en/2.0/topics/cache/#setting-up-the-cache .. _`Django's cache framework: The low-level cache API`: https://docs.djangoproject.com/en/2.0/topics/cache/#the-low-level-cache-api - """ + """ # noqa backend = promote_backend(backend) return fbase.factory( backend, key_prefix=key_prefix, on_manufactured=None, @@ -216,10 +216,12 @@ def cache( def cache_page( timeout, cache=None, key_prefix=None, # original parameters - user_interface=CachePageUserInterface, storage_class=fbase.BaseStorage): + user_interface=CachePageUserInterface, + storage_class=fbase.BaseStorage): """The drop-in-replacement of Django's per-view cache. - Use this decorator instead of :func:`django.views.decorators.cache.cache_page`. + Use this decorator instead of + :func:`django.views.decorators.cache.cache_page`. The decorated view function itself is compatible. Ring decorated function additionally have ring-styled sub-functions. In the common cases, `delete` and `update` are helpful. @@ -267,7 +269,7 @@ def article_post_django(request): :see: `Django's cache framework: The per-view cache `_ :see: :func:`django.views.decorators.cache.cache_page`. - """ + """ # noqa middleware_class = CacheMiddleware middleware = middleware_class( cache_timeout=timeout, cache_alias=cache, key_prefix=key_prefix) diff --git a/ring/func_asyncio.py b/ring/func_asyncio.py index 0347d4c..6b52939 100644 --- a/ring/func_asyncio.py +++ b/ring/func_asyncio.py @@ -1,4 +1,4 @@ -""":mod:`ring.func_asyncio` --- a collection of :mod:`asyncio` factory functions. +""":mod:`ring.func_asyncio` --- collection of :mod:`asyncio` factory functions. This module includes building blocks and storage implementations of **Ring** factories for :mod:`asyncio`. @@ -18,8 +18,8 @@ type_dict = dict -def factory_doctor(wire_frame, ring) -> None: - callable = ring.callable +def factory_doctor(wire_rope) -> None: + callable = wire_rope.callable if not callable.is_coroutine: raise TypeError( "The function for cache '{}' must be an async function.".format( @@ -206,7 +206,7 @@ def get_many(self, keys, miss_value): """Get and return values for the given key.""" values = yield from self.get_many_values(keys) results = [ - self.ring.coder.decode(v) if v is not fbase.NotFound else miss_value + self.ring.coder.decode(v) if v is not fbase.NotFound else miss_value # noqa for v in values] return results @@ -315,7 +315,8 @@ def delete_many_values(self, keys): raise NotImplementedError("aiomcache doesn't support delete_multi.") -class AioredisStorage(CommonMixinStorage, fbase.StorageMixin, BulkStorageMixin): +class AioredisStorage( + CommonMixinStorage, fbase.StorageMixin, BulkStorageMixin): """Storage implementation for :class:`aioredis.Redis`.""" @asyncio.coroutine @@ -403,12 +404,12 @@ def aiomcache( Expected client package is aiomcache_. aiomcache expect `Memcached` client or dev package is installed on your - machine. If you are new to Memcached, check how to install it and the python - package on your platform. + machine. If you are new to Memcached, check how to install it and the + python package on your platform. :param aiomcache.Client client: aiomcache client object. - :param object key_refactor: The default key refactor may hash the cache key - when it doesn't meet memcached key restriction. + :param object key_refactor: The default key refactor may hash the cache + key when it doesn't meet memcached key restriction. :see: :func:`ring.func_asyncio.CacheUserInterface` for single access sub-functions. @@ -442,8 +443,8 @@ def aioredis( Expected client package is aioredis_. aioredis expect `Redis` client or dev package is installed on your - machine. If you are new to Memcached, check how to install it and the python - package on your platform. + machine. If you are new to Memcached, check how to install it and the + python package on your platform. Note that aioredis>=1.0.0 only supported. diff --git a/ring/func_base.py b/ring/func_base.py index cdeb282..028cce7 100644 --- a/ring/func_base.py +++ b/ring/func_base.py @@ -10,19 +10,14 @@ from ._compat import functools from .callable import Callable from .key import CallableKey -from .wire import Wire +from .wire import Wire, WireRope, RopeCore from .coder import registry as default_registry __all__ = ( - 'BaseRing', 'factory', 'NotFound', + 'factory', 'NotFound', 'BaseUserInterface', 'BaseStorage', 'CommonMixinStorage', 'StorageMixin') -@six.add_metaclass(abc.ABCMeta) -class BaseRing(object): - """Abstract principal root class of Ring classes.""" - - def suggest_ignorable_keys(c, ignorable_keys): if ignorable_keys is None: _ignorable_keys = [] @@ -126,7 +121,8 @@ def compose_key(bound_args, kwargs): for i, prearg in enumerate(bound_args): full_kwargs[c.parameters[i].name] = bound_args[i] coerced_kwargs = { - k: coerce(v) for k, v in full_kwargs.items() if k not in ignorable_keys} + k: coerce(v) for k, v in full_kwargs.items() + if k not in ignorable_keys} key = key_generator.build(coerced_kwargs) if encoding: key = key.encode(encoding) @@ -489,6 +485,77 @@ def touch_many(self, wire, *args_list): # pragma: no cover raise NotImplementedError +class RingWire(Wire): + + def __init__(self, rope, *args, **kwargs): + super(RingWire, self).__init__(rope, *args, **kwargs) + + self.encode = rope.coder.encode + self.decode = rope.coder.decode + self.storage = rope.storage + + def __getattr__(self, name): + try: + return super(RingWire, self).__getattr__(name) + except AttributeError: + pass + try: + return self.__getattribute__(name) + except AttributeError: + pass + + attr = getattr(self._rope.user_interface, name) + if callable(attr): + transform_args = getattr( + attr, 'transform_args', None) + + def impl_f(*args, **kwargs): + if transform_args: + transform_func, transform_rules = transform_args + args, kwargs = transform_func( + self, transform_rules, args, kwargs) + return attr(self, *args, **kwargs) + + cc = self._callable.wrapped_callable + functools.wraps(cc)(impl_f) + impl_f.__name__ = '.'.join((cc.__name__, name)) + if six.PY34: + impl_f.__qualname__ = '.'.join((cc.__qualname__, name)) + + annotations = getattr( + impl_f, '__annotations__', {}) + annotations_override = getattr( + attr, '__annotations_override__', {}) + for field, override in annotations_override.items(): + if isinstance(override, types.FunctionType): + new_annotation = override(annotations) + else: + new_annotation = override + annotations[field] = new_annotation + + setattr(self, name, impl_f) + + return self.__getattribute__(name) + + def _merge_args(self, args, kwargs): + """Create a fake kwargs object by merging actual arguments. + + The merging follows the signature of wrapped function and current + instance. + """ + if type(self.__func__) is types.MethodType: # noqa + bound_args = range(len(self._bound_objects)) + else: + bound_args = () + full_kwargs = self._callable.kwargify( + args, kwargs, bound_args=bound_args) + return full_kwargs + + def run(self, action, *args, **kwargs): + attr = getattr(self, action) + return attr(*args, **kwargs) + + def factory( storage_backend, # actual storage key_prefix, # manual key prefix @@ -542,122 +609,58 @@ def factory( :data:`None`; Otherwise it is omitted. :return: The factory decorator to create new ring wire or wire bridge. - :rtype: (Callable)->Union[ring.wire.Wire,ring.wire.WiredProperty] + :rtype: (Callable)->ring.wire.RopeCore """ if coder_registry is None: coder_registry = default_registry raw_coder = coder - coder = coder_registry.get_or_coderize(raw_coder) + ring_coder = coder_registry.get_or_coderize(raw_coder) if isinstance(user_interface, (tuple, list)): user_interface = type('_ComposedUserInterface', user_interface, {}) def _decorator(f): - cw = Callable(f) - _ignorable_keys = suggest_ignorable_keys(cw, ignorable_keys) - _key_prefix = suggest_key_prefix(cw, key_prefix) - key_builder = create_key_builder( - cw, _key_prefix, _ignorable_keys, - encoding=key_encoding, key_refactor=key_refactor) - - class RingCore(BaseRing): - callable = cw - compose_key = staticmethod(key_builder) - def __init__(self): - super(BaseRing, self).__init__() - self.user_interface = user_interface(self) - self.storage = storage_class(self, storage_backend) + _storage_class = storage_class - RingCore.miss_value = miss_value - RingCore.expire_default = expire_default - RingCore.coder = coder + class RingCore(RopeCore): - ring = RingCore() - - class RingWire(Wire): + coder = ring_coder + user_interface_class = user_interface + storage_class = _storage_class def __init__(self, *args, **kwargs): - super(RingWire, self).__init__(*args, **kwargs) - self._ring = ring - - self.encode = ring.coder.encode - self.decode = ring.coder.decode - self.storage = ring.storage - - if default_action is not None: - @functools.wraps(ring.callable.wrapped_callable) + super(RingCore, self).__init__(*args, **kwargs) + self.user_interface = self.user_interface_class(self) + self.storage = self.storage_class(self, storage_backend) + + _ignorable_keys = suggest_ignorable_keys( + self.callable, ignorable_keys) + _key_prefix = suggest_key_prefix(self.callable, key_prefix) + self.compose_key = create_key_builder( + self.callable, _key_prefix, _ignorable_keys, + encoding=key_encoding, key_refactor=key_refactor) + + if default_action is not None: + func = f if type(f) is types.FunctionType else f.__func__ # noqa + + class _RingWire(RingWire): + @functools.wraps(func) def __call__(self, *args, **kwargs): - return self.run(default_action, *args, **kwargs) - else: # Empty block to test coverage - pass - - def __getattr__(self, name): - try: - return super(RingWire, self).__getattr__(name) - except AttributeError: - pass - try: - return self.__getattribute__(name) - except AttributeError: - pass - - attr = getattr(self._ring.user_interface, name) - if callable(attr): - transform_args = getattr( - attr, 'transform_args', None) - - def impl_f(*args, **kwargs): - if transform_args: - transform_func, transform_rules = transform_args - args, kwargs = transform_func( - self, transform_rules, args, kwargs) - return attr(self, *args, **kwargs) - - cc = self._callable.wrapped_callable - functools.wraps(cc)(impl_f) - impl_f.__name__ = '.'.join((cc.__name__, name)) - if six.PY34: - impl_f.__qualname__ = '.'.join((cc.__qualname__, name)) - - annotations = getattr( - impl_f, '__annotations__', {}) - annotations_override = getattr( - attr, '__annotations_override__', {}) - for field, override in annotations_override.items(): - if isinstance(override, types.FunctionType): - new_annotation = override(annotations) - else: - new_annotation = override - annotations[field] = new_annotation - - setattr(self, name, impl_f) - - return self.__getattribute__(name) - - def _merge_args(self, args, kwargs): - """Create a fake kwargs object by merging actual arguments. - - The merging follows the signature of wrapped function and current - instance. - """ - if type(self.__func__) is types.MethodType: # noqa - bound_args = range(len(self._bound_objects)) - else: - bound_args = () - full_kwargs = self._callable.kwargify( - args, kwargs, bound_args=bound_args) - return full_kwargs + return self.run(self._rope.default_action, *args, **kwargs) + else: + _RingWire = RingWire - def run(self, action, *args, **kwargs): - attr = getattr(self, action) - return attr(*args, **kwargs) + wire_rope = WireRope(_RingWire, RingCore) + strand = wire_rope(f) + strand.miss_value = miss_value + strand.expire_default = expire_default + strand.default_action = default_action - wire = RingWire.for_callable(ring.callable) if on_manufactured is not None: - on_manufactured(wire_frame=wire, ring=ring) + on_manufactured(wire_rope=strand) - return wire + return strand return _decorator diff --git a/ring/func_sync.py b/ring/func_sync.py index 90cf686..1ba32b6 100644 --- a/ring/func_sync.py +++ b/ring/func_sync.py @@ -1,4 +1,4 @@ -""":mod:`ring.func_sync` --- a collection of factory functions. +""":mod:`ring.func_sync` --- collection of factory functions. This module includes building blocks and storage implementations of **Ring** factories. @@ -152,7 +152,7 @@ class BulkStorageMixin(object): def get_many(self, keys, miss_value): values = self.get_many_values(keys) results = [ - self.ring.coder.decode(v) if v is not fbase.NotFound else miss_value + self.ring.coder.decode(v) if v is not fbase.NotFound else miss_value # noqa for v in values] return results diff --git a/ring/wire.py b/ring/wire.py index ca2039c..0decca6 100644 --- a/ring/wire.py +++ b/ring/wire.py @@ -1,6 +1,7 @@ """:mod:`ring.wire` --- Universal method/function wrapper. ========================================================== """ +from .callable import Callable from ._compat import functools from ._util import cached_property @@ -16,82 +17,25 @@ def descriptor_bind_(descriptor, obj, type): return type -class WiredProperty(object): - """Wire-friendly property to create method wrapper for each instance. - - When the property is wrapping a method or a class method, - :class:`ring.wire.Wire` object will be created for each distinguishable - owner instance and class. - """ - - def __init__(self, func): - self.__func__ = func - - def __get__(self, obj, type=None): - return self.__func__(obj, type) - - def _add_function(self, key): - def _decorator(f): - self._dynamic_attrs[key] = f - return f - return _decorator - - class Wire(object): - """The universal method/function wrapper. + """The core data object for each function for bound method. + + Inherit this class to implement your own Wire classes. - For normal functions, each function is directly wrapped by **Wire**. - - For methods, each method is wrapped by :class:`ring.wire.WiredProperty` - and it creates **Wire** object for each instance. - - For class methods, each class method is wrapped by - :class:`ring.wire.WiredProperty` and it creates **Wire** object for - each subclass. - - :note: DO NOT manually instantiate a **Wire** object. That's not what - you want to do. - :see: :meth:`ring.wire.Wire.for_callable` for actual wrapper function. + - For any methods or descriptors (including classmethod, staticmethod), + each one is wrapped by :class:`ring.wire.MethodRopeMixin` + and it creates **Wire** object for each bound object. """ - @classmethod - def for_callable(cls, cw): - """Wrap a function/method definition. - - :return: Wrapper object. The return type is up to given callable is - function or method. - :rtype: ring.wire.Wire or ring.wire.WiredProperty - """ - _shared_attrs = {'attrs': {}} - - if not cw.is_barefunction: - co = cw.wrapped_object - - def __w(obj, type): - owner = descriptor_bind(co, obj, type) - if owner is None: # invalid binding but still wire it - owner = obj if obj is not None else type - wrapper_name_parts = ['__wire_', cw.wrapped_callable.__name__] - if owner is type: - wrapper_name_parts.extend(('_', type.__name__)) - wrapper_name = ''.join(wrapper_name_parts) - wrapper = getattr(owner, wrapper_name, None) - if wrapper is None: - boundmethod = co.__get__(obj, type) - wire = cls(cw, (obj, type), _shared_attrs) - wrapper = functools.wraps(boundmethod)(wire) - setattr(owner, wrapper_name, wrapper) - wire._shared_attrs = _shared_attrs - return wrapper - - _w = WiredProperty(__w) - _w._dynamic_attrs = _shared_attrs['attrs'] + def __init__(self, rope, binding): + self._rope = rope + self._callable = rope.callable + self._binding = binding + if binding: + self.__func__ = self._callable.wrapped_object.__get__(*binding) else: - _w = cls(cw, None, _shared_attrs) - - _w._callable = cw - _w._shared_attrs = _shared_attrs - - functools.wraps(cw.wrapped_callable)(_w) - return _w + self.__func__ = self._callable.wrapped_object @cached_property def _bound_objects(self): @@ -99,35 +43,100 @@ def _bound_objects(self): return () else: return (descriptor_bind( - self._callable.wrapped_object, *self._binding), ) + self._callable.wrapped_object, *self._binding),) + + +class RopeCore(object): + + def __init__(self, callable, rope): + super(RopeCore, self).__init__() + self.callable = callable + self.rope = rope + self.wire_class = rope.wire_class - def __init__(self, callable, binding, shared_attrs): - self._callable = callable - self._binding = binding - if binding: - self.__func__ = callable.wrapped_object.__get__(*binding) - else: - self.__func__ = callable.wrapped_object - self._shared_attrs = shared_attrs - @property - def _dynamic_attrs(self): - return self._shared_attrs.get('attrs', ()) +class MethodRopeMixin(object): + + def __init__(self, *args, **kwargs): + super(MethodRopeMixin, self).__init__(*args, **kwargs) + assert not self.callable.is_barefunction + + def __get__(self, obj, type=None): + cw = self.callable + co = cw.wrapped_object + owner = descriptor_bind(co, obj, type) + if owner is None: # invalid binding but still wire it + owner = obj if obj is not None else type + wrapper_name_parts = ['__wire_', cw.wrapped_callable.__name__] + if owner is type: + wrapper_name_parts.extend(('_', type.__name__)) + wrapper_name = ''.join(wrapper_name_parts) + wrapper = getattr(owner, wrapper_name, None) + if wrapper is None: + boundmethod = co.__get__(obj, type) + wire = self.wire_class(self, (obj, type)) + wrapper = functools.wraps(boundmethod)(wire) + setattr(owner, wrapper_name, wrapper) + return wrapper + + +class FunctionRopeMixin(object): + + def __init__(self, *args, **kwargs): + super(FunctionRopeMixin, self).__init__(*args, **kwargs) + assert self.callable.is_barefunction + boundmethod = self.callable.wrapped_object + wire = self.wire_class(self, None) + self._wire = functools.wraps(boundmethod)(wire) def __getattr__(self, name): try: return self.__getattribute__(name) except AttributeError: pass + return getattr(self._wire, name) + - if name in self._dynamic_attrs: - attr = self._dynamic_attrs.get(name) - if self._binding: - attr = attr.__get__(*self._binding) +class CallableRopeMixin(object): - def impl_f(*args, **kwargs): - return attr(self, *args, **kwargs) + def __init__(self, *args, **kwargs): + super(CallableRopeMixin, self).__init__(*args, **kwargs) + self.__call__ = functools.wraps(self.callable.wrapped_object)(self) - setattr(self, name, impl_f) + def __call__(self, *args, **kwargs): + return self._wire(*args, **kwargs) - return self.__getattribute__(name) + +class WireRope(object): + + def __init__(self, wire_class, core_class=RopeCore): + if isinstance(core_class, tuple): + core_classes = core_class + else: + core_classes = (core_class,) + + self.wire_class = wire_class + self.method_rope = type( + '_MethodRope', (MethodRopeMixin,) + core_classes, {}) + self.function_rope = type( + '_FunctionRope', (FunctionRopeMixin,) + core_classes, {}) + self.callable_function_rope = type( + '_CallableFunctionRope', + (CallableRopeMixin, FunctionRopeMixin,) + core_classes, {}) + + def __call__(self, function): + """Wrap a function/method definition. + + :return: Wrapper object. The return type is up to given callable is + function or method. + """ + wrapper = Callable(function) + if wrapper.is_barefunction: + if hasattr(self.wire_class, '__call__'): + rope_class = self.callable_function_rope + else: + rope_class = self.function_rope + else: + rope_class = self.method_rope + rope = rope_class(wrapper, rope=self) + return rope diff --git a/setup.py b/setup.py index 4d13878..a899c11 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,8 @@ def get_readme(): setup( name='ring', version=get_version(), - description='Shift cache paradigm to code and forget about storages. With built-in memcache & redis + asyncio support.', + description='Function-oriented cache interface with built-in memcache ' + '& redis + asyncio support.', long_description=get_readme(), author='Jeong YunWon', author_email='ring@youknowone.org', diff --git a/tests/_test_func_asyncio.py b/tests/_test_func_asyncio.py index 466717e..a692085 100644 --- a/tests/_test_func_asyncio.py +++ b/tests/_test_func_asyncio.py @@ -174,7 +174,7 @@ def f2(a, b): yield from f2(1, 2) yield from f2(1, 2) - f2._ring.storage.now = lambda: time.time() + 100 # expirable duration + f2._rope.storage.now = lambda: time.time() + 100 # expirable duration assert ((yield from f2.get(1, 2))) is None diff --git a/tests/test_func_sync.py b/tests/test_func_sync.py index 13a628d..5b78eb8 100644 --- a/tests/test_func_sync.py +++ b/tests/test_func_sync.py @@ -259,7 +259,7 @@ def f(a, b): assert 30102 == f.update(1, b=2) f.touch(1, b=2) - f._ring.storage.now = lambda: time.time() + 100 # expirable duration + f._rope.storage.now = lambda: time.time() + 100 # expirable duration assert f.get(1, b=2) is None diff --git a/tests/test_wire.py b/tests/test_wire.py index 9916a4d..1624de2 100644 --- a/tests/test_wire.py +++ b/tests/test_wire.py @@ -1,43 +1,42 @@ -from ring.wire import Wire -from ring.key import Callable +from ring.wire import Wire, WireRope def test_wire(): class TestWire(Wire): - pass + x = 7 - def wrapper(f): - c = Callable(f) - w = TestWire.for_callable(c) - return w + def y(self): + return self._bound_objects[0].v + + class CallableWire(Wire): + def __call__(self): + return self._bound_objects[0].v + + test_rope = WireRope(TestWire) + callable_rope = WireRope(CallableWire) class A(object): def __init__(self, v): self.v = v - @wrapper + @test_rope def f(self): return self.v - @f._add_function('call') - def f_call(self, wire): - return wire.__func__() - - @f._add_function('key') - def f_key(self, wire): - return 'key' + @callable_rope + def g(self): + return self.v a = A(10) - assert a.f.call() == 10 - assert a.f.key() == 'key' - b = A(20) - assert a.f.call() == 10, (a.f, a.f.call()) - assert b.f.call() == 20, (b.f, b.f.call()) - assert isinstance(A.f, TestWire) - assert isinstance(a.f, TestWire) - assert A.f is not a.f + assert a.f.x == 7 + assert a.f.y() == 10 + assert b.f.y() == 20 + assert not callable(a.f) + assert a.g() == 10 + assert b.g() == 20 + assert callable(a.g)