server / source /cachetools /_cachedmethod.py
Harmony18090's picture
Add source batch 2/11
76f9669 verified
raw
history blame
13.9 kB
"""Method decorator helpers."""
__all__ = ()
import functools
import warnings
import weakref
def _warn_classmethod(stacklevel):
warnings.warn(
"decorating class methods with @cachedmethod is deprecated",
DeprecationWarning,
stacklevel=stacklevel,
)
def _warn_instance_dict(msg, stacklevel):
warnings.warn(
msg,
DeprecationWarning,
stacklevel=stacklevel,
)
class _WrapperBase:
"""Wrapper base class providing default implementations for properties."""
def __init__(self, obj, method, cache, key, lock=None, cond=None):
if isinstance(obj, type):
_warn_classmethod(stacklevel=5)
functools.update_wrapper(self, method)
self._obj = obj # protected
self.__cache = cache
self.__key = key
self.__lock = lock
self.__cond = cond
def __call__(self, *args, **kwargs):
raise NotImplementedError() # pragma: no cover
def cache_clear(self):
raise NotImplementedError() # pragma: no cover
@property
def cache(self):
return self.__cache(self._obj)
@property
def cache_key(self):
return self.__key
@property
def cache_lock(self):
return None if self.__lock is None else self.__lock(self._obj)
@property
def cache_condition(self):
return None if self.__cond is None else self.__cond(self._obj)
class _DescriptorBase:
"""Descriptor base class implementing the basic descriptor protocol."""
def __init__(self, deprecated=False):
self.__attrname = None
self.__deprecated = deprecated
def __set_name__(self, owner, name):
if self.__attrname is None:
self.__attrname = name
elif name != self.__attrname:
raise TypeError(
"Cannot assign the same @cachedmethod to two different names "
f"({self.__attrname!r} and {name!r})."
)
def __get__(self, obj, objtype=None):
wrapper = self.Wrapper(obj)
if self.__attrname is not None:
# replace descriptor instance with wrapper in instance dict
try:
# In case of a race condition where another thread already replaced
# the descriptor, prefer the initial wrapper.
wrapper = obj.__dict__.setdefault(self.__attrname, wrapper)
except AttributeError:
# not all objects have __dict__ (e.g. class defines slots)
msg = (
f"No '__dict__' attribute on {type(obj).__name__!r} "
f"instance to cache {self.__attrname!r} property."
)
if self.__deprecated:
_warn_instance_dict(msg, 3)
else:
raise TypeError(msg) from None
except TypeError:
msg = (
f"The '__dict__' attribute on {type(obj).__name__!r} "
f"instance does not support item assignment for "
f"caching {self.__attrname!r} property."
)
if self.__deprecated:
_warn_instance_dict(msg, 3)
else:
raise TypeError(msg) from None
elif self.__deprecated:
pass # deprecated @classmethod, warning already raised elsewhere
else:
msg = "Cannot use @cachedmethod instance without calling __set_name__ on it"
raise TypeError(msg) from None
return wrapper
class _DeprecatedDescriptorBase(_DescriptorBase):
"""Descriptor base class supporting deprecated @classmethod use."""
def __init__(self, wrapper, cache_clear):
super().__init__(deprecated=True)
self.__wrapper = wrapper
self.__cache_clear = cache_clear
# called for @classmethod with Python >= 3.13
def __call__(self, *args, **kwargs):
_warn_classmethod(stacklevel=3)
return self.__wrapper(*args, **kwargs)
# backward-compatible @classmethod handling with Python >= 3.13
def cache_clear(self, objtype):
_warn_classmethod(stacklevel=3)
return self.__cache_clear(objtype)
# At least for now, the implementation prefers clarity and performance
# over ease of maintenance, thus providing separate descriptors for
# all valid combinations of decorator parameters lock, condition and
# info.
def _condition_info(method, cache, key, lock, cond, info):
class Descriptor(_DescriptorBase):
class Wrapper(_WrapperBase):
def __init__(self, obj):
super().__init__(obj, method, cache, key, lock, cond)
self.__hits = self.__misses = 0
self.__pending = set()
def __call__(self, *args, **kwargs):
cache = self.cache
lock = self.cache_lock
cond = self.cache_condition
key = self.cache_key(self._obj, *args, **kwargs)
with lock:
cond.wait_for(lambda: key not in self.__pending)
try:
result = cache[key]
self.__hits += 1
return result
except KeyError:
self.__pending.add(key)
self.__misses += 1
try:
val = method(self._obj, *args, **kwargs)
with lock:
try:
cache[key] = val
except ValueError:
pass # value too large
return val
finally:
with lock:
self.__pending.remove(key)
cond.notify_all()
def cache_clear(self):
with self.cache_lock:
self.cache.clear()
self.__hits = self.__misses = 0
def cache_info(self):
with self.cache_lock:
return info(self.cache, self.__hits, self.__misses)
return Descriptor()
def _locked_info(method, cache, key, lock, info):
class Descriptor(_DescriptorBase):
class Wrapper(_WrapperBase):
def __init__(self, obj):
super().__init__(obj, method, cache, key, lock)
self.__hits = self.__misses = 0
def __call__(self, *args, **kwargs):
cache = self.cache
lock = self.cache_lock
key = self.cache_key(self._obj, *args, **kwargs)
with lock:
try:
result = cache[key]
self.__hits += 1
return result
except KeyError:
self.__misses += 1
val = method(self._obj, *args, **kwargs)
with lock:
try:
# In case of a race condition, i.e. if another thread
# stored a value for this key while we were calling
# method(), prefer the cached value.
return cache.setdefault(key, val)
except ValueError:
return val # value too large
def cache_clear(self):
with self.cache_lock:
self.cache.clear()
self.__hits = self.__misses = 0
def cache_info(self):
with self.cache_lock:
return info(self.cache, self.__hits, self.__misses)
return Descriptor()
def _unlocked_info(method, cache, key, info):
class Descriptor(_DescriptorBase):
class Wrapper(_WrapperBase):
def __init__(self, obj):
super().__init__(obj, method, cache, key)
self.__hits = self.__misses = 0
def __call__(self, *args, **kwargs):
cache = self.cache
key = self.cache_key(self._obj, *args, **kwargs)
try:
result = cache[key]
self.__hits += 1
return result
except KeyError:
self.__misses += 1
val = method(self._obj, *args, **kwargs)
try:
cache[key] = val
except ValueError:
pass # value too large
return val
def cache_clear(self):
self.cache.clear()
self.__hits = self.__misses = 0
def cache_info(self):
return info(self.cache, self.__hits, self.__misses)
return Descriptor()
def _condition(method, cache, key, lock, cond):
# backward-compatible weakref dictionary for Python >= 3.13
pending = weakref.WeakKeyDictionary()
def wrapper(self, pending, *args, **kwargs):
c = cache(self)
k = key(self, *args, **kwargs)
with lock(self):
cond(self).wait_for(lambda: k not in pending)
try:
return c[k]
except KeyError:
pending.add(k)
try:
v = method(self, *args, **kwargs)
with lock(self):
try:
c[k] = v
except ValueError:
pass # value too large
return v
finally:
with lock(self):
pending.remove(k)
cond(self).notify_all()
def cache_clear(self):
c = cache(self)
with lock(self):
c.clear()
def classmethod_wrapper(self, *args, **kwargs):
p = pending.setdefault(self, set())
return wrapper(self, p, *args, **kwargs)
class Descriptor(_DeprecatedDescriptorBase):
class Wrapper(_WrapperBase):
def __init__(self, obj):
super().__init__(obj, method, cache, key, lock, cond)
self.__pending = set()
def __call__(self, *args, **kwargs):
return wrapper(self._obj, self.__pending, *args, **kwargs)
# objtype: backward-compatible @classmethod handling with Python < 3.13
def cache_clear(self, _objtype=None):
return cache_clear(self._obj)
return Descriptor(classmethod_wrapper, cache_clear)
def _locked(method, cache, key, lock):
def wrapper(self, *args, **kwargs):
c = cache(self)
k = key(self, *args, **kwargs)
with lock(self):
try:
return c[k]
except KeyError:
pass # key not found
v = method(self, *args, **kwargs)
with lock(self):
try:
# In case of a race condition, i.e. if another thread
# stored a value for this key while we were calling
# method(), prefer the cached value.
return c.setdefault(k, v)
except ValueError:
return v # value too large
def cache_clear(self):
c = cache(self)
with lock(self):
c.clear()
class Descriptor(_DeprecatedDescriptorBase):
class Wrapper(_WrapperBase):
def __init__(self, obj):
super().__init__(obj, method, cache, key, lock)
def __call__(self, *args, **kwargs):
return wrapper(self._obj, *args, **kwargs)
# objtype: backward-compatible @classmethod handling with Python < 3.13
def cache_clear(self, _objtype=None):
return cache_clear(self._obj)
return Descriptor(wrapper, cache_clear)
def _unlocked(method, cache, key):
def wrapper(self, *args, **kwargs):
c = cache(self)
k = key(self, *args, **kwargs)
try:
return c[k]
except KeyError:
pass # key not found
v = method(self, *args, **kwargs)
try:
c[k] = v
except ValueError:
pass # value too large
return v
def cache_clear(self):
c = cache(self)
c.clear()
class Descriptor(_DeprecatedDescriptorBase):
class Wrapper(_WrapperBase):
def __init__(self, obj):
super().__init__(obj, method, cache, key)
def __call__(self, *args, **kwargs):
return wrapper(self._obj, *args, **kwargs)
# objtype: backward-compatible @classmethod handling with Python < 3.13
def cache_clear(self, _objtype=None):
return cache_clear(self._obj)
return Descriptor(wrapper, cache_clear)
def _wrapper(method, cache, key, lock=None, cond=None, info=None):
if info is not None:
if cond is not None and lock is not None:
wrapper = _condition_info(method, cache, key, lock, cond, info)
elif cond is not None:
wrapper = _condition_info(method, cache, key, cond, cond, info)
elif lock is not None:
wrapper = _locked_info(method, cache, key, lock, info)
else:
wrapper = _unlocked_info(method, cache, key, info)
else:
if cond is not None and lock is not None:
wrapper = _condition(method, cache, key, lock, cond)
elif cond is not None:
wrapper = _condition(method, cache, key, cond, cond)
elif lock is not None:
wrapper = _locked(method, cache, key, lock)
else:
wrapper = _unlocked(method, cache, key)
# backward-compatible properties for deprecated @classmethod use
wrapper.cache = cache
wrapper.cache_key = key
wrapper.cache_lock = lock if lock is not None else cond
wrapper.cache_condition = cond
return functools.update_wrapper(wrapper, method)