Skip to content

Commit 963fa2c

Browse files
committed
Adding cached_classproperty allows for singletons
1 parent d4d48d2 commit 963fa2c

File tree

3 files changed

+154
-0
lines changed

3 files changed

+154
-0
lines changed

README.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,35 @@ Now use it:
182182
**Note:** The ``ttl`` tools do not reliably allow the clearing of the cache. This
183183
is why they are broken out into seperate tools. See https://github.com/pydanny/cached-property/issues/16.
184184

185+
Working with a Class Property
186+
-----------------------------
187+
188+
What if you want to cache a property accross different instances, you can use
189+
``cached_classproperty``. Note that cached_classproperty cannot be invalidated.
190+
191+
.. code-block:: python
192+
193+
from cached_property import cached_classproperty
194+
195+
class Monopoly(object):
196+
197+
boardwalk_price = 500
198+
199+
@cached_classproperty
200+
def boardwalk(cls):
201+
cls.boardwalk_price += 50
202+
return cls.boardwalk_price
203+
204+
Now use it:
205+
206+
.. code-block:: python
207+
208+
>>> Monopoly().boardwalk
209+
550
210+
>>> Monopoly().boardwalk
211+
550
212+
213+
185214
Credits
186215
--------
187216

cached_property.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,24 @@ def __get__(self, obj, cls):
129129
# Alias to make threaded_cached_property_with_ttl easier to use
130130
threaded_cached_property_ttl = threaded_cached_property_with_ttl
131131
timed_threaded_cached_property = threaded_cached_property_with_ttl
132+
133+
134+
class cached_classproperty(object):
135+
"""
136+
A property that is only computed once per class and then replaces
137+
itself with an ordinary attribute. Deleting the attribute resets the
138+
property.
139+
"""
140+
141+
def __init__(self, func):
142+
self.__doc__ = getattr(func, '__doc__')
143+
self.func = func
144+
145+
def __get__(self, obj, cls):
146+
if obj is None and cls is None:
147+
return self
148+
if cls is None:
149+
cls = type(obj)
150+
value = self.func(cls)
151+
setattr(cls, self.func.__name__, value)
152+
return value

tests/test_cached_classproperty.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import time
4+
import unittest
5+
from threading import Lock, Thread
6+
from freezegun import freeze_time
7+
8+
import cached_property
9+
10+
11+
def CheckFactory(cached_property_decorator, threadsafe=False):
12+
"""
13+
Create dynamically a Check class whose add_cached method is decorated by
14+
the cached_property_decorator.
15+
"""
16+
17+
class Check(object):
18+
19+
cached_total = 0
20+
lock = Lock()
21+
22+
@cached_property_decorator
23+
def add_cached(cls):
24+
if threadsafe:
25+
time.sleep(1)
26+
# Need to guard this since += isn't atomic.
27+
with cls.lock:
28+
cls.cached_total += 1
29+
else:
30+
cls.cached_total += 1
31+
return cls.cached_total
32+
33+
def run_threads(self, num_threads):
34+
threads = []
35+
for _ in range(num_threads):
36+
thread = Thread(target=lambda: self.add_cached)
37+
thread.start()
38+
threads.append(thread)
39+
for thread in threads:
40+
thread.join()
41+
42+
return Check
43+
44+
45+
class TestCachedClassProperty(unittest.TestCase):
46+
"""Tests for cached_property"""
47+
48+
cached_property_factory = cached_property.cached_classproperty
49+
50+
def assert_cached(self, check, expected):
51+
"""
52+
Assert that both `add_cached` and 'cached_total` equal `expected`
53+
"""
54+
self.assertEqual(check.add_cached, expected)
55+
self.assertEqual(check.cached_total, expected)
56+
57+
def test_cached_property(self):
58+
Check = CheckFactory(self.cached_property_factory)
59+
60+
# The cached version demonstrates how nothing is added after the first
61+
self.assert_cached(Check(), 1)
62+
self.assert_cached(Check(), 1)
63+
64+
# The cache does not expire
65+
with freeze_time("9999-01-01"):
66+
self.assert_cached(Check(), 1)
67+
68+
def test_none_cached_property(self):
69+
class Check(object):
70+
71+
cached_total = None
72+
73+
@self.cached_property_factory
74+
def add_cached(cls):
75+
return cls.cached_total
76+
77+
self.assert_cached(Check(), None)
78+
79+
def test_set_cached_property(self):
80+
Check = CheckFactory(self.cached_property_factory)
81+
Check.add_cached = 'foo'
82+
self.assertEqual(Check().add_cached, 'foo')
83+
self.assertEqual(Check().cached_total, 0)
84+
85+
def test_threads(self):
86+
Check = CheckFactory(self.cached_property_factory, threadsafe=True)
87+
num_threads = 5
88+
89+
# cached_property_with_ttl is *not* thread-safe!
90+
Check().run_threads(num_threads)
91+
# This assertion hinges on the fact the system executing the test can
92+
# spawn and start running num_threads threads within the sleep period
93+
# (defined in the Check class as 1 second). If num_threads were to be
94+
# massively increased (try 10000), the actual value returned would be
95+
# between 1 and num_threads, depending on thread scheduling and
96+
# preemption.
97+
self.assert_cached(Check(), num_threads)
98+
self.assert_cached(Check(), num_threads)
99+
100+
# The cache does not expire
101+
with freeze_time("9999-01-01"):
102+
Check().run_threads(num_threads)
103+
self.assert_cached(Check(), num_threads)
104+
self.assert_cached(Check(), num_threads)

0 commit comments

Comments
 (0)