Update README.md
[cached-property.git] / cached_property.py
index 85009ae91656ef9be291dc912739cdae57bb897c..3135871bfb0d9cda0a4c4eac6cdf8c47d890a289 100644 (file)
@@ -1,39 +1,97 @@
 # -*- coding: utf-8 -*-
 
-__author__ = 'Daniel Greenfeld'
-__email__ = 'pydanny@gmail.com'
-__version__ = '1.0.0'
-__license__ = 'BSD'
+__author__ = "Daniel Greenfeld"
+__email__ = "pydanny@gmail.com"
+__version__ = "1.5.2"
+__license__ = "BSD"
 
+from functools import wraps
 from time import time
 import threading
 
+try:
+    import asyncio
+except (ImportError, SyntaxError):
+    asyncio = None
+
 
 class cached_property(object):
-    """ A property that is only computed once per instance and then replaces
-        itself with an ordinary attribute. Deleting the attribute resets the
-        property.
+    """
+    A property that is only computed once per instance and then replaces itself
+    with an ordinary attribute. Deleting the attribute resets the property.
+    Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
+    """  # noqa
+
+    def __init__(self, func):
+        self.__doc__ = getattr(func, "__doc__")
+        self.func = func
+
+    def __get__(self, obj, cls):
+        if obj is None:
+            return self
 
-        Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
-        """  # noqa
+        if asyncio and asyncio.iscoroutinefunction(self.func):
+            return self._wrap_in_coroutine(obj)
 
-    def __init__(self, ttl=None):
-        ttl_or_func = ttl
-        self.ttl = None
-        if callable(ttl_or_func):
-            self.prepare_func(ttl_or_func)
-        else:
-            self.ttl = ttl_or_func
+        value = obj.__dict__[self.func.__name__] = self.func(obj)
+        return value
+
+    def _wrap_in_coroutine(self, obj):
+        @wraps(obj)
+        @asyncio.coroutine
+        def wrapper():
+            future = asyncio.ensure_future(self.func(obj))
+            obj.__dict__[self.func.__name__] = future
+            return future
 
-    def prepare_func(self, func, doc=None):
-        '''Prepare to cache object method.'''
+        return wrapper()
+
+
+class threaded_cached_property(object):
+    """
+    A cached_property version for use in environments where multiple threads
+    might concurrently try to access the property.
+    """
+
+    def __init__(self, func):
+        self.__doc__ = getattr(func, "__doc__")
         self.func = func
-        self.__doc__ = doc or func.__doc__
-        self.__name__ = func.__name__
-        self.__module__ = func.__module__
+        self.lock = threading.RLock()
 
-    def __call__(self, func, doc=None):
-        self.prepare_func(func, doc)
+    def __get__(self, obj, cls):
+        if obj is None:
+            return self
+
+        obj_dict = obj.__dict__
+        name = self.func.__name__
+        with self.lock:
+            try:
+                # check if the value was computed before the lock was acquired
+                return obj_dict[name]
+
+            except KeyError:
+                # if not, do the calculation and release the lock
+                return obj_dict.setdefault(name, self.func(obj))
+
+
+class cached_property_with_ttl(object):
+    """
+    A property that is only computed once per instance and then replaces itself
+    with an ordinary attribute. Setting the ttl to a number expresses how long
+    the property will last before being timed out.
+    """
+
+    def __init__(self, ttl=None):
+        if callable(ttl):
+            func = ttl
+            ttl = None
+        else:
+            func = None
+        self.ttl = ttl
+        self._prepare_func(func)
+
+    def __call__(self, func):
+        self._prepare_func(func)
         return self
 
     def __get__(self, obj, cls):
@@ -41,39 +99,55 @@ class cached_property(object):
             return self
 
         now = time()
+        obj_dict = obj.__dict__
+        name = self.__name__
         try:
-            value, last_update = obj._cache[self.__name__]
-            if self.ttl and self.ttl > 0 and now - last_update > self.ttl:
-                raise AttributeError
-        except (KeyError, AttributeError):
-            value = self.func(obj)
-            try:
-                cache = obj._cache
-            except AttributeError:
-                cache = obj._cache = {}
-            cache[self.__name__] = (value, now)
+            value, last_updated = obj_dict[name]
+        except KeyError:
+            pass
+        else:
+            ttl_expired = self.ttl and self.ttl < now - last_updated
+            if not ttl_expired:
+                return value
 
+        value = self.func(obj)
+        obj_dict[name] = (value, now)
         return value
 
-    def __delattr__(self, name):
-        print(name)
+    def __delete__(self, obj):
+        obj.__dict__.pop(self.__name__, None)
+
+    def __set__(self, obj, value):
+        obj.__dict__[self.__name__] = (value, time())
+
+    def _prepare_func(self, func):
+        self.func = func
+        if func:
+            self.__doc__ = func.__doc__
+            self.__name__ = func.__name__
+            self.__module__ = func.__module__
+
 
+# Aliases to make cached_property_with_ttl easier to use
+cached_property_ttl = cached_property_with_ttl
+timed_cached_property = cached_property_with_ttl
+
+
+class threaded_cached_property_with_ttl(cached_property_with_ttl):
+    """
+    A cached_property version for use in environments where multiple threads
+    might concurrently try to access the property.
+    """
 
-class threaded_cached_property(cached_property):
-    """ A cached_property version for use in environments where multiple
-        threads might concurrently try to access the property.
-        """
     def __init__(self, ttl=None):
-        super(threaded_cached_property, self).__init__(ttl)
+        super(threaded_cached_property_with_ttl, self).__init__(ttl)
         self.lock = threading.RLock()
 
     def __get__(self, obj, cls):
         with self.lock:
-            # Double check if the value was computed before the lock was
-            # acquired.
-            prop_name = self.__name__
-            if hasattr(obj, '_cache') and prop_name in obj._cache:
-                return obj._cache[prop_name][0]
-
-            # If not, do the calculation and release the lock.
-            return super(threaded_cached_property, self).__get__(obj, cls)
+            return super(threaded_cached_property_with_ttl, self).__get__(obj, cls)
+
+
+# Alias to make threaded_cached_property_with_ttl easier to use
+threaded_cached_property_ttl = threaded_cached_property_with_ttl
+timed_threaded_cached_property = threaded_cached_property_with_ttl