1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2013 LEAP
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 from twisted.internet import defer
32 logger = logging.getLogger(__name__)
35 class _memoized(object):
39 Caches a function's return value each time it is called.
40 If called later with the same arguments, the cached value is returned
44 # cache invalidation time, in seconds
45 CACHE_INVALIDATION_DELTA = 1800
47 def __init__(self, func, ignore_kwargs=None, is_method=False,
51 :param ignore_kwargs: If True, ignore all kwargs.
52 If tuple, ignore those kwargs.
53 :type ignore_kwargs: bool, tuple or None
54 :param is_method: whether the decorated function is a method.
55 (ignores the self argument if so).
58 self.ignore_kwargs = ignore_kwargs if ignore_kwargs else []
59 self.is_method = is_method
63 self.CACHE_INVALIDATION_DELTA = invalidation
65 # TODO should put bounds to the cache dict so we do not
66 # consume a huge amount of memory.
71 def __call__(self, *args, **kwargs):
78 key = self._build_key(*args, **kwargs)
79 if not isinstance(key, collections.Hashable):
80 # uncacheable. a list, for instance.
81 # better to not cache than blow up.
82 logger.warning("Key is not hashable, bailing out!")
83 return self.func(*args, **kwargs)
86 if self._is_cache_still_valid(key):
87 logger.debug("Got value from cache...")
88 value = self._get_value(key)
89 return self._ret_or_raise(value)
91 logger.debug("Cache is invalid, evaluating again...")
93 # no cache, or cache invalid
95 value = self.func(*args, **kwargs)
96 except Exception as exc:
97 logger.error("Exception while calling function: %r" % (exc,))
100 if isinstance(value, defer.Deferred):
101 value.addBoth(self._store_deferred_value(key))
103 self.cache[key] = value
104 self.is_deferred[key] = False
105 self.cache_ts[key] = datetime.datetime.now()
106 return self._ret_or_raise(value)
108 def _build_key(self, *args, **kwargs):
110 Build key from the function arguments
113 # forget about `self` as key
118 if self.ignore_kwargs is True:
121 key = (key_args, frozenset(
122 [(k, v) for k, v in kwargs.items()
123 if k not in self.ignore_kwargs]))
126 def _get_value(self, key):
128 Get a value form cache for a key
130 if self.is_deferred[key]:
131 value = self.cache[key]
132 # it produces an errback if value is Failure
133 return defer.succeed(value)
135 return self.cache[key]
137 def _ret_or_raise(self, value):
139 Returns the value except if it is an exception,
140 in which case it's raised.
142 if isinstance(value, Exception):
146 def _is_cache_still_valid(self, key, now=datetime.datetime.now):
148 Returns True if the cache value is still valid, False otherwise.
150 For now, this happen if less than CACHE_INVALIDATION_DELTA seconds
151 have passed from the time in which we recorded the cached value.
153 :param key: the key to lookup in the cache
155 :param now: a callable that returns a datetime object. override
156 for dependency injection during testing.
160 cached_ts = self.cache_ts[key]
161 delta = datetime.timedelta(seconds=self.CACHE_INVALIDATION_DELTA)
162 return (now() - cached_ts) < delta
164 def _store_deferred_value(self, key):
166 Returns a callback to store values from deferreds
169 self.cache[key] = value
170 self.is_deferred[key] = True
176 Return the function's docstring.
178 return self.func.__doc__
180 def __get__(self, obj, objtype):
182 Support instance methods.
184 return functools.partial(self.__call__, obj)
187 def memoized_method(function=None, ignore_kwargs=None,
188 invalidation=_memoized.CACHE_INVALIDATION_DELTA):
190 Wrap _memoized to allow for deferred calling
192 :type function: callable, or None.
193 :type ignore_kwargs: None, True or tuple.
194 :type invalidation: int seconds.
197 return _memoized(function, is_method=True, invalidation=invalidation)
199 def wrapper(function):
201 function, ignore_kwargs=ignore_kwargs, is_method=True,
202 invalidation=invalidation)