Add timed_cached_property decorator
authorDaniel Greenfeld <pydanny@gmail.com>
Sun, 18 May 2014 18:29:53 +0000 (11:29 -0700)
committerDaniel Greenfeld <pydanny@gmail.com>
Sun, 18 May 2014 18:29:53 +0000 (11:29 -0700)
HISTORY.rst
Makefile
README.rst
cached_property.py
tests/test_cached_property.py

index f4c5ee046b028732e9fd6cfdb6d35034eb53eead..9964ecb4442e5767b1052ed69e94e4ab79574670 100644 (file)
@@ -6,6 +6,13 @@ History
 0.1.4 (2014-05-18)
 ++++++++++++++++++
 
+* Updated credits
+* Sourced the bottle implementation
+* Added a modified version of Christopher Arndt's implementation, which allows for caches that expire.
+
+0.1.4 (2014-05-17)
+++++++++++++++++++
+
 * Fix the dang-blarged py_modules argument.
 
 0.1.3 (2014-05-17)
index 4dec135b85b6fea7d887f19a4e60e391b3a05d8c..22ac8b860e13b180b7794affa89faebf4a6a1abe 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -28,7 +28,7 @@ lint:
        flake8 cached_property tests
 
 test:
-       py.test cached_property.py
+       py.test
 
 test-all:
        tox
index ffd395c8124801be3b17829516fdc3af0f652cc7..990eb2d81f2f788046e5c8063f5497d7c5b7ea9c 100644 (file)
@@ -84,6 +84,28 @@ Now when we run it the price stays at $550.
 
 Why doesn't the value of `monopoly.boardwalk` change? Because it's a **cached property**!
 
+What if I want to use a cache that times out?
+-------------------------------------------------
+
+Just import the `timed_cached_property`:
+
+.. code-block:: python
+
+    from cached_property import timed_cached_property
+
+    class Monopoly(object):
+
+        def __init__(self):
+            self.boardwalk_price = 500
+
+        # Times out in 5 minutes
+        @timed_cached_property(ttl=300)
+        def boardwalk(self):
+            # Sometimes the market crashes and prices drop back down to their
+            #   original value.
+            self.boardwalk_price += 50
+            return self.boardwalk_price
+
 Credits
 --------
 
index 72ab0bb2c9b6e46b181ce94f08bbbfc6bac3df96..787593457d487097895e6a6a695da5ff001392b3 100644 (file)
@@ -5,6 +5,8 @@ __email__ = 'pydanny@gmail.com'
 __version__ = '0.1.4'
 __license__ = 'BSD'
 
+import time
+
 
 class cached_property(object):
     """ A property that is only computed once per instance and then replaces
@@ -23,3 +25,62 @@ class cached_property(object):
             return self
         value = obj.__dict__[self.func.__name__] = self.func(obj)
         return value
+
+
+class timed_cached_property(object):
+    '''Decorator for read-only properties evaluated only once within TTL period.
+
+    It can be used to created a cached property like this::
+
+        import random
+
+        # the class containing the property must be a new-style class
+        class MyClass(object):
+            # create property whose value is cached for ten minutes
+            @cached_property(ttl=600)
+            def randint(self):
+                # will only be evaluated every 10 min. at maximum.
+                return random.randint(0, 100)
+
+    The value is cached  in the '_cache' attribute of the object instance that
+    has the property getter method wrapped by this decorator. The '_cache'
+    attribute value is a dictionary which has a key for every property of the
+    object which is wrapped by this decorator. Each entry in the cache is
+    created only when the property is accessed for the first time and is a
+    two-element tuple with the last computed property value and the last time
+    it was updated in seconds since the epoch.
+
+    The default time-to-live (TTL) is 0, which also means the cache never expires.
+
+    To expire a cached property value manually just do::
+
+        del instance._cache[<property name>]
+
+    © 2011 Christopher Arndt, MIT License
+    source: https://wiki.python.org/moin/PythonDecoratorLibrary#Cached_Properties
+
+    '''
+    def __init__(self, ttl=0):
+        self.ttl = ttl
+
+    def __call__(self, fget, doc=None):
+        self.fget = fget
+        self.__doc__ = doc or fget.__doc__
+        self.__name__ = fget.__name__
+        self.__module__ = fget.__module__
+        return self
+
+    def __get__(self, inst, owner):
+        now = time.time()
+        try:
+            value, last_update = inst._cache[self.__name__]
+            if self.ttl > 0 and now - last_update > self.ttl:
+                raise AttributeError
+        except (KeyError, AttributeError):
+            value = self.fget(inst)
+            try:
+                cache = inst._cache
+            except AttributeError:
+                cache = inst._cache = {}
+            cache[self.__name__] = (value, now)
+        return value
index 75508e408b0d8d24e6078cc73a30b4ef0810a7b8..50884d4e61df92151c1baaec56b7f5c38d0bfcbc 100755 (executable)
@@ -10,7 +10,7 @@ Tests for `cached-property` module.
 
 import unittest
 
-from cached_property import cached_property
+from cached_property import cached_property, timed_cached_property
 
 
 class TestCachedProperty(unittest.TestCase):
@@ -44,5 +44,35 @@ class TestCachedProperty(unittest.TestCase):
         self.assertEqual(c.add_cached, 1)
 
 
+class TestTimedCachedProperty(unittest.TestCase):
+
+    def test_normal_cached_property(self):
+
+        class Check(object):
+
+            def __init__(self):
+                self.total1 = 0
+                self.total2 = 0
+
+            @property
+            def add_control(self):
+                self.total1 += 1
+                return self.total1
+
+            @timed_cached_property(ttl=1)
+            def add_cached(self):
+                self.total2 += 1
+                return self.total2
+
+        c = Check()
+
+        # The control shows that we can continue to add 1.
+        self.assertEqual(c.add_control, 1)
+        self.assertEqual(c.add_control, 2)
+
+        # The cached version demonstrates how nothing new is added
+        self.assertEqual(c.add_cached, 1)
+        self.assertEqual(c.add_cached, 1)
+
 if __name__ == '__main__':
     unittest.main()