summaryrefslogtreecommitdiffstats
path: root/azure/aria/aria-extension-cloudify/src/aria/aria/utils/caching.py
blob: 5f8cd885da2229fd86c7a4741d42169fc8f5df43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# 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()