_functools.py 1.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
  1. import functools
  2. from collections.abc import Callable
  3. from typing import Concatenate, TypeVar
  4. from typing_extensions import ParamSpec
  5. _P = ParamSpec("_P")
  6. _T = TypeVar("_T")
  7. _C = TypeVar("_C")
  8. # Sentinel used to indicate that cache lookup failed.
  9. _cache_sentinel = object()
  10. def cache_method(
  11. f: Callable[Concatenate[_C, _P], _T],
  12. ) -> Callable[Concatenate[_C, _P], _T]:
  13. """
  14. Like `@functools.cache` but for methods.
  15. `@functools.cache` (and similarly `@functools.lru_cache`) shouldn't be used
  16. on methods because it caches `self`, keeping it alive
  17. forever. `@cache_method` ignores `self` so won't keep `self` alive (assuming
  18. no cycles with `self` in the parameters).
  19. Footgun warning: This decorator completely ignores self's properties so only
  20. use it when you know that self is frozen or won't change in a meaningful
  21. way (such as the wrapped function being pure).
  22. """
  23. cache_name = "_cache_method_" + f.__name__
  24. @functools.wraps(f)
  25. def wrap(self: _C, *args: _P.args, **kwargs: _P.kwargs) -> _T:
  26. if kwargs:
  27. raise AssertionError("cache_method does not accept keyword arguments")
  28. if not (cache := getattr(self, cache_name, None)):
  29. cache = {}
  30. setattr(self, cache_name, cache)
  31. # pyrefly: ignore [unbound-name]
  32. cached_value = cache.get(args, _cache_sentinel)
  33. if cached_value is not _cache_sentinel:
  34. return cached_value
  35. value = f(self, *args, **kwargs)
  36. # pyrefly: ignore [unbound-name]
  37. cache[args] = value
  38. return value
  39. return wrap