diff --git a/HISTORY.rst b/HISTORY.rst index 60bf2eb..ca6e3c2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,11 +3,17 @@ History ------- -1.0.0 (2014-02-13) +1.1.0 (2015-04-04) +++++++++++++++++++ + +* Regression: As the cache was not always clearing, we've broken out the time to expire feature to it's own set of specific tools. +* Fixed typo in README, thanks to @zoidbergwill. + +1.0.0 (2015-02-13) ++++++++++++++++++ * Added timed to expire feature to ``cached_property`` decorator. -* Changed ``del monopoly.boardwalk`` to ``del monopoly['boardwalk'] in order to support the new TTL feature. +* **Backwards incompatiblity**: Changed ``del monopoly.boardwalk`` to ``del monopoly['boardwalk']`` in order to support the new TTL feature. 0.1.5 (2014-05-20) ++++++++++++++++++ diff --git a/PKG-INFO b/PKG-INFO index 986fb84..b420bf8 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: cached-property -Version: 1.0.0 +Version: 1.1.0 Summary: A cached-property for decorating methods in classes. Home-page: https://github.com/pydanny/cached-property Author: Daniel Greenfeld @@ -12,7 +12,7 @@ Description: =============================== .. image:: https://badge.fury.io/py/cached-property.png :target: http://badge.fury.io/py/cached-property - + .. image:: https://travis-ci.org/pydanny/cached-property.png?branch=master :target: https://travis-ci.org/pydanny/cached-property @@ -61,7 +61,6 @@ Description: =============================== Let's convert the boardwalk property into a ``cached_property``. - .. code-block:: python from cached_property import cached_property @@ -105,43 +104,12 @@ Description: =============================== >>> monopoly.boardwalk 550 >>> # invalidate the cache - >>> del monopoly['boardwalk'] + >>> del monopoly.boardwalk >>> # request the boardwalk property again >>> monopoly.boardwalk 600 >>> monopoly.boardwalk 600 - - Timing out the cache - -------------------- - - Sometimes you want the price of things to reset after a time. - - .. code-block:: python - - import random - from cached_property import cached_property - - class Monopoly(object): - - @cached_property(ttl=5) # cache invalidates after 10 seconds - def dice(self): - # I dare the reader to implement a game using this method of 'rolling dice'. - return random.randint(2,12) - - .. code-block:: python - - >>> monopoly = Monopoly() - >>> monopoly.dice - 10 - >>> monopoly.dice - 10 - >>> from time import sleep - >>> sleep(6) # Sleeps long enough to expire the cache - >>> monopoly.dice - 3 - >>> monopoly.dice - 3 Working with Threads --------------------- @@ -193,6 +161,47 @@ Description: =============================== >>> self.assertEqual(m.boardwalk, 550) + Timing out the cache + -------------------- + + Sometimes you want the price of things to reset after a time. Use the ``ttl`` + versions of ``cached_property`` and ``threaded_cached_property``. + + .. code-block:: python + + import random + from cached_property import cached_property_with_ttl + + class Monopoly(object): + + @cached_property_with_ttl(ttl=5) # cache invalidates after 5 seconds + def dice(self): + # I dare the reader to implement a game using this method of 'rolling dice'. + return random.randint(2,12) + + Now use it: + + .. code-block:: python + + >>> monopoly = Monopoly() + >>> monopoly.dice + 10 + >>> monopoly.dice + 10 + >>> from time import sleep + >>> sleep(6) # Sleeps long enough to expire the cache + >>> monopoly.dice + 3 + >>> monopoly.dice + 3 + >>> # This cache clearing does not always work, see note below. + >>> del monopoly['dice'] + >>> monopoly.dice + 6 + + **Note:** The ``ttl`` tools do not reliably allow the clearing of the cache. This + is why they are broken out into seperate tools. See https://github.com/pydanny/cached-property/issues/16. + Credits -------- @@ -211,11 +220,17 @@ Description: =============================== History ------- - 1.0.0 (2014-02-13) + 1.1.0 (2015-04-04) + ++++++++++++++++++ + + * Regression: As the cache was not always clearing, we've broken out the time to expire feature to it's own set of specific tools. + * Fixed typo in README, thanks to @zoidbergwill. + + 1.0.0 (2015-02-13) ++++++++++++++++++ * Added timed to expire feature to ``cached_property`` decorator. - * Changed ``del monopoly.boardwalk`` to ``del monopoly['boardwalk'] in order to support the new TTL feature. + * **Backwards incompatiblity**: Changed ``del monopoly.boardwalk`` to ``del monopoly['boardwalk']`` in order to support the new TTL feature. 0.1.5 (2014-05-20) ++++++++++++++++++ diff --git a/README.rst b/README.rst index bbdb552..bdb9c51 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ cached-property .. image:: https://badge.fury.io/py/cached-property.png :target: http://badge.fury.io/py/cached-property - + .. image:: https://travis-ci.org/pydanny/cached-property.png?branch=master :target: https://travis-ci.org/pydanny/cached-property @@ -53,7 +53,6 @@ Now run it: Let's convert the boardwalk property into a ``cached_property``. - .. code-block:: python from cached_property import cached_property @@ -97,43 +96,12 @@ Results of cached functions can be invalidated by outside forces. Let's demonstr >>> monopoly.boardwalk 550 >>> # invalidate the cache - >>> del monopoly['boardwalk'] + >>> del monopoly.boardwalk >>> # request the boardwalk property again >>> monopoly.boardwalk 600 >>> monopoly.boardwalk 600 - -Timing out the cache --------------------- - -Sometimes you want the price of things to reset after a time. - -.. code-block:: python - - import random - from cached_property import cached_property - - class Monopoly(object): - - @cached_property(ttl=5) # cache invalidates after 10 seconds - def dice(self): - # I dare the reader to implement a game using this method of 'rolling dice'. - return random.randint(2,12) - -.. code-block:: python - - >>> monopoly = Monopoly() - >>> monopoly.dice - 10 - >>> monopoly.dice - 10 - >>> from time import sleep - >>> sleep(6) # Sleeps long enough to expire the cache - >>> monopoly.dice - 3 - >>> monopoly.dice - 3 Working with Threads --------------------- @@ -185,6 +153,47 @@ Now use it: >>> self.assertEqual(m.boardwalk, 550) +Timing out the cache +-------------------- + +Sometimes you want the price of things to reset after a time. Use the ``ttl`` +versions of ``cached_property`` and ``threaded_cached_property``. + +.. code-block:: python + + import random + from cached_property import cached_property_with_ttl + + class Monopoly(object): + + @cached_property_with_ttl(ttl=5) # cache invalidates after 5 seconds + def dice(self): + # I dare the reader to implement a game using this method of 'rolling dice'. + return random.randint(2,12) + +Now use it: + +.. code-block:: python + + >>> monopoly = Monopoly() + >>> monopoly.dice + 10 + >>> monopoly.dice + 10 + >>> from time import sleep + >>> sleep(6) # Sleeps long enough to expire the cache + >>> monopoly.dice + 3 + >>> monopoly.dice + 3 + >>> # This cache clearing does not always work, see note below. + >>> del monopoly['dice'] + >>> monopoly.dice + 6 + +**Note:** The ``ttl`` tools do not reliably allow the clearing of the cache. This +is why they are broken out into seperate tools. See https://github.com/pydanny/cached-property/issues/16. + Credits -------- diff --git a/cached_property.egg-info/PKG-INFO b/cached_property.egg-info/PKG-INFO index 986fb84..b420bf8 100644 --- a/cached_property.egg-info/PKG-INFO +++ b/cached_property.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: cached-property -Version: 1.0.0 +Version: 1.1.0 Summary: A cached-property for decorating methods in classes. Home-page: https://github.com/pydanny/cached-property Author: Daniel Greenfeld @@ -12,7 +12,7 @@ Description: =============================== .. image:: https://badge.fury.io/py/cached-property.png :target: http://badge.fury.io/py/cached-property - + .. image:: https://travis-ci.org/pydanny/cached-property.png?branch=master :target: https://travis-ci.org/pydanny/cached-property @@ -61,7 +61,6 @@ Description: =============================== Let's convert the boardwalk property into a ``cached_property``. - .. code-block:: python from cached_property import cached_property @@ -105,43 +104,12 @@ Description: =============================== >>> monopoly.boardwalk 550 >>> # invalidate the cache - >>> del monopoly['boardwalk'] + >>> del monopoly.boardwalk >>> # request the boardwalk property again >>> monopoly.boardwalk 600 >>> monopoly.boardwalk 600 - - Timing out the cache - -------------------- - - Sometimes you want the price of things to reset after a time. - - .. code-block:: python - - import random - from cached_property import cached_property - - class Monopoly(object): - - @cached_property(ttl=5) # cache invalidates after 10 seconds - def dice(self): - # I dare the reader to implement a game using this method of 'rolling dice'. - return random.randint(2,12) - - .. code-block:: python - - >>> monopoly = Monopoly() - >>> monopoly.dice - 10 - >>> monopoly.dice - 10 - >>> from time import sleep - >>> sleep(6) # Sleeps long enough to expire the cache - >>> monopoly.dice - 3 - >>> monopoly.dice - 3 Working with Threads --------------------- @@ -193,6 +161,47 @@ Description: =============================== >>> self.assertEqual(m.boardwalk, 550) + Timing out the cache + -------------------- + + Sometimes you want the price of things to reset after a time. Use the ``ttl`` + versions of ``cached_property`` and ``threaded_cached_property``. + + .. code-block:: python + + import random + from cached_property import cached_property_with_ttl + + class Monopoly(object): + + @cached_property_with_ttl(ttl=5) # cache invalidates after 5 seconds + def dice(self): + # I dare the reader to implement a game using this method of 'rolling dice'. + return random.randint(2,12) + + Now use it: + + .. code-block:: python + + >>> monopoly = Monopoly() + >>> monopoly.dice + 10 + >>> monopoly.dice + 10 + >>> from time import sleep + >>> sleep(6) # Sleeps long enough to expire the cache + >>> monopoly.dice + 3 + >>> monopoly.dice + 3 + >>> # This cache clearing does not always work, see note below. + >>> del monopoly['dice'] + >>> monopoly.dice + 6 + + **Note:** The ``ttl`` tools do not reliably allow the clearing of the cache. This + is why they are broken out into seperate tools. See https://github.com/pydanny/cached-property/issues/16. + Credits -------- @@ -211,11 +220,17 @@ Description: =============================== History ------- - 1.0.0 (2014-02-13) + 1.1.0 (2015-04-04) + ++++++++++++++++++ + + * Regression: As the cache was not always clearing, we've broken out the time to expire feature to it's own set of specific tools. + * Fixed typo in README, thanks to @zoidbergwill. + + 1.0.0 (2015-02-13) ++++++++++++++++++ * Added timed to expire feature to ``cached_property`` decorator. - * Changed ``del monopoly.boardwalk`` to ``del monopoly['boardwalk'] in order to support the new TTL feature. + * **Backwards incompatiblity**: Changed ``del monopoly.boardwalk`` to ``del monopoly['boardwalk']`` in order to support the new TTL feature. 0.1.5 (2014-05-20) ++++++++++++++++++ diff --git a/cached_property.egg-info/SOURCES.txt b/cached_property.egg-info/SOURCES.txt index ca7171f..3f9936d 100644 --- a/cached_property.egg-info/SOURCES.txt +++ b/cached_property.egg-info/SOURCES.txt @@ -14,4 +14,5 @@ cached_property.egg-info/not-zip-safe cached_property.egg-info/top_level.txt tests/__init__.py tests/test_cached_property.py +tests/test_cached_property_ttl.py tests/test_threaded_cached_property.py \ No newline at end of file diff --git a/cached_property.py b/cached_property.py index 85009ae..3547b43 100644 --- a/cached_property.py +++ b/cached_property.py @@ -2,7 +2,7 @@ __author__ = 'Daniel Greenfeld' __email__ = 'pydanny@gmail.com' -__version__ = '1.0.0' +__version__ = '1.1.0' __license__ = 'BSD' from time import time @@ -13,8 +13,44 @@ 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. - Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76 + """ + + def __init__(self, func): + self.__doc__ = getattr(func, '__doc__') + self.func = func + + def __get__(self, obj, cls): + if obj is None: + return self + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +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) + 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] + + # If not, do the calculation and release the lock. + return super(threaded_cached_property, self).__get__(obj, cls) + + +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. """ # noqa def __init__(self, ttl=None): @@ -55,16 +91,17 @@ class cached_property(object): return value - def __delattr__(self, name): - print(name) +# 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(cached_property): +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. """ 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): @@ -76,4 +113,9 @@ class threaded_cached_property(cached_property): 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 + diff --git a/setup.py b/setup.py index 0de70e8..2a6bca4 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ try: except ImportError: from distutils.core import setup -__version__ = '1.0.0' +__version__ = '1.1.0' readme = open('README.rst').read() history = open('HISTORY.rst').read().replace('.. :changelog:', '') diff --git a/tests/test_cached_property.py b/tests/test_cached_property.py index 7ef773d..d39778a 100755 --- a/tests/test_cached_property.py +++ b/tests/test_cached_property.py @@ -10,7 +10,6 @@ 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 @@ -45,10 +44,6 @@ class TestCachedProperty(unittest.TestCase): self.assertEqual(c.add_cached, 1) self.assertEqual(c.add_cached, 1) - # Cannot expire the cache. - with freeze_time("9999-01-01"): - self.assertEqual(c.add_cached, 1) - # It's customary for descriptors to return themselves if accessed # though the class, rather than through an instance. self.assertTrue(isinstance(Check.add_cached, cached_property)) @@ -72,7 +67,7 @@ class TestCachedProperty(unittest.TestCase): self.assertEqual(c.add_cached, 1) # Reset the cache. - del c._cache['add_cached'] + del c.add_cached self.assertEqual(c.add_cached, 2) self.assertEqual(c.add_cached, 2) @@ -98,7 +93,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): @@ -135,29 +130,3 @@ 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_cached_property_ttl.py b/tests/test_cached_property_ttl.py new file mode 100644 index 0000000..78e2e27 --- /dev/null +++ b/tests/test_cached_property_ttl.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- + +""" +test_threaded_cache_property.py +---------------------------------- + +Tests for `cached-property` module, cached_property_with_ttl. +Tests for `cached-property` module, threaded_cache_property_with_ttl. +""" +import unittest +from freezegun import freeze_time + +from cached_property import ( + cached_property_with_ttl, + threaded_cached_property_with_ttl +) + + +from time import sleep +from threading import Lock, Thread +import unittest +from freezegun import freeze_time + +from cached_property import cached_property + + +class TestCachedProperty(unittest.TestCase): + + def test_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 + + @cached_property_with_ttl + 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) + + # Cannot expire the cache. + with freeze_time("9999-01-01"): + self.assertEqual(c.add_cached, 1) + + # It's customary for descriptors to return themselves if accessed + # though the class, rather than through an instance. + self.assertTrue(isinstance(Check.add_cached, cached_property_with_ttl)) + + def test_reset_cached_property(self): + + class Check(object): + + def __init__(self): + self.total = 0 + + @cached_property_with_ttl + 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) + + # Reset the cache. + del c._cache['add_cached'] + self.assertEqual(c.add_cached, 2) + self.assertEqual(c.add_cached, 2) + + def test_none_cached_property(self): + + class Check(object): + + def __init__(self): + self.total = None + + @cached_property_with_ttl + def add_cached(self): + return self.total + + c = Check() + + # Run standard cache assertion + self.assertEqual(c.add_cached, None) + + +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): + + def __init__(self): + self.total = 0 + self.lock = Lock() + + @cached_property_with_ttl + def add_cached(self): + sleep(1) + # Need to guard this since += isn't atomic. + with self.lock: + self.total += 1 + return self.total + + c = Check() + threads = [] + num_threads = 10 + for x in range(num_threads): + thread = Thread(target=lambda: c.add_cached) + thread.start() + threads.append(thread) + + for thread in threads: + thread.join() + + # Threads means that caching is bypassed. + self.assertNotEqual(c.add_cached, 1) + + # This assertion hinges on the fact the system executing the test can + # spawn and start running num_threads threads within the sleep period + # (defined in the Check class as 1 second). If num_threads were to be + # massively increased (try 10000), the actual value returned would be + # 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_with_ttl(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) + + +class TestCachedProperty(unittest.TestCase): + + def test_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 + + @threaded_cached_property_with_ttl + 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) + + def test_reset_cached_property(self): + + class Check(object): + + def __init__(self): + self.total = 0 + + @threaded_cached_property_with_ttl + 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) + + # Reset the cache. + del c._cache['add_cached'] + self.assertEqual(c.add_cached, 2) + self.assertEqual(c.add_cached, 2) + + def test_none_cached_property(self): + + class Check(object): + + def __init__(self): + self.total = None + + @threaded_cached_property_with_ttl + def add_cached(self): + return self.total + + c = Check() + + # Run standard cache assertion + self.assertEqual(c.add_cached, None) + + +class TestThreadingIssues(unittest.TestCase): + + def test_threads(self): + """ How well does this implementation work with threads?""" + + class Check(object): + + def __init__(self): + self.total = 0 + self.lock = Lock() + + @threaded_cached_property_with_ttl + def add_cached(self): + sleep(1) + # Need to guard this since += isn't atomic. + with self.lock: + self.total += 1 + return self.total + + c = Check() + threads = [] + for x in range(10): + thread = Thread(target=lambda: c.add_cached) + thread.start() + threads.append(thread) + + for thread in threads: + thread.join() + + self.assertEqual(c.add_cached, 1) \ No newline at end of file diff --git a/tests/test_threaded_cached_property.py b/tests/test_threaded_cached_property.py index 8022104..0d87673 100755 --- a/tests/test_threaded_cached_property.py +++ b/tests/test_threaded_cached_property.py @@ -3,7 +3,6 @@ """ test_threaded_cache_property.py ---------------------------------- - Tests for `cached-property` module, threaded_cache_property. """ @@ -63,7 +62,7 @@ class TestCachedProperty(unittest.TestCase): self.assertEqual(c.add_cached, 1) # Reset the cache. - del c._cache['add_cached'] + del c.add_cached self.assertEqual(c.add_cached, 2) self.assertEqual(c.add_cached, 2)