Update README.md
[cached-property.git] / cached_property.py
1 # -*- coding: utf-8 -*-
2
3 __author__ = "Daniel Greenfeld"
4 __email__ = "pydanny@gmail.com"
5 __version__ = "1.5.2"
6 __license__ = "BSD"
7
8 from functools import wraps
9 from time import time
10 import threading
11
12 try:
13 import asyncio
14 except (ImportError, SyntaxError):
15 asyncio = None
16
17
18 class cached_property(object):
19 """
20 A property that is only computed once per instance and then replaces itself
21 with an ordinary attribute. Deleting the attribute resets the property.
22 Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
23 """ # noqa
24
25 def __init__(self, func):
26 self.__doc__ = getattr(func, "__doc__")
27 self.func = func
28
29 def __get__(self, obj, cls):
30 if obj is None:
31 return self
32
33 if asyncio and asyncio.iscoroutinefunction(self.func):
34 return self._wrap_in_coroutine(obj)
35
36 value = obj.__dict__[self.func.__name__] = self.func(obj)
37 return value
38
39 def _wrap_in_coroutine(self, obj):
40 @wraps(obj)
41 @asyncio.coroutine
42 def wrapper():
43 future = asyncio.ensure_future(self.func(obj))
44 obj.__dict__[self.func.__name__] = future
45 return future
46
47 return wrapper()
48
49
50 class threaded_cached_property(object):
51 """
52 A cached_property version for use in environments where multiple threads
53 might concurrently try to access the property.
54 """
55
56 def __init__(self, func):
57 self.__doc__ = getattr(func, "__doc__")
58 self.func = func
59 self.lock = threading.RLock()
60
61 def __get__(self, obj, cls):
62 if obj is None:
63 return self
64
65 obj_dict = obj.__dict__
66 name = self.func.__name__
67 with self.lock:
68 try:
69 # check if the value was computed before the lock was acquired
70 return obj_dict[name]
71
72 except KeyError:
73 # if not, do the calculation and release the lock
74 return obj_dict.setdefault(name, self.func(obj))
75
76
77 class cached_property_with_ttl(object):
78 """
79 A property that is only computed once per instance and then replaces itself
80 with an ordinary attribute. Setting the ttl to a number expresses how long
81 the property will last before being timed out.
82 """
83
84 def __init__(self, ttl=None):
85 if callable(ttl):
86 func = ttl
87 ttl = None
88 else:
89 func = None
90 self.ttl = ttl
91 self._prepare_func(func)
92
93 def __call__(self, func):
94 self._prepare_func(func)
95 return self
96
97 def __get__(self, obj, cls):
98 if obj is None:
99 return self
100
101 now = time()
102 obj_dict = obj.__dict__
103 name = self.__name__
104 try:
105 value, last_updated = obj_dict[name]
106 except KeyError:
107 pass
108 else:
109 ttl_expired = self.ttl and self.ttl < now - last_updated
110 if not ttl_expired:
111 return value
112
113 value = self.func(obj)
114 obj_dict[name] = (value, now)
115 return value
116
117 def __delete__(self, obj):
118 obj.__dict__.pop(self.__name__, None)
119
120 def __set__(self, obj, value):
121 obj.__dict__[self.__name__] = (value, time())
122
123 def _prepare_func(self, func):
124 self.func = func
125 if func:
126 self.__doc__ = func.__doc__
127 self.__name__ = func.__name__
128 self.__module__ = func.__module__
129
130
131 # Aliases to make cached_property_with_ttl easier to use
132 cached_property_ttl = cached_property_with_ttl
133 timed_cached_property = cached_property_with_ttl
134
135
136 class threaded_cached_property_with_ttl(cached_property_with_ttl):
137 """
138 A cached_property version for use in environments where multiple threads
139 might concurrently try to access the property.
140 """
141
142 def __init__(self, ttl=None):
143 super(threaded_cached_property_with_ttl, self).__init__(ttl)
144 self.lock = threading.RLock()
145
146 def __get__(self, obj, cls):
147 with self.lock:
148 return super(threaded_cached_property_with_ttl, self).__get__(obj, cls)
149
150
151 # Alias to make threaded_cached_property_with_ttl easier to use
152 threaded_cached_property_ttl = threaded_cached_property_with_ttl
153 timed_threaded_cached_property = threaded_cached_property_with_ttl