diff --git a/aguaclara/core/cache.py b/aguaclara/core/cache.py new file mode 100644 index 00000000..28ade853 --- /dev/null +++ b/aguaclara/core/cache.py @@ -0,0 +1,54 @@ +# The cache decorator does not support objects nested within datastructures or other objects. It is quite limited +# because the hash function is limited. This could be revisited and we could use a serialization library like Pickle +# if this proved to be an issue (which it most likely will when we get more complex classes) + +import collections +import warnings + +# The cache has key=(function name , parameter serialization) and value= +__ac_cache__ = {} + + +def ac_cache(method): + def _cache(*args, **kw): + global __cache__ + param_list = [args, kw] + params_key = tuple([method.__name__, ac_hash(param_list)]) + try: + value = __ac_cache__[params_key] + except KeyError: + value = method(*args, **kw) + __ac_cache__[params_key] = value + + return value + + # Attempt to hash any object + def ac_hash(hashable_object): + if is_simple_hashable(hashable_object): + a_hash = repr(hashable_object) + elif isinstance(hashable_object, HashableObject): + a_hash = hashable_object.ac_hash() + elif isinstance(hashable_object, collections.Iterable): + a_hash = ac_hash_iterable_into_tuple(hashable_object) + else: + a_hash = repr(hashable_object) + warnings.warn("Using repr() to make a hash of {}. Please consider inheriting HashableObject class as repr " + "will not guarantee replicable hashing and can result in bad cache returns.".format( + repr(hashable_object)), Warning, stacklevel=2) + return a_hash + + def ac_hash_iterable_into_tuple(hashable_object_list): + hash_tuple = () + for hashable_object in hashable_object_list: + hash_tuple += (ac_hash(hashable_object),) + return hash_tuple + + primitive = (int, str, bool, ...) + def is_simple_hashable(thing): + return type(thing) in primitive + + return _cache + +class HashableObject: + def ac_hash(self): + return tuple(sorted(self.__dict__.items())) \ No newline at end of file diff --git a/tests/core/test_cache.py b/tests/core/test_cache.py new file mode 100644 index 00000000..46284789 --- /dev/null +++ b/tests/core/test_cache.py @@ -0,0 +1,62 @@ +from aguaclara.core.cache import ac_cache, HashableObject + + +class ComputedObject(HashableObject): + def __init__(self): + self.a = 2 + self.b = 3 + self.c = 4 + + @property + @ac_cache + def product(self): + increment_n_calls() + return self.a * self.b * self.c + + @property + @ac_cache + def sum(self): + increment_n_calls() + return self.a + self.b + self.c + + @ac_cache + def sum_with_arg(self, my_arg): + increment_n_calls() + return self.sum + my_arg + + @ac_cache + def sum_with_kwarg(self, my_arg=10): + increment_n_calls() + return self.sum + my_arg + + +# Keep track of the total number of calls +side_effect_n_calls = 0 + + +def increment_n_calls(): + global side_effect_n_calls + side_effect_n_calls = side_effect_n_calls +1 + + +def test_ac_cache(): + my_computed_object = ComputedObject() + assert 24 == my_computed_object.product + assert 1 == side_effect_n_calls + assert 9 == my_computed_object.sum + assert 2 == side_effect_n_calls + assert 9 == my_computed_object.sum + assert 2 == side_effect_n_calls + my_computed_object.a=3 + assert 36 == my_computed_object.product + assert 3 == side_effect_n_calls + assert 10 == my_computed_object.sum + assert 4 == side_effect_n_calls + assert 15 == my_computed_object.sum_with_arg(5) + assert 5 == side_effect_n_calls + assert 20 == my_computed_object.sum_with_kwarg() + assert 6 == side_effect_n_calls + assert 20 == my_computed_object.sum_with_kwarg() + assert 6 == side_effect_n_calls + assert 25 == my_computed_object.sum_with_kwarg(my_arg=15) + assert 7 == side_effect_n_calls