Skip to content

Commit

Permalink
Can cache properties with mangled names
Browse files Browse the repository at this point in the history
  • Loading branch information
AWhetter committed Jul 13, 2019
1 parent b4cc28f commit 0216c6a
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 69 deletions.
25 changes: 25 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,31 @@ Now when we run it the price stays at $550.
Why doesn't the value of ``monopoly.boardwalk`` change? Because it's a **cached property**!

Decorating Mangled Properties
-----------------------------

When decorating properties that have their name mangled,
``cached_property`` will attempt to figure out the mangled name.
If you don't want this to happen, or ``cached_property`` guesses the wrong name
then you can specify the name yourself.

.. code-block:: python
from cached_property import cached_property
class Monopoly(object):
def __init__(self):
self.boardwalk_price = 500
@cached_property(name="_Monopoly__private_boardwalk")
def __private_boardwalk(self):
# Again, this is a silly example. Don't worry about it, this is
# just an example for clarity.
self.boardwalk_price += 50
return self.boardwalk_price
Invalidating the Cache
----------------------

Expand Down
98 changes: 85 additions & 13 deletions cached_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
__version__ = "1.5.1"
__license__ = "BSD"

import inspect
from time import time
import threading

Expand All @@ -14,54 +15,104 @@
asyncio = None


def _is_mangled(name):
"""Check whether a name will be mangled when accessed as an attribute."""
return name.startswith("__") and not name.endswith("__")


def _resolve_name(klass, default=None):
"""Get the name of a function used to access it from an object."""
name = default

if _is_mangled(name) and klass:
for base_cls in inspect.getmro(klass):
resolved_name = "_{}{}".format(base_cls.__name__, name)
if resolved_name in base_cls.__dict__:
name = resolved_name
break

return name


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

def __init__(self, func):
self.__doc__ = getattr(func, "__doc__")
self.func = func
def __init__(self, func, name=None):
if not callable(func):
func = None
name = func
self._name = name
self._prepare_func(func)

def __call__(self, func):
self._prepare_func(func)
return self

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

name = self._name
if not name:
name = _resolve_name(cls, self.func.__name__)
self._name = name

if asyncio and asyncio.iscoroutinefunction(self.func):
return self._wrap_in_coroutine(obj)
return self._wrap_in_coroutine(obj, name)

value = obj.__dict__[self.func.__name__] = self.func(obj)
value = obj.__dict__[name] = self.func(obj)
return value

def _wrap_in_coroutine(self, obj):
def _wrap_in_coroutine(self, obj, name):
@asyncio.coroutine
def wrapper():
future = asyncio.ensure_future(self.func(obj))
obj.__dict__[self.func.__name__] = future
obj.__dict__[name] = future
return future

return wrapper()

def _prepare_func(self, func):
self.func = func
if func:
self.__doc__ = func.__doc__
self.__name__ = func.__name__
self.__module__ = func.__module__


class threaded_cached_property(object):
"""
A cached_property version for use in environments where multiple threads
might concurrently try to access the property.
"""

def __init__(self, func):
def __init__(self, func, name=None):
if not callable(func):
func = None
name = func
self.__doc__ = getattr(func, "__doc__")
self.func = func
self._name = name
self.lock = threading.RLock()
self._prepare_func(func)

def __call__(self, func):
self._prepare_func(func)
return self

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

obj_dict = obj.__dict__
name = self.func.__name__
name = self._name
if not name:
name = _resolve_name(cls, self.func.__name__)
self._name = name
with self.lock:
try:
# check if the value was computed before the lock was acquired
Expand All @@ -71,6 +122,13 @@ def __get__(self, obj, cls):
# if not, do the calculation and release the lock
return obj_dict.setdefault(name, self.func(obj))

def _prepare_func(self, func):
self.func = func
if func:
self.__doc__ = func.__doc__
self.__name__ = func.__name__
self.__module__ = func.__module__


class cached_property_with_ttl(object):
"""
Expand All @@ -79,13 +137,14 @@ class cached_property_with_ttl(object):
the property will last before being timed out.
"""

def __init__(self, ttl=None):
def __init__(self, ttl=None, name=None):
if callable(ttl):
func = ttl
ttl = None
else:
func = None
self.ttl = ttl
self._name = name
self._prepare_func(func)

def __call__(self, func):
Expand All @@ -98,7 +157,10 @@ def __get__(self, obj, cls):

now = time()
obj_dict = obj.__dict__
name = self.__name__
name = self._name
if not name:
name = _resolve_name(cls, self.__name__)
self._name = name
try:
value, last_updated = obj_dict[name]
except KeyError:
Expand All @@ -113,10 +175,20 @@ def __get__(self, obj, cls):
return value

def __delete__(self, obj):
obj.__dict__.pop(self.__name__, None)
name = self._name
if not name:
cls = getattr(obj, "__class__", None)
name = _resolve_name(cls, self.__name__)
self._name = name
obj.__dict__.pop(name, None)

def __set__(self, obj, value):
obj.__dict__[self.__name__] = (value, time())
name = self._name
if not name:
cls = getattr(obj, "__class__", None)
name = _resolve_name(cls, self.__name__)
self._name = name
obj.__dict__[name] = (value, time())

def _prepare_func(self, func):
self.func = func
Expand Down
1 change: 0 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import sys

# Whether "import asyncio" works
Expand Down
Loading

0 comments on commit 0216c6a

Please sign in to comment.