From 2d6c9785eab7b694a3d36774fbc0148190eb4b45 Mon Sep 17 00:00:00 2001 From: Daniel Greenfeld Date: Sun, 18 May 2014 11:29:53 -0700 Subject: [PATCH] Add timed_cached_property decorator --- HISTORY.rst | 7 ++++ Makefile | 2 +- README.rst | 22 +++++++++++++ cached_property.py | 61 +++++++++++++++++++++++++++++++++++ tests/test_cached_property.py | 32 +++++++++++++++++- 5 files changed, 122 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f4c5ee0..9964ecb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -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) diff --git a/Makefile b/Makefile index 4dec135..22ac8b8 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ lint: flake8 cached_property tests test: - py.test cached_property.py + py.test test-all: tox diff --git a/README.rst b/README.rst index ffd395c..990eb2d 100644 --- a/README.rst +++ b/README.rst @@ -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 -------- diff --git a/cached_property.py b/cached_property.py index 72ab0bb..7875934 100644 --- a/cached_property.py +++ b/cached_property.py @@ -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[] + + © 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 diff --git a/tests/test_cached_property.py b/tests/test_cached_property.py index 75508e4..50884d4 100755 --- a/tests/test_cached_property.py +++ b/tests/test_cached_property.py @@ -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() -- 2.30.2