Let's convert the boardwalk property into a ``cached_property``.
-
.. code-block:: python
from cached_property import cached_property
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 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
Working with Threads
---------------------
>>> 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
+
+**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
--------
""" 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):
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):
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
+
from time import sleep
from threading import Lock, Thread
import unittest
-from freezegun import freeze_time
from cached_property import cached_property
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))
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)
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):
# 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)
--- /dev/null
+# -*- 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
"""
test_threaded_cache_property.py
----------------------------------
-
Tests for `cached-property` module, threaded_cache_property.
"""
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)