Make cached_property thread-safe and alias threaded_cached_property to it.
authorGeorge Sakkis <george.sakkis@gmail.com>
Sun, 19 Apr 2015 20:57:59 +0000 (23:57 +0300)
committerGeorge Sakkis <george.sakkis@gmail.com>
Sun, 19 Apr 2015 21:00:49 +0000 (00:00 +0300)
dict.setdefault() is atomic since three years ago; see http://bugs.python.org/issue13521

cached_property.py
tests/test_cached_property.py
tests/test_threaded_cached_property.py [deleted file]

index 25ffe47e5a3ccec278decf87fe7acfe59bab2624..8e07fde3c708fd148c509d99a85bf1430d006e84 100644 (file)
@@ -23,30 +23,11 @@ class cached_property(object):
     def __get__(self, obj, cls):
         if obj is None:
             return self
-        value = obj.__dict__[self.func.__name__] = self.func(obj)
-        return value
-
+        return obj.__dict__.setdefault(self.func.__name__, self.func(obj))
 
-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)
+# Leave for backwards compatibility
+threaded_cached_property = cached_property
 
 
 class cached_property_with_ttl(object):
index 40a5ad9d7378e87eb0972ca15695e73af18e930d..56678cf2c122c9999cb8d179c72fbe4c98af776e 100644 (file)
@@ -1,11 +1,6 @@
 # -*- coding: utf-8 -*-
 
-"""
-tests.py
-----------------------------------
-
-Tests for `cached-property` module.
-"""
+"""Tests for cached_property"""
 
 from time import sleep
 from threading import Lock, Thread
@@ -88,10 +83,7 @@ class TestCachedProperty(unittest.TestCase):
         self.assertEqual(c.add_cached, None)
 
     def test_threads(self):
-        """
-        How well does the standard cached_property implementation work with
-        threads? It doesn't, use threaded_cached_property instead!
-        """
+        """How well does this implementation work with threads?"""
 
         class Check(object):
 
@@ -109,8 +101,7 @@ class TestCachedProperty(unittest.TestCase):
 
         c = Check()
         threads = []
-        num_threads = 10
-        for x in range(num_threads):
+        for x in range(10):
             thread = Thread(target=lambda: c.add_cached)
             thread.start()
             threads.append(thread)
@@ -118,13 +109,4 @@ class TestCachedProperty(unittest.TestCase):
         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)
+        self.assertEqual(c.add_cached, 1)
diff --git a/tests/test_threaded_cached_property.py b/tests/test_threaded_cached_property.py
deleted file mode 100644 (file)
index aa4898c..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-test_threaded_cache_property.py
-----------------------------------
-Tests for `cached-property` module, threaded_cache_property.
-"""
-
-from time import sleep
-from threading import Thread, Lock
-import unittest
-
-from cached_property import threaded_cached_property
-
-
-class TestThreadedCachedProperty(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
-            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
-            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.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
-            def add_cached(self):
-                return self.total
-
-        c = Check()
-
-        # Run standard cache assertion
-        self.assertEqual(c.add_cached, None)
-
-    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
-            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)