Fix the previous commit and add more test assertions to show why it was wrong.
[cached-property.git] / cached_property.py
1 # -*- coding: utf-8 -*-
2
3 __author__ = 'Daniel Greenfeld'
4 __email__ = 'pydanny@gmail.com'
5 __version__ = '1.0.0'
6 __license__ = 'BSD'
7
8 from time import time
9 import threading
10
11
12 class cached_property(object):
13 """
14 A property that is only computed once per instance and then replaces itself
15 with an ordinary attribute. Deleting the attribute resets the property.
16 Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
17 """ # noqa
18
19 def __init__(self, func):
20 self.__doc__ = getattr(func, '__doc__')
21 self.func = func
22
23 def __get__(self, obj, cls):
24 if obj is None:
25 return self
26 value = obj.__dict__[self.func.__name__] = self.func(obj)
27 return value
28
29
30 class threaded_cached_property(object):
31 """
32 A cached_property version for use in environments where multiple threads
33 might concurrently try to access the property.
34 """
35
36 def __init__(self, func):
37 self.__doc__ = getattr(func, '__doc__')
38 self.func = func
39 self.lock = threading.RLock()
40
41 def __get__(self, obj, cls):
42 if obj is None:
43 return self
44
45 obj_dict = obj.__dict__
46 name = self.func.__name__
47 with self.lock:
48 try:
49 # check if the value was computed before the lock was acquired
50 return obj_dict[name]
51 except KeyError:
52 # if not, do the calculation and release the lock
53 return obj_dict.setdefault(name, self.func(obj))
54
55
56 class cached_property_with_ttl(object):
57 """
58 A property that is only computed once per instance and then replaces itself
59 with an ordinary attribute. Setting the ttl to a number expresses how long
60 the property will last before being timed out.
61 """
62
63 def __init__(self, ttl=None):
64 ttl_or_func = ttl
65 self.ttl = None
66 if callable(ttl_or_func):
67 self.prepare_func(ttl_or_func)
68 else:
69 self.ttl = ttl_or_func
70
71 def prepare_func(self, func, doc=None):
72 '''Prepare to cache object method.'''
73 self.func = func
74 self.__doc__ = doc or func.__doc__
75 self.__name__ = func.__name__
76 self.__module__ = func.__module__
77
78 def __call__(self, func, doc=None):
79 self.prepare_func(func, doc)
80 return self
81
82 def __get__(self, obj, cls):
83 if obj is None:
84 return self
85
86 now = time()
87 try:
88 value, last_update = obj._cache[self.__name__]
89 if self.ttl and self.ttl > 0 and now - last_update > self.ttl:
90 raise AttributeError
91 except (KeyError, AttributeError):
92 value = self.func(obj)
93 try:
94 cache = obj._cache
95 except AttributeError:
96 cache = obj._cache = {}
97 cache[self.__name__] = (value, now)
98
99 return value
100
101 def __delete__(self, obj):
102 try:
103 del obj._cache[self.__name__]
104 except (KeyError, AttributeError):
105 pass
106
107 # Aliases to make cached_property_with_ttl easier to use
108 cached_property_ttl = cached_property_with_ttl
109 timed_cached_property = cached_property_with_ttl
110
111
112 class threaded_cached_property_with_ttl(cached_property_with_ttl):
113 """
114 A cached_property version for use in environments where multiple threads
115 might concurrently try to access the property.
116 """
117
118 def __init__(self, ttl=None):
119 super(threaded_cached_property_with_ttl, self).__init__(ttl)
120 self.lock = threading.RLock()
121
122 def __get__(self, obj, cls):
123 with self.lock:
124 # Double check if the value was computed before the lock was
125 # acquired.
126 prop_name = self.__name__
127 if hasattr(obj, '_cache') and prop_name in obj._cache:
128 return obj._cache[prop_name][0]
129
130 # If not, do the calculation and release the lock.
131 return super(threaded_cached_property_with_ttl, self).__get__(obj,
132 cls)
133
134 # Alias to make threaded_cached_property_with_ttl easier to use
135 threaded_cached_property_ttl = threaded_cached_property_with_ttl
136 timed_threaded_cached_property = threaded_cached_property_with_ttl