from copy import copy from django.core.cache import cache class Cacheable(object): '''A higher level API for caching results of callables. This class provides a simple API for caching and retrieving transparently the result of a call using Django's cache, as well as (re)setting and deleting the cached value. In addition to being themselves callable, ``Cacheable`` instances are non-data descriptors and work seamlessly as methods. Example:: class Foo(object): @Cacheable.decorate(key='bar_{1}') def bar(self, x): print('Computing bar(%s)..' % x) return x*x >>> foo = Foo() >>> foo.bar(4) Computing bar(4).. 16 >>> foo.bar(4) # result is now cached 16 >>> foo.bar.delete_cache(4) # delete the cache for this param >>> foo.bar(4) # recompute (and cache) it Computing bar(4).. 16 ''' def __init__(self, func, key, timeout=None): '''Initializes a cacheable wrapper of ``func``. :param func: The callable to be wrapped. It may take any number of positional and/or keywords arguments. :param key: Specifies how to determine the cache key for a given ``func(*args, **kwds)`` call; it can be: - A callable: The cache key is computed as ``key(*args, **kwds)``. Obviously ``key`` must have the same (or compatible) signature with ``func``. - A string: A format string in PEP 3101 syntax. The cache key is determined as ``key.format(*args, **kwds)``. :param timeout: If given, that timeout will be used for the key; otherwise the default cache timeout will be used. ''' self._func = func self._timeout = timeout self._obj = None # for bound methods' im_self object if callable(key): self._key = key elif isinstance(key, basestring): self._key = key.format else: raise TypeError('%s keys are invalid' % key.__class__.__name__) @classmethod def decorate(cls, *args, **kwds): '''A decorator for wrapping a callable into a :class:`Cacheable` instance.''' return lambda func: cls(func, *args, **kwds) def __call__(self, *args, **kwds): '''Returns the cached result of ``func(*args, **kwds)`` or computes and caches it if it's not already cached. This method alone covers the majority of use cases, allowing clients to use ``Cacheable`` instances as plain callables. :returns: The cached or computed result. ''' if self._obj is not None: args = (self._obj,) + args key = self._key(*args, **kwds) value = cache.get(key) if value is None: value = self._func(*args, **kwds) cache.set(key, value, timeout=self._timeout) return value def set_cache(self, *args, **kwds): '''Computes ``func(*args, **kwds)`` and stores it in the cache. Unlike :meth:`__call__`, this method sets the cache unconditionally, without checking first if a key corresponding to a call with the same parameters already exists. :returns: The newly computed (and cached) result. ''' if self._obj is not None: args = (self._obj,) + args key = self._key(*args, **kwds) value = self._func(*args, **kwds) cache.set(key, value, timeout=self._timeout) return value def delete_cache(self, *args, **kwds): '''Deletes the cached result (if any) corresponding to a call with ``args`` and ``kwds``. ''' cache.delete(self.get_cache_key(*args, **kwds)) def get_cache_key(self, *args, **kwds): '''Returns the cache key corresponding to a call with ``args`` and ``kwds``. This is mainly for debugging and for interfacing with external services; clients of this class normally don't need to deal with cache keys explicitly. ''' if self._obj is not None: return self._key(self._obj, *args, **kwds) return self._key(*args, **kwds) def __get__(self, obj, objtype=None): if obj is None: return self new = copy(self) new._obj = obj return new class CacheableProperty(property, Cacheable): '''A :class:`Cacheable` that is also a property. Example:: class Foo(object): @CacheableProperty.decorate(key='bar_{0}') def bar(self): print('Computing bar()..') return 42 >>> foo = Foo() >>> foo.bar # just like a regular property Computing bar().. 42 >>> foo.bar # result is now cached 42 >>> # Cacheable methods are available through the class >>> Foo.bar.delete_cache(foo) >>> foo.bar Computing bar().. 42 ''' def __init__(self, func, key, timeout=None, fset=None, fdel=None, doc=None): Cacheable.__init__(self, func, key, timeout=timeout) property.__init__(self, fget=self.__call__, fset=fset, fdel=fdel) self.__doc__ = doc or getattr(func, '__doc__', None)