blob: 5f8cd885da2229fd86c7a4741d42169fc8f5df43 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Caching utilities.
"""
from __future__ import absolute_import # so we can import standard 'collections' and 'threading'
from threading import Lock
from functools import partial
from .collections import OrderedDict
class cachedmethod(object): # pylint: disable=invalid-name
"""
Decorator for caching method return values.
The implementation is thread-safe.
Supports ``cache_info`` to be compatible with Python 3's ``functools.lru_cache``. Note that the
statistics are combined for all instances of the class.
Won't use the cache if not called when bound to an object, allowing you to override the cache.
Adapted from `this solution
<http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/>`__.
"""
ENABLED = True
def __init__(self, func):
self.__doc__ = func.__doc__
self.func = func
self.hits = 0
self.misses = 0
self.lock = Lock()
def cache_info(self):
with self.lock:
return (self.hits, self.misses, None, self.misses)
def reset_cache_info(self):
with self.lock:
self.hits = 0
self.misses = 0
def __get__(self, instance, owner):
if instance is None:
# Don't use cache if not bound to an object
# Note: This is also a way for callers to override the cache
return self.func
return partial(self, instance)
def __call__(self, *args, **kwargs):
if not self.ENABLED:
return self.func(*args, **kwargs)
instance = args[0]
if not hasattr(instance, '_method_cache'):
instance._method_cache = {}
method_cache = instance._method_cache
key = (self.func, args[1:], frozenset(kwargs.items()))
try:
with self.lock:
return_value = method_cache[key]
self.hits += 1
except KeyError:
return_value = self.func(*args, **kwargs)
with self.lock:
method_cache[key] = return_value
self.misses += 1
# Another thread may override our cache entry here, so we need to read
# it again to make sure all threads use the same return value
return_value = method_cache.get(key, return_value)
return return_value
class HasCachedMethods(object):
"""
Provides convenience methods for working with :class:`cachedmethod`.
"""
def __init__(self, method_cache=None):
self._method_cache = method_cache or {}
@property
def _method_cache_info(self):
"""
The cache infos of all cached methods.
:rtype: dict of str, 4-tuple
"""
cached_info = OrderedDict()
for k, v in self.__class__.__dict__.iteritems():
if isinstance(v, property):
# The property getter might be cached
v = v.fget
if hasattr(v, 'cache_info'):
cached_info[k] = v.cache_info()
return cached_info
def _reset_method_cache(self):
"""
Resets the caches of all cached methods.
"""
if hasattr(self, '_method_cache'):
self._method_cache = {}
# Note: Another thread may already be storing entries in the cache here.
# But it's not a big deal! It only means that our cache_info isn't
# guaranteed to be accurate.
for entry in self.__class__.__dict__.itervalues():
if isinstance(entry, property):
# The property getter might be cached
entry = entry.fget
if hasattr(entry, 'reset_cache_info'):
entry.reset_cache_info()