# 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 `__. """ 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()