Make cached_property thread-safe and alias threaded_cached_property to it.
[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 return obj.__dict__.setdefault(self.func.__name__, self.func(obj))
27
28
29 # Leave for backwards compatibility
30 threaded_cached_property = cached_property
31
32
33 class cached_property_with_ttl(object):
34 """
35 A property that is only computed once per instance and then replaces itself
36 with an ordinary attribute. Setting the ttl to a number expresses how long
37 the property will last before being timed out.
38 """
39
40 def __init__(self, ttl=None):
41 ttl_or_func = ttl
42 self.ttl = None
43 if callable(ttl_or_func):
44 self.prepare_func(ttl_or_func)
45 else:
46 self.ttl = ttl_or_func
47
48 def prepare_func(self, func, doc=None):
49 '''Prepare to cache object method.'''
50 self.func = func
51 self.__doc__ = doc or func.__doc__
52 self.__name__ = func.__name__
53 self.__module__ = func.__module__
54
55 def __call__(self, func, doc=None):
56 self.prepare_func(func, doc)
57 return self
58
59 def __get__(self, obj, cls):
60 if obj is None:
61 return self
62
63 now = time()
64 try:
65 value, last_update = obj._cache[self.__name__]
66 if self.ttl and self.ttl > 0 and now - last_update > self.ttl:
67 raise AttributeError
68 except (KeyError, AttributeError):
69 value = self.func(obj)
70 try:
71 cache = obj._cache
72 except AttributeError:
73 cache = obj._cache = {}
74 cache[self.__name__] = (value, now)
75
76 return value
77
78 def __delete__(self, obj):
79 try:
80 del obj._cache[self.__name__]
81 except (KeyError, AttributeError):
82 pass
83
84 # Aliases to make cached_property_with_ttl easier to use
85 cached_property_ttl = cached_property_with_ttl
86 timed_cached_property = cached_property_with_ttl
87
88
89 class threaded_cached_property_with_ttl(cached_property_with_ttl):
90 """
91 A cached_property version for use in environments where multiple threads
92 might concurrently try to access the property.
93 """
94
95 def __init__(self, ttl=None):
96 super(threaded_cached_property_with_ttl, self).__init__(ttl)
97 self.lock = threading.RLock()
98
99 def __get__(self, obj, cls):
100 with self.lock:
101 # Double check if the value was computed before the lock was
102 # acquired.
103 prop_name = self.__name__
104 if hasattr(obj, '_cache') and prop_name in obj._cache:
105 return obj._cache[prop_name][0]
106
107 # If not, do the calculation and release the lock.
108 return super(threaded_cached_property_with_ttl, self).__get__(obj,
109 cls)
110
111 # Alias to make threaded_cached_property_with_ttl easier to use
112 threaded_cached_property_ttl = threaded_cached_property_with_ttl
113 timed_threaded_cached_property = threaded_cached_property_with_ttl