Source code for fictive.cache.memoize

"""
fictive.cache.memocache

Object wrapper for memoizing using a cache

"""

import functools
import hashlib
import inspect
import json
import types
import weakref


import wrapt


[docs]class memoize(object): # pylint: disable=invalid-name """ Decorator that memoizes the decorated `callable` using caches """ # pylint: disable=too-few-public-methods
[docs] def __init__(self, *, cache_factory, alias=None, additional_key=None): """ :param callable cache_factory: a callable that will return a suitable cache when called with a string key :param aliased: a list of parameter names (to the decorated `callable`) that will be ignored when generating the unique string key for a memo cache. I.e., calls to the decorated `callable` that differ only in "aliased" parameters will map to the same cache value. This can be useful for e.g. ignoring `self`, temporary tokens, or other transient / insignificant parameters when accessing the cache(s). :param additional_key: a function for furster specifying the cachening key (i.e, used to determine whether to use the memoized value or call the function). **MUST** take this instance as a first positional argument, the wrapped function as a second positional argument, and then any positional or keyword arguments that will be provided to the decorated function. The callable should return a string-like representation that encapsulates all of the additional attributes that would distinguish cached results. E.g.:: :code:`def additional_key(memoize_instance, wrapped_function, *args, **kwargs) -> str` """ self.cache_factory = cache_factory self.aliased = alias or () self.cache_map = dict() if additional_key: self._additional_key = types.MethodType(additional_key, self) # pylint: disable=invalid-name self.MEMO_KEY_PARTS += ('_additional_key',)
def __call__(self, func): """ Decorate `func` so that calls to `func` are memoized When the decorated function is called, it will use the bound parameters to create a key and use that key index a value cache. If a cahced value for the key exists, that value is immediately returned (rather than calling the decorated function). Otherwise, the decorated function is called and the result is cached and returned. """ @functools.wraps(func) def decorated(*args, **kwargs): """ Use remote caches as memoization stores """ key = self.memo_key(func, *args, **kwargs) cache = self.get_cache(key) cache.fetch_impl = lambda cache_: func(*args, **kwargs) return cache.value return decorated
[docs] def get_cache(self, key): """ Retrieve the memo cache for `func` based on the `key` """ try: return self.cache_map[key] except KeyError: self.cache_map[key] = self.cache_factory(key) return self.cache_map[key]
#: the methods that will be used to assemble the memoization key MEMO_KEY_PARTS = ( '_func_memo_key', '_args_memo_key', )
[docs] def memo_key(self, func, *args, **kwargs): """ Generate a hash key using serialized invocation details """ full_key = '|'.join( hashlib.sha1( getattr(self, key_func_name)(func, *args, **kwargs).encode('utf-8') ).hexdigest() for key_func_name in self.MEMO_KEY_PARTS ) return hashlib.sha1(full_key.encode('utf-8')).hexdigest()
@classmethod def _func_memo_key(cls, func, *args, **kwargs): """ Serialized function information """ return "{file}:{line}::{func}".format( file=func.__code__.co_filename, line=func.__code__.co_firstlineno, func=func.__name__, ) def _args_memo_key(self, func, *args, **kwargs): """ Serialized function argument information """ def without_aliased(dictionary): """ remove aliased entries from dictionary """ return type(dictionary)( (name, value) for (name, value) in dictionary.items() if name not in self.aliased ) signature = inspect.signature(func) bound_arguments = signature.bind(*args, **kwargs) bound_arguments.apply_defaults() if not self.aliased: # return a key with all parameter values return json.dumps(bound_arguments.arguments, default=str, sort_keys=True) # otherwise, remove aliased parameters bound_arguments.arguments = without_aliased(bound_arguments.arguments) # and remove any aliased **kwarg values var_keyword_gen = ( parameter for parameter in signature.parameters.items() if parameter[1].kind == inspect.Parameter.VAR_KEYWORD ) var_keyword_param = next(var_keyword_gen, None) if var_keyword_param: name = var_keyword_param[0] bound_arguments.arguments[name] = without_aliased(bound_arguments.arguments[name]) return json.dumps(bound_arguments.arguments, default=str, sort_keys=True)
[docs]class MemoizeProxy(wrapt.ObjectProxy): """ An object proxy wrapper that memoizes an object's method calls """ # pylint: disable=too-few-public-methods # pylint: disable=abstract-method
[docs] def __init__(self, wrapped, memoizing_decorator, only=None, exclude=None): """ :param wrapped: the object that will have its routines memoized :param memoizing_decorator: the decorator that will be used to memoize routines :param only: if set, routiens will only be memoized if their name is in `only` :param exclude: any routines with thei rname in `exclude will not be memoized """ super(MemoizeProxy, self).__init__(wrapped) self._self_memoizing_decorator = memoizing_decorator self._self_only = only self._self_exclude = exclude self._self_decorated = dict()
def __getattribute__(self, name): """ return a memoized version of appropriate routine attributes """ _self_ = super(MemoizeProxy, self).__getattribute__ if name in _self_('_self_decorated'): # already memoized; return the memoized routine return _self_('_self_decorated')[name] default = _self_(name) if not inspect.isroutine(default): # don't memoize non-routine attributes return default if _self_('_self_exclude') and name in _self_('_self_exclude'): # don't memoize excluded attributes return default if _self_('_self_only') and name not in _self_('_self_only'): # don't memoize attributes not in only (if set) return default memoizing_decorator = _self_('_self_memoizing_decorator') if isinstance(default, types.MethodType): # memoize the *unbound* method memoized = types.MethodType( memoizing_decorator(default.__func__), weakref.proxy(default.__self__), ) else: memoized = memoizing_decorator(default) _self_('_self_decorated')[name] = memoized return _self_('_self_decorated')[name]