From a17ec7ed651ba2b098a7fe279209c9554e13a20a Mon Sep 17 00:00:00 2001 From: hbc Date: Mon, 9 Feb 2015 22:23:49 +0800 Subject: [PATCH] Implement ttl support. Borrowed from https://wiki.python.org/moin/PythonDecoratorLibrary#Cached_Properties. --- cached_property.py | 42 ++++++++++++++++++++------ requirements.txt | 1 + tests/test_cached_property.py | 38 +++++++++++++++++++---- tests/test_threaded_cached_property.py | 10 +++--- 4 files changed, 71 insertions(+), 20 deletions(-) diff --git a/cached_property.py b/cached_property.py index c07f508..904d88f 100644 --- a/cached_property.py +++ b/cached_property.py @@ -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) diff --git a/requirements.txt b/requirements.txt index 6e6b0ad..ed9a0e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ coverage pytest pytest-cov +freezegun wheel==0.23.0 diff --git a/tests/test_cached_property.py b/tests/test_cached_property.py index d39778a..f8c6e2c 100755 --- a/tests/test_cached_property.py +++ b/tests/test_cached_property.py @@ -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) diff --git a/tests/test_threaded_cached_property.py b/tests/test_threaded_cached_property.py index f73e45e..8b157bd 100755 --- a/tests/test_threaded_cached_property.py +++ b/tests/test_threaded_cached_property.py @@ -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. -- 2.30.2