Implement ttl support.
authorhbc <bcxxxxxx@gmail.com>
Mon, 9 Feb 2015 14:23:49 +0000 (22:23 +0800)
committerhbc <bcxxxxxx@gmail.com>
Mon, 9 Feb 2015 14:23:49 +0000 (22:23 +0800)
Borrowed from https://wiki.python.org/moin/PythonDecoratorLibrary#Cached_Properties.

cached_property.py
requirements.txt
tests/test_cached_property.py
tests/test_threaded_cached_property.py

index c07f5089855fab4e26627aa0ac893d0349f9c96f..904d88fc39a5308397b67f8f31df5f8d7756ea48 100644 (file)
@@ -5,6 +5,7 @@ __email__ = 'pydanny@gmail.com'
 __version__ = '0.1.5'
 __license__ = 'BSD'
 
+from time import time
 import threading
 
 
@@ -14,34 +15,57 @@ class cached_property(object):
         property.
 
         Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
-        """
+        """  # noqa
+
+    def __init__(self, ttl=300):
+        self.ttl = ttl
 
-    def __init__(self, func):
-        self.__doc__ = getattr(func, '__doc__')
+    def __call__(self, func, doc=None):
         self.func = func
+        self.__doc__ = doc or func.__doc__
+        self.__name__ = func.__name__
+        self.__module__ = func.__module__
+
+        return self
 
     def __get__(self, obj, cls):
         if obj is None:
             return self
-        value = obj.__dict__[self.func.__name__] = self.func(obj)
+
+        now = time()
+        try:
+            value, last_update = obj._cache[self.__name__]
+            if 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)
+
         return value
 
+    def __delattr__(self, name):
+        print(name)
+
 
 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, func):
-        super(threaded_cached_property, self).__init__(func)
+    def __init__(self, ttl=None):
+        super(threaded_cached_property, 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.func.__name__
-            if prop_name in obj.__dict__:
-                return obj.__dict__[prop_name]
+            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)
index 6e6b0ad3f62112f10ea7431a424ab42c1be31f9f..ed9a0e0ae304f4b8b479766885437b5124049789 100644 (file)
@@ -2,4 +2,5 @@
 coverage
 pytest
 pytest-cov
+freezegun
 wheel==0.23.0
index d39778a3f16c2026c563b0d0d979948d49e2a0a6..f8c6e2c1c42d14420364ec994837907ad276316b 100755 (executable)
@@ -10,6 +10,7 @@ Tests for `cached-property` module.
 from time import sleep
 from threading import Lock, Thread
 import unittest
+from freezegun import freeze_time
 
 from cached_property import cached_property
 
@@ -29,7 +30,7 @@ class TestCachedProperty(unittest.TestCase):
                 self.total1 += 1
                 return self.total1
 
-            @cached_property
+            @cached_property()
             def add_cached(self):
                 self.total2 += 1
                 return self.total2
@@ -55,7 +56,7 @@ class TestCachedProperty(unittest.TestCase):
             def __init__(self):
                 self.total = 0
 
-            @cached_property
+            @cached_property()
             def add_cached(self):
                 self.total += 1
                 return self.total
@@ -67,7 +68,7 @@ class TestCachedProperty(unittest.TestCase):
         self.assertEqual(c.add_cached, 1)
 
         # Reset the cache.
-        del c.add_cached
+        del c._cache['add_cached']
         self.assertEqual(c.add_cached, 2)
         self.assertEqual(c.add_cached, 2)
 
@@ -78,7 +79,7 @@ class TestCachedProperty(unittest.TestCase):
             def __init__(self):
                 self.total = None
 
-            @cached_property
+            @cached_property()
             def add_cached(self):
                 return self.total
 
@@ -93,7 +94,7 @@ class TestThreadingIssues(unittest.TestCase):
     def test_threads(self):
         """ How well does the standard cached_property implementation work with threads?
             Short answer: It doesn't! Use threaded_cached_property instead!
-        """
+        """  # noqa
 
         class Check(object):
 
@@ -101,7 +102,7 @@ class TestThreadingIssues(unittest.TestCase):
                 self.total = 0
                 self.lock = Lock()
 
-            @cached_property
+            @cached_property()
             def add_cached(self):
                 sleep(1)
                 # Need to guard this since += isn't atomic.
@@ -130,3 +131,28 @@ class TestThreadingIssues(unittest.TestCase):
         # between 1 and num_threads, depending on thread scheduling and
         # preemption.
         self.assertEqual(c.add_cached, num_threads)
+
+
+class TestCachedPropertyWithTTL(unittest.TestCase):
+    def test_ttl_expiry(self):
+
+        class Check(object):
+
+            def __init__(self):
+                self.total = 0
+
+            @cached_property(ttl=100000)
+            def add_cached(self):
+                self.total += 1
+                return self.total
+
+        c = Check()
+
+        # Run standard cache assertion
+        self.assertEqual(c.add_cached, 1)
+        self.assertEqual(c.add_cached, 1)
+
+        # Expire the cache.
+        with freeze_time("9999-01-01"):
+            self.assertEqual(c.add_cached, 2)
+        self.assertEqual(c.add_cached, 2)
index f73e45e2c3aeed08f39ce9125f648b8297aeb96a..8b157bdb013ff72e518a60c7c76e6c8a60653167 100755 (executable)
@@ -29,7 +29,7 @@ class TestCachedProperty(unittest.TestCase):
                 self.total1 += 1
                 return self.total1
 
-            @threaded_cached_property
+            @threaded_cached_property()
             def add_cached(self):
                 self.total2 += 1
                 return self.total2
@@ -51,7 +51,7 @@ class TestCachedProperty(unittest.TestCase):
             def __init__(self):
                 self.total = 0
 
-            @threaded_cached_property
+            @threaded_cached_property()
             def add_cached(self):
                 self.total += 1
                 return self.total
@@ -63,7 +63,7 @@ class TestCachedProperty(unittest.TestCase):
         self.assertEqual(c.add_cached, 1)
 
         # Reset the cache.
-        del c.add_cached
+        del c._cache['add_cached']
         self.assertEqual(c.add_cached, 2)
         self.assertEqual(c.add_cached, 2)
 
@@ -74,7 +74,7 @@ class TestCachedProperty(unittest.TestCase):
             def __init__(self):
                 self.total = None
 
-            @threaded_cached_property
+            @threaded_cached_property()
             def add_cached(self):
                 return self.total
 
@@ -95,7 +95,7 @@ class TestThreadingIssues(unittest.TestCase):
                 self.total = 0
                 self.lock = Lock()
 
-            @threaded_cached_property
+            @threaded_cached_property()
             def add_cached(self):
                 sleep(1)
                 # Need to guard this since += isn't atomic.