#!/usr/bin/env python
#-*- coding:utf-8 -*-

"""Simple caching functionality using decorated functions.

Cached values are stored in a dicitonary. The keys are generated by a callable
given to the decorator. This can e.g. be used to cache expensive calculations
that depend on an object's state. In that case, the key generating-function
needs to be made aware of all relevant state attributes.
"""

# TODO: testing concept?


from functools import wraps

cache_registry = []


class NotInCacheError(Exception):
    """Exception to be thrown when a key that is not present in cache is read.
    """

    pass


class Cache(object):
    """Base class for caches.
    """

    def get(self, key):
        """Get a cached value from key.

        Parameters
        ----------
        key : string

        Raises
        ------
        NotInCacheError
            In case key does not exist in cache.
        """
        return NotImplementedError

    def set(self, key, value):
        """Set a key/value pair in cache.

        Parameters
        ----------
        key : string
        value : object
        """
        return NotImplementedError

    def clean(self):
        """Clean out all cached items.
        """

        return NotImplementedError


class FiniteCache(Cache):
    """Implements a cache that can only hold a given number of elements.

    Uses a dict, but watches out for the number of keys in dict. If the maximum
    size is reached, the oldest key/value pair needs to go. This can be used
    for computations that return large objects.
    """

    def __init__(self, max_size):
        """Create a cacher that behaves much like a dictionary with a finite
        number of entries.

        Parameters
        ----------
        max_size : int
            Maximum number of entries.
        """

        self.max_size = max_size
        self.cache = {}  # dicts are not ordered, so...
        self.keys_in_order = []  # lists are ordered!

    def get(self, key):
        """Get a cached value from key.

        Parameters
        ----------
        key : string

        Raises
        ------
        NotInCacheError
            In case key does not exist in cache.
        """

        try:
            cached_val = self.cache[key]
            return cached_val
        except KeyError:
            raise NotInCacheError

    def set(self, key, value):
        """Set a key/value pair in cache.

        Parameters
        ----------
        key : string
        value : object
        """

        self.keys_in_order.append(key)
        self.cache[key] = value
        #print("Caching new key: {}".format(key))
        #print("\tAvailable keys {}; cache {}".format(self.keys_in_order, self.cache))

        # if cache has grown too big (i.e. has too many cached items): clean
        # out oldest cached item
        if len(self.cache) > self.max_size:
            oldest_key = self.keys_in_order[0]  # first item is oldest key
            #print("Keys of cache dict: {}".format(self.cache.keys()))
            #print("Keys in key list: {}".format(self.keys_in_order))
            self.cache.pop(oldest_key)
            self.keys_in_order.pop(0)
            #print("Cleaning out old key: {}".format(oldest_key))

    def clear(self):
        """Clean out all cached items.
        """

        self.cache.clear()
        for key in self.keys_in_order:
            del key
        self.keys_in_order = []


def cacher(key_generator=None):
    """Decorator that wraps a class member function with caching capabilities.

    Parameters
    ----------
    key_generator : callable
        A callable that returns a string that describes the class' state at
        runtime.
    """

    if key_generator is None:
        raise ValueError("A key_generator callable must be given")

    cache = FiniteCache(1)  # TODO: make this optional
    cache_registry.append(cache)

    def decorate(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            func_name = func.__repr__()
            key = (func_name + "_"
                   + key_generator(self, *args, **kwargs).format(self=self,
                                                                 *args, **kwargs)
                   )
            try:
                ret = cache.get(key)
            except NotInCacheError:
                ret = func(self, *args, **kwargs)
                cache.set(key, ret)

            return ret

        return wrapper

    return decorate


def clear_all_registered_caches():
    """Clear all caches registered.
    """

    #print("Now cleaning all caches!")
    for cache in cache_registry:
        cache.clear()

@@ -905,37 +905,36 @@
    %22%22%22%0A        r
-eturn
+aise
  NotImplementedE
@@ -929,32 +929,34 @@
 ImplementedError
+()
 %0A%0A    def set(se
@@ -1110,37 +1110,36 @@
    %22%22%22%0A        r
-eturn
+aise
  NotImplementedE
@@ -1138,24 +1138,26 @@
 ementedError
+()
 %0A%0A    def cl
@@ -1228,21 +1228,20 @@
        r
-eturn
+aise
  NotImpl
@@ -1252,16 +1252,90 @@
 tedError
+()%0A%0A    # TODO: is it reasonable to add a keys member, a delete() function
 %0A%0A%0Aclass

Raise instead of return NotImplementedErrors in Cache base class