| """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 |
| self.__cache = cache |
| self.__key = key |
| self.__lock = lock |
| self.__cond = cond |
|
|
| def __call__(self, *args, **kwargs): |
| raise NotImplementedError() |
|
|
| def cache_clear(self): |
| raise NotImplementedError() |
|
|
| @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: |
| |
| try: |
| |
| |
| wrapper = obj.__dict__.setdefault(self.__attrname, wrapper) |
| except AttributeError: |
| |
| 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 |
| 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 |
|
|
| |
| def __call__(self, *args, **kwargs): |
| _warn_classmethod(stacklevel=3) |
| return self.__wrapper(*args, **kwargs) |
|
|
| |
| def cache_clear(self, objtype): |
| _warn_classmethod(stacklevel=3) |
| return self.__cache_clear(objtype) |
|
|
|
|
| |
| |
| |
| |
|
|
|
|
| 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 |
| 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: |
| |
| |
| |
| return cache.setdefault(key, val) |
| except ValueError: |
| return val |
|
|
| 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 |
| 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): |
| |
| 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 |
| 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) |
|
|
| |
| 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 |
| v = method(self, *args, **kwargs) |
| with lock(self): |
| try: |
| |
| |
| |
| return c.setdefault(k, v) |
| except ValueError: |
| return v |
|
|
| 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) |
|
|
| |
| 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 |
| v = method(self, *args, **kwargs) |
| try: |
| c[k] = v |
| except ValueError: |
| pass |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|