Source code for django_auxilium.utils.functools.cache

from __future__ import print_function, unicode_literals
import abc
import types
from functools import partial, wraps

import six

from .decorators import HybridDecorator


[docs]class NotInCache(Exception): """ Exception for when a value is not present in cache. Primary purpose of the exception is to normalize various exception types from different cache implementations. """
[docs]class BaseCache(six.with_metaclass(abc.ABCMeta, object)): """ Base class for implementing cache implementations Parameters ---------- parent : object A parent object where cache is stored attr : str Name of the attribute under which cache will be stored in the ``parent`` object """ def __init__(self, parent, attr): self.parent = parent self.attr = attr
[docs] def get(self, *args, **kwargs): """ This method must be overwritten by subclasses which should return cache value ``args`` and ``kwargs`` are the parameters passed to the cached function and can be used by the method to extract the cache value from the ``parent`` object When cache value is not found, this method should raise :py:class:`NotInCache` """
[docs] def set(self, value, *args, **kwargs): """ This method must be overwritten by subclasses which should set the cache for the given parameters ``args`` and ``kwargs`` are the parameters passed to the cached function and can be used by the method to correctly store the cache value in the ``parent`` object """
[docs] def delete(self, *args, **kwargs): """ This method must be overwritten by subclasses which should delete cache value for the given parameters ``args`` and ``kwargs`` are the parameters passed to the cached function and can be used by the method to determine which cache value from the ``parent`` object to remove When cache value is not found, this method should raise :py:class:`NotInCache` """
[docs]class Caching(BaseCache): """ Caching implementation which stores single cache value on the parent object The cache is simply stored on the given attribute. When the attribute is present on the ``parent`` object, that indicates that the cache was set and that value is used for cache. When not present, that means cache is missing and hence :py:class:`NotInCache` is raised in most use-cases. """
[docs] def get(self, *args, **kwargs): """ Get the cache value from the ``parent`` object Raises ------ NotInCache When the cache is not set """ try: return getattr(self.parent, self.attr) except AttributeError: raise NotInCache
[docs] def set(self, value, *args, **kwargs): """ Store the cache value on the ``parent`` object """ setattr(self.parent, self.attr, value) return value
[docs] def delete(self, *args, **kwargs): """ Delete the cache value from the ``parent`` object Raises ------ NotInCache When the cache is not set and so cannot be deleted """ try: return self.parent.__dict__.pop(self.attr) except KeyError: raise NotInCache
[docs]class Memoizing(BaseCache): """ Caching implementation which stores single cache value for a set of given parameters on the parent object hence is called memoization The cache is stored on the given attribute as a dictionary. Keys are string representations of the given parameters to the cached function and the values are their corresponding cache values. When the key is in the dictionary, that is used as cache value. Otherwise, in most cases :py:class:`NotInCache` is raised. """ def _get_key(self, *args, **kwargs): return repr(args) + repr(sorted(kwargs.items()))
[docs] def get(self, *args, **kwargs): """ Get the cache value from the ``parent`` object by computing the key from the given parameters Raises ------ NotInCache When the cache is not set """ key = self._get_key(*args, **kwargs) try: return getattr(self.parent, self.attr)[key] except (AttributeError, TypeError, KeyError): raise NotInCache
[docs] def set(self, value, *args, **kwargs): """ Store the cache value on the ``parent`` object for the key as computed for the given parameters """ key = self._get_key(*args, **kwargs) try: store = getattr(self.parent, self.attr) except AttributeError: store = {} setattr(self.parent, self.attr, store) store[key] = value return value
[docs] def delete(self, *args, **kwargs): """ Delete the cache value from the ``parent`` object for the key as computed by the given parameters Raises ------ NotInCache When the cache is not set and so cannot be deleted """ key = self._get_key(*args, **kwargs) try: return getattr(self.parent, self.attr).pop(key) except (AttributeError, TypeError, KeyError): raise NotInCache
[docs]class CacheDescriptor(object): """ Cache descriptor to be used to add instance-level cache to class methods. .. note:: This descriptor could be used independently (and there are even some examples below) however it is meant to be used along with :py:class:`CacheDecorator` which provides much cleaner API. Examples -------- :: >>> def bar(self): ... print('computing') ... return 'bar' >>> class Foo(object): ... foo = CacheDescriptor(bar) >>> f = Foo() >>> print(f.foo()) computing bar >>> print(f.foo()) bar >>> print(f.foo.pop()) bar >>> print(f.foo()) computing bar >>> f.foo.push('another value') >>> print(f.foo()) another value Parameters ---------- method : function Callable which this descriptor is meant to wrap and cache cache_class : type, optional Caching implementation cache which should be used to apply caching. By default :py:attr:`.default_cache_class` is used. as_property : bool, optional Whether to implement the descriptor as a property. By default it is ``False``. .. warning:: This option as ``True`` can only be used with some caching implementations such as :py:class:`Caching`. Other implementations do not suppose this. """ cache_attribute_pattern = '{name}_cache_{hash}' """ String pattern for constructing the cache attribute name under which cache will be stored on the instance. This attribute is meant to be changed in subclasses to customize the functionality. """ default_cache_class = Caching """ Cache implementation class which will be used for the caching. This attribute is meant to be changed in subclasses to customize the functionality. """ def __init__(self, method, cache_class=None, as_property=False): self.method = method self.cache_attribute = self.cache_attribute_pattern.format( name=method.__name__, hash=abs(hash(method.__name__)), ) self.cache_class = cache_class or self.default_cache_class self.as_property = as_property
[docs] def get_cache(self, instance): """ Helper method which given returns cache implementation instance for the given instance with given parameters """ return self.cache_class(instance, self.cache_attribute)
[docs] def getter(self, instance, *args, **kwargs): """ Wrapper method around the decorator-wrapped callable (``method`` parameter) which returns cache value when available or otherwise computes and stores the value in cache by evaluating wrapped method """ cache = self.get_cache(instance) try: return cache.get(*args, **kwargs) except NotInCache: return cache.set(self.method(instance, *args, **kwargs), *args, **kwargs)
[docs] def pop(self, instance, *args, **kwargs): """ Method for popping cache value corresponding to the given parameters from the instance as implemented by the caching implementation """ cache = self.get_cache(instance) return cache.delete(*args, **kwargs)
[docs] def push(self, instance, value, *args, **kwargs): """ Method for setting custom cache value given function parameters """ cache = self.get_cache(instance) cache.set(value, *args, **kwargs)
def _wrap(self, wrapping, proxy, instance): f = types.MethodType(proxy, instance) return wraps(wrapping)(partial(f)) def __get__(self, instance, owner): if self.as_property: if instance is None: return self else: return self.getter(instance) else: if instance is None: return self.method else: f = self._wrap(self.method, self.getter, instance) f.pop = self._wrap(self.__class__.pop, self.pop, instance) f.push = self._wrap(self.__class__.push, self.push, instance) return f def __set__(self, instance, value): if self.as_property: cache = self.get_cache(instance) # no args and kwargs since this is only for case when cache is used as property return cache.set(value) # required to be overwritten for pypy # see https://bitbucket.org/pypy/pypy/issues/2033/attributeerror-object-attribute-is-read # even though ticket is resolved its still affecting pypy3 raise AttributeError def __delete__(self, instance): if self.as_property: self.pop(instance) else: raise AttributeError
[docs]class MemoizeDescriptor(CacheDescriptor): """ Cache descriptor to be used to add instance-level cache to class methods which considers method parameters when caching values. In other words, this descriptor caches method results per given parameters. .. note:: This descriptor could be used independently (and there are even some examples below) however it is meant to be used along with :py:class:`MemoizeDecorator` which provides much cleaner API. Examples -------- :: >>> def bar(self, x): ... print('computing for', x) ... return x + 'bar' >>> class Foo(object): ... foo = MemoizeDescriptor(bar) >>> f = Foo() >>> print(f.foo('foo')) computing for foo foobar >>> print(f.foo('awesome')) computing for awesome awesomebar >>> print(f.foo('foo')) foobar >>> print(f.foo.pop('foo')) foobar >>> print(f.foo('foo')) computing for foo foobar >>> print(f.foo('awesome')) awesomebar """ cache_attribute_pattern = '{name}_memoize_{hash}' """ String pattern for constructing the cache attribute name under which cache will be stored on the instance. This attribute is meant to be changed in subclasses to customize the functionality. """ default_cache_class = Memoizing """ Cache implementation class which will be used for the caching. This attribute is meant to be changed in subclasses to customize the functionality. """
[docs]class BaseCacheDecorator(HybridDecorator): """ Base decorator for caching callables so that they only execute once and all subsequent calls return cached value This is very useful for expensive functions """ cache_descriptor_class = None """ Descriptor class to be used when caching is applied to class methods. This attribute is meant to be changed in subclasses. """ cache_class = Caching """ Caching implementation to use when wrapping standalone functions. This attribute is meant to be changed in subclasses. """
[docs] def get_cache_descriptor(self): """ Hook for instantiating cache descriptor class """ return self.cache_descriptor_class(self.to_wrap)
[docs] def get_wrapped_object(self): """ Main method for wrapping the given object. This method has separate code paths for the following scenarios: :class method: When wrapping class method descriptor is returned as created by :py:meth:`get_cache_descriptor`. That descriptor is then responsible for maintaining cache state for the class instance. :function: When wrapping standalone function, this method returns a wrapping function which caches the value on this decorator instance. That means that the cache becomes global since functions in Python are singletons. """ to_wrap = super(BaseCacheDecorator, self).get_wrapped_object() if self.in_class: return self.get_cache_descriptor() else: self.cache = self.cache_class(self, 'cached_value') def wrapper(*args, **kwargs): try: return self.cache.get(*args, **kwargs) except NotInCache: return self.cache.set(to_wrap(*args, **kwargs), *args, **kwargs) wrapper.pop = self.pop wrapper.decorator = self return wrapper
[docs] def pop(self, *args, **kwargs): """ Method for popping cache value corresponding to the given parameters """ return self.cache.delete(*args, **kwargs)
[docs]class CacheDecorator(BaseCacheDecorator): """ Decorator for caching callables so that they only execute once and all subsequent calls return cached value. .. note:: This caching decorator does not account function parameters to cache values. If you need to cache different calls per given parameters, please look at :py:class:`MemoizeDecorator` Examples -------- When caching standalone functions:: >>> @CacheDecorator.as_decorator() ... def compute(): ... print('computing here') ... return 'foo' >>> print(compute()) computing here foo >>> print(compute()) foo >>> print(compute.pop()) foo >>> print(compute()) computing here foo When caching class methods:: >>> class Foo(object): ... @CacheDecorator.as_decorator() ... def foo(self): ... print('computing here') ... return 'foo' >>> f = Foo() >>> print(f.foo()) computing here foo >>> print(f.foo()) foo >>> print(f.foo.pop()) foo >>> print(f.foo()) computing here foo Parameters ---------- as_property : bool Boolean whether to create a property descriptor when using it on a class method .. warning:: This is only meant to be used when the wrapping method does not accept any parameters since there is no way in Python to pass parameters to properties """ cache_descriptor_class = CacheDescriptor """ Descriptor class to be used when caching is applied to class methods """ cache_class = Caching """ Caching implementation to use when wrapping standalone functions. """ def __init__(self, as_property=False, *args, **kwargs): self.as_property = as_property super(CacheDecorator, self).__init__(*args, **kwargs)
[docs] def get_cache_descriptor(self): """ Custom implementation for getting the cache descriptor which allows to use ``as_property`` parameter """ return self.cache_descriptor_class( self.to_wrap, as_property=self.as_property, )
[docs]class MemoizeDecorator(BaseCacheDecorator): """ Decorator for memoizing functions so that they only execute once and all subsequent calls with same parameters This is very useful for expensive functions which accept parameters Examples -------- When caching standalone functions:: >>> @MemoizeDecorator.as_decorator() ... def compute(x): ... print('computing for', x) ... return x + 'foo' >>> print(compute('bar')) computing for bar barfoo >>> print(compute('awesome')) computing for awesome awesomefoo >>> print(compute('bar')) barfoo >>> print(compute.pop('bar')) barfoo >>> print(compute('bar')) computing for bar barfoo >>> print(compute('awesome')) awesomefoo When caching class methods:: >>> class Foo(object): ... @MemoizeDecorator.as_decorator() ... def foo(self, x): ... print('computing for', x) ... return x + 'foo' >>> f = Foo() >>> print(f.foo('bar')) computing for bar barfoo >>> print(f.foo('awesome')) computing for awesome awesomefoo >>> print(f.foo('bar')) barfoo >>> print(f.foo('awesome')) awesomefoo >>> print(f.foo.pop('bar')) barfoo >>> print(f.foo('bar')) computing for bar barfoo >>> print(f.foo('awesome')) awesomefoo """ cache_descriptor_class = MemoizeDescriptor """ Descriptor class to be used when caching is applied to class methods """ cache_class = Memoizing """ Caching implementation to use when wrapping standalone functions. """
cache = CacheDecorator.as_decorator() memoize = MemoizeDecorator.as_decorator() cache_property = CacheDecorator.as_decorator(as_property=True) """ Shortcut for :py:data:`cache` which automatically creates properties when used on class methods. These are equivalent:: @cache(as_property=True) def foo(self): pass @cache_property def bar(self): pass """ cache_method = CacheDecorator.as_decorator(is_method=True) """ Shortcut for :py:data:`cache` which automatically indicates that the cached object is a method. These are equivalent:: class Foo(object): @cache(is_method=True) def foo(self): pass class Foo(object): @cache_method def bar(self): pass """