Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make cached_property derive from property #137

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ Contributors
* Ionel Cristian Mărieș (@ionelmc)
* Malyshev Artem (@proofit404)
* Volker Braun (@vbraun)
* Vadim Pushtaev (@VadimPushtaev)
* Martin Larralde (@althonos)
110 changes: 66 additions & 44 deletions cached_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,42 @@
__version__ = "1.5.1"
__license__ = "BSD"

from time import time
import functools
import sys
import threading
import weakref
from time import time

try:
import asyncio
except (ImportError, SyntaxError):
asyncio = None


class cached_property(object):
class cached_property(property):
"""
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
""" # noqa

_sentinel = object()

if sys.version_info[0] < 3:

def _update_wrapper(self, func):
self.__doc__ = getattr(func, "__doc__", None)
self.__module__ = getattr(func, "__module__", None)
self.__name__ = getattr(func, "__name__", None)

else:

_update_wrapper = functools.update_wrapper

def __init__(self, func):
self.__doc__ = getattr(func, "__doc__")
self.cache = weakref.WeakKeyDictionary()
self.func = func
self._update_wrapper(func)

def __get__(self, obj, cls):
if obj is None:
Expand All @@ -32,47 +49,59 @@ def __get__(self, obj, cls):
if asyncio and asyncio.iscoroutinefunction(self.func):
return self._wrap_in_coroutine(obj)

value = obj.__dict__[self.func.__name__] = self.func(obj)
value = self.cache.get(obj, self._sentinel)
if value is self._sentinel:
value = self.cache[obj] = self.func(obj)

return value

def __set_name__(self, owner, name):
self.__name__ = name

def __set__(self, obj, value):
self.cache[obj] = value

def __delete__(self, obj):
del self.cache[obj]

def _wrap_in_coroutine(self, obj):

@asyncio.coroutine
def wrapper():
future = asyncio.ensure_future(self.func(obj))
obj.__dict__[self.func.__name__] = future
return future
value = self.cache.get(obj, self._sentinel)
if value is self._sentinel:
self.cache[obj] = value = asyncio.ensure_future(self.func(obj))
return value

return wrapper()


class threaded_cached_property(object):
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):
self.__doc__ = getattr(func, "__doc__")
self.func = func
super(threaded_cached_property, self).__init__(func)
self.lock = threading.RLock()

def __get__(self, obj, cls):
if obj is None:
return self
with self.lock:
return super(threaded_cached_property, self).__get__(obj, cls)

obj_dict = obj.__dict__
name = self.func.__name__
def __set__(self, obj, value):
with self.lock:
try:
# check if the value was computed before the lock was acquired
return obj_dict[name]
super(threaded_cached_property, self).__set__(obj, value)

except KeyError:
# if not, do the calculation and release the lock
return obj_dict.setdefault(name, self.func(obj))
def __delete__(self, obj):
with self.lock:
super(threaded_cached_property, self).__delete__(obj)


class cached_property_with_ttl(object):
class cached_property_with_ttl(cached_property):
"""
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
Expand All @@ -86,52 +115,37 @@ def __init__(self, ttl=None):
else:
func = None
self.ttl = ttl
self._prepare_func(func)
super(cached_property_with_ttl, self).__init__(func)

def __call__(self, func):
self._prepare_func(func)
super(cached_property_with_ttl, self).__init__(func)
return self

def __get__(self, obj, cls):
if obj is None:
return self

now = time()
obj_dict = obj.__dict__
name = self.__name__
try:
value, last_updated = obj_dict[name]
except KeyError:
pass
else:
ttl_expired = self.ttl and self.ttl < now - last_updated
if not ttl_expired:
if obj in self.cache:
value, last_updated = self.cache[obj]
if not self.ttl or self.ttl > now - last_updated:
return value

value = self.func(obj)
obj_dict[name] = (value, now)
value, _ = self.cache[obj] = (self.func(obj), now)
return value

def __delete__(self, obj):
obj.__dict__.pop(self.__name__, None)

def __set__(self, obj, value):
obj.__dict__[self.__name__] = (value, time())

def _prepare_func(self, func):
self.func = func
if func:
self.__doc__ = func.__doc__
self.__name__ = func.__name__
self.__module__ = func.__module__
super(cached_property_with_ttl, self).__set__(obj, (value, time()))


# 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_with_ttl(cached_property_with_ttl):
class threaded_cached_property_with_ttl(
cached_property_with_ttl, threaded_cached_property
):
"""
A cached_property version for use in environments where multiple threads
might concurrently try to access the property.
Expand All @@ -145,6 +159,14 @@ def __get__(self, obj, cls):
with self.lock:
return super(threaded_cached_property_with_ttl, self).__get__(obj, cls)

def __set__(self, obj, value):
with self.lock:
return super(threaded_cached_property_with_ttl, self).__set__(obj, value)

def __delete__(self, obj):
with self.lock:
return super(threaded_cached_property_with_ttl, self).__delete__(obj)


# Alias to make threaded_cached_property_with_ttl easier to use
threaded_cached_property_ttl = threaded_cached_property_with_ttl
Expand Down
40 changes: 40 additions & 0 deletions tests/test_cached_property.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-

import platform
import time
import unittest
from threading import Lock, Thread
Expand Down Expand Up @@ -27,6 +28,8 @@ def add_control(self):

@cached_property_decorator
def add_cached(self):
"""A cached adder.
"""
if threadsafe:
time.sleep(1)
# Need to guard this since += isn't atomic.
Expand Down Expand Up @@ -68,6 +71,12 @@ def assert_cached(self, check, expected):
self.assertEqual(check.add_cached, expected)
self.assertEqual(check.cached_total, expected)

def test_magic_attributes(self):
Check = CheckFactory(self.cached_property_factory)
self.assertEqual(Check.add_cached.__doc__.strip(), "A cached adder.")
self.assertEqual(Check.add_cached.__name__.strip(), "add_cached")
self.assertEqual(Check.add_cached.__module__, __name__)

def test_cached_property(self):
Check = CheckFactory(self.cached_property_factory)
check = Check()
Expand Down Expand Up @@ -143,6 +152,37 @@ def test_threads(self):
self.assert_cached(check, num_threads)
self.assert_cached(check, num_threads)

@unittest.skipUnless(platform.python_implementation() == "CPython",
"unknow garbage collection mechanism")
def test_garbage_collection(self):
Check = CheckFactory(self.cached_property_factory)
check = Check()
check.add_cached = "foo"

# check the instance is in the cache
self.assertIn(check, Check.add_cached.cache)
# remove the only reference to the Check instance
del check
# make sure the cache of the deleted object was removed
self.assertEqual(Check.add_cached.cache, {})

def test_object_independent(self):
Check = CheckFactory(self.cached_property_factory)
check1 = Check()
check2 = Check()

self.assert_cached(check1, 1)
self.assert_cached(check1, 1)
self.assert_cached(check2, 1)
self.assert_cached(check2, 1)

del check1.add_cached

self.assert_cached(check1, 2)
self.assert_cached(check1, 2)
self.assert_cached(check2, 1)
self.assert_cached(check2, 1)


class TestThreadedCachedProperty(TestCachedProperty):
"""Tests for threaded_cached_property"""
Expand Down