diff --git a/boltons/cacheutils.py b/boltons/cacheutils.py index 780530e..8df1470 100644 --- a/boltons/cacheutils.py +++ b/boltons/cacheutils.py @@ -572,6 +572,22 @@ def cachedmethod(cache, typed=False, selfish=True): return cached_method_decorator +class cachedproperty(object): + def __init__(self, func): + self.func = func + + def __get__(self, obj, objtype=None): + if obj is None: + return self + value = self.func(obj) + setattr(obj, self.func.__name__, value) + return value + + def __repr__(self): + cn = self.__class__.__name__ + return '<%s func=%s>' % (cn, self.func) + + class ThresholdCounter(object): """A **bounded** dict-like Mapping from keys to counts. The ThresholdCounter automatically compacts after every (1 / diff --git a/tests/test_cacheutils.py b/tests/test_cacheutils.py index 473069f..0312769 100644 --- a/tests/test_cacheutils.py +++ b/tests/test_cacheutils.py @@ -2,7 +2,7 @@ import string -from boltons.cacheutils import LRU, LRI, cached, cachedmethod +from boltons.cacheutils import LRU, LRI, cached, cachedmethod, cachedproperty class CountingCallable(object): @@ -242,3 +242,31 @@ def test_cachedmethod(): print(repr(car_two.door)) print(repr(Car.door)) return + + +def test_cachedproperty(): + class Proper(object): + def __init__(self): + self.expensive_func = CountingCallable() + + @cachedproperty + def useful_attr(self): + return self.expensive_func() + + prop = Proper() + + assert prop.expensive_func.call_count == 0 + assert prop.useful_attr == 1 + assert prop.expensive_func.call_count == 1 + assert prop.useful_attr == 1 + assert prop.expensive_func.call_count == 1 + + prop.useful_attr += 1 # would not be possible with normal properties + assert prop.useful_attr == 2 + + delattr(prop, 'useful_attr') + assert prop.expensive_func.call_count == 1 + assert prop.useful_attr + assert prop.expensive_func.call_count == 2 + + repr(Proper.useful_attr)