functools.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. from __future__ import annotations
  2. __all__ = (
  3. "AsyncCacheInfo",
  4. "AsyncCacheParameters",
  5. "AsyncLRUCacheWrapper",
  6. "cache",
  7. "lru_cache",
  8. "reduce",
  9. )
  10. import functools
  11. from collections import OrderedDict
  12. from collections.abc import (
  13. AsyncIterable,
  14. Awaitable,
  15. Callable,
  16. Coroutine,
  17. Hashable,
  18. Iterable,
  19. )
  20. from functools import update_wrapper
  21. from inspect import iscoroutinefunction
  22. from typing import (
  23. Any,
  24. Generic,
  25. NamedTuple,
  26. ParamSpec,
  27. TypedDict,
  28. TypeVar,
  29. cast,
  30. final,
  31. overload,
  32. )
  33. from weakref import WeakKeyDictionary
  34. from ._core._eventloop import current_time
  35. from ._core._synchronization import Lock
  36. from .lowlevel import RunVar, checkpoint
  37. T = TypeVar("T")
  38. S = TypeVar("S")
  39. P = ParamSpec("P")
  40. lru_cache_items: RunVar[
  41. WeakKeyDictionary[
  42. AsyncLRUCacheWrapper[Any, Any],
  43. OrderedDict[
  44. Hashable,
  45. tuple[_InitialMissingType, Lock, float | None]
  46. | tuple[Any, None, float | None],
  47. ],
  48. ]
  49. ] = RunVar("lru_cache_items")
  50. class _InitialMissingType:
  51. pass
  52. initial_missing: _InitialMissingType = _InitialMissingType()
  53. class AsyncCacheInfo(NamedTuple):
  54. hits: int
  55. misses: int
  56. maxsize: int | None
  57. currsize: int
  58. ttl: int | None
  59. class AsyncCacheParameters(TypedDict):
  60. maxsize: int | None
  61. typed: bool
  62. always_checkpoint: bool
  63. ttl: int | None
  64. class _LRUMethodWrapper(Generic[T]):
  65. def __init__(self, wrapper: AsyncLRUCacheWrapper[..., T], instance: object):
  66. self.__wrapper = wrapper
  67. self.__instance = instance
  68. def cache_info(self) -> AsyncCacheInfo:
  69. return self.__wrapper.cache_info()
  70. def cache_parameters(self) -> AsyncCacheParameters:
  71. return self.__wrapper.cache_parameters()
  72. def cache_clear(self) -> None:
  73. self.__wrapper.cache_clear()
  74. async def __call__(self, *args: Any, **kwargs: Any) -> T:
  75. if self.__instance is None:
  76. return await self.__wrapper(*args, **kwargs)
  77. return await self.__wrapper(self.__instance, *args, **kwargs)
  78. @final
  79. class AsyncLRUCacheWrapper(Generic[P, T]):
  80. def __init__(
  81. self,
  82. func: Callable[P, Awaitable[T]],
  83. maxsize: int | None,
  84. typed: bool,
  85. always_checkpoint: bool,
  86. ttl: int | None,
  87. ):
  88. self.__wrapped__ = func
  89. self._hits: int = 0
  90. self._misses: int = 0
  91. self._maxsize = max(maxsize, 0) if maxsize is not None else None
  92. self._currsize: int = 0
  93. self._typed = typed
  94. self._always_checkpoint = always_checkpoint
  95. self._ttl = ttl
  96. update_wrapper(self, func)
  97. def cache_info(self) -> AsyncCacheInfo:
  98. return AsyncCacheInfo(
  99. self._hits, self._misses, self._maxsize, self._currsize, self._ttl
  100. )
  101. def cache_parameters(self) -> AsyncCacheParameters:
  102. return {
  103. "maxsize": self._maxsize,
  104. "typed": self._typed,
  105. "always_checkpoint": self._always_checkpoint,
  106. "ttl": self._ttl,
  107. }
  108. def cache_clear(self) -> None:
  109. if cache := lru_cache_items.get(None):
  110. cache.pop(self, None)
  111. self._hits = self._misses = self._currsize = 0
  112. async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
  113. # Easy case first: if maxsize == 0, no caching is done
  114. if self._maxsize == 0:
  115. value = await self.__wrapped__(*args, **kwargs)
  116. self._misses += 1
  117. return value
  118. # The key is constructed as a flat tuple to avoid memory overhead
  119. key: tuple[Any, ...] = args
  120. if kwargs:
  121. # initial_missing is used as a separator
  122. key += (initial_missing,) + sum(kwargs.items(), ())
  123. if self._typed:
  124. key += tuple(type(arg) for arg in args)
  125. if kwargs:
  126. key += (initial_missing,) + tuple(type(val) for val in kwargs.values())
  127. try:
  128. cache = lru_cache_items.get()
  129. except LookupError:
  130. cache = WeakKeyDictionary()
  131. lru_cache_items.set(cache)
  132. try:
  133. cache_entry = cache[self]
  134. except KeyError:
  135. cache_entry = cache[self] = OrderedDict()
  136. cached_value: T | _InitialMissingType
  137. try:
  138. cached_value, lock, expires_at = cache_entry[key]
  139. except KeyError:
  140. # We're the first task to call this function
  141. cached_value, lock, expires_at = (
  142. initial_missing,
  143. Lock(fast_acquire=not self._always_checkpoint),
  144. None,
  145. )
  146. cache_entry[key] = cached_value, lock, expires_at
  147. if lock is None:
  148. if expires_at is not None and current_time() >= expires_at:
  149. self._currsize -= 1
  150. cached_value, lock, expires_at = (
  151. initial_missing,
  152. Lock(fast_acquire=not self._always_checkpoint),
  153. None,
  154. )
  155. cache_entry[key] = cached_value, lock, expires_at
  156. else:
  157. # The value was already cached
  158. self._hits += 1
  159. cache_entry.move_to_end(key)
  160. if self._always_checkpoint:
  161. await checkpoint()
  162. return cast(T, cached_value)
  163. async with lock:
  164. # Check if another task filled the cache while we acquired the lock
  165. if (cached_value := cache_entry[key][0]) is initial_missing:
  166. self._misses += 1
  167. if self._maxsize is not None and self._currsize >= self._maxsize:
  168. cache_entry.popitem(last=False)
  169. else:
  170. self._currsize += 1
  171. value = await self.__wrapped__(*args, **kwargs)
  172. expires_at = (
  173. current_time() + self._ttl if self._ttl is not None else None
  174. )
  175. cache_entry[key] = value, None, expires_at
  176. else:
  177. # Another task filled the cache while we were waiting for the lock
  178. self._hits += 1
  179. cache_entry.move_to_end(key)
  180. value = cast(T, cached_value)
  181. return value
  182. def __get__(
  183. self, instance: object, owner: type | None = None
  184. ) -> _LRUMethodWrapper[T]:
  185. wrapper = _LRUMethodWrapper(self, instance)
  186. update_wrapper(wrapper, self.__wrapped__)
  187. return wrapper
  188. class _LRUCacheWrapper:
  189. def __init__(
  190. self, maxsize: int | None, typed: bool, always_checkpoint: bool, ttl: int | None
  191. ):
  192. self._maxsize = maxsize
  193. self._typed = typed
  194. self._always_checkpoint = always_checkpoint
  195. self._ttl = ttl
  196. @overload
  197. def __call__( # type: ignore[overload-overlap]
  198. self, func: Callable[P, Coroutine[Any, Any, T]], /
  199. ) -> AsyncLRUCacheWrapper[P, T]: ...
  200. @overload
  201. def __call__(
  202. self, func: Callable[..., T], /
  203. ) -> functools._lru_cache_wrapper[T]: ...
  204. def __call__(
  205. self, f: Callable[P, Coroutine[Any, Any, T]] | Callable[..., T], /
  206. ) -> AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T]:
  207. if iscoroutinefunction(f):
  208. return AsyncLRUCacheWrapper(
  209. f, self._maxsize, self._typed, self._always_checkpoint, self._ttl
  210. )
  211. return functools.lru_cache(maxsize=self._maxsize, typed=self._typed)(f) # type: ignore[arg-type]
  212. @overload
  213. def cache( # type: ignore[overload-overlap]
  214. func: Callable[P, Coroutine[Any, Any, T]], /
  215. ) -> AsyncLRUCacheWrapper[P, T]: ...
  216. @overload
  217. def cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ...
  218. def cache(func: Callable[..., Any] | Callable[P, Coroutine[Any, Any, Any]], /) -> Any:
  219. """
  220. A convenient shortcut for :func:`lru_cache` with ``maxsize=None``.
  221. This is the asynchronous equivalent to :func:`functools.cache`.
  222. """
  223. return lru_cache(maxsize=None)(func)
  224. @overload
  225. def lru_cache(
  226. *,
  227. maxsize: int | None = ...,
  228. typed: bool = ...,
  229. always_checkpoint: bool = ...,
  230. ttl: int | None = ...,
  231. ) -> _LRUCacheWrapper: ...
  232. @overload
  233. def lru_cache( # type: ignore[overload-overlap]
  234. func: Callable[P, Coroutine[Any, Any, T]], /
  235. ) -> AsyncLRUCacheWrapper[P, T]: ...
  236. @overload
  237. def lru_cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ...
  238. def lru_cache(
  239. func: Callable[..., Coroutine[Any, Any, Any]] | Callable[..., Any] | None = None,
  240. /,
  241. *,
  242. maxsize: int | None = 128,
  243. typed: bool = False,
  244. always_checkpoint: bool = False,
  245. ttl: int | None = None,
  246. ) -> Any:
  247. """
  248. An asynchronous version of :func:`functools.lru_cache`.
  249. If a synchronous function is passed, the standard library
  250. :func:`functools.lru_cache` is applied instead.
  251. :param always_checkpoint: if ``True``, every call to the cached function will be
  252. guaranteed to yield control to the event loop at least once
  253. :param ttl: time in seconds after which to invalidate cache entries
  254. .. note:: Caches and locks are managed on a per-event loop basis.
  255. """
  256. if func is None:
  257. return _LRUCacheWrapper(maxsize, typed, always_checkpoint, ttl)
  258. if not callable(func):
  259. raise TypeError("the first argument must be callable")
  260. return _LRUCacheWrapper(maxsize, typed, always_checkpoint, ttl)(func)
  261. @overload
  262. async def reduce(
  263. function: Callable[[T, S], Awaitable[T]],
  264. iterable: Iterable[S] | AsyncIterable[S],
  265. /,
  266. initial: T,
  267. ) -> T: ...
  268. @overload
  269. async def reduce(
  270. function: Callable[[T, T], Awaitable[T]],
  271. iterable: Iterable[T] | AsyncIterable[T],
  272. /,
  273. ) -> T: ...
  274. async def reduce( # type: ignore[misc]
  275. function: Callable[[T, T], Awaitable[T]] | Callable[[T, S], Awaitable[T]],
  276. iterable: Iterable[T] | Iterable[S] | AsyncIterable[T] | AsyncIterable[S],
  277. /,
  278. initial: T | _InitialMissingType = initial_missing,
  279. ) -> T:
  280. """
  281. Asynchronous version of :func:`functools.reduce`.
  282. :param function: a coroutine function that takes two arguments: the accumulated
  283. value and the next element from the iterable
  284. :param iterable: an iterable or async iterable
  285. :param initial: the initial value (if missing, the first element of the iterable is
  286. used as the initial value)
  287. """
  288. element: Any
  289. function_called = False
  290. if isinstance(iterable, AsyncIterable):
  291. async_it = iterable.__aiter__()
  292. if initial is initial_missing:
  293. try:
  294. value = cast(T, await async_it.__anext__())
  295. except StopAsyncIteration:
  296. raise TypeError(
  297. "reduce() of empty sequence with no initial value"
  298. ) from None
  299. else:
  300. value = cast(T, initial)
  301. async for element in async_it:
  302. value = await function(value, element)
  303. function_called = True
  304. elif isinstance(iterable, Iterable):
  305. it = iter(iterable)
  306. if initial is initial_missing:
  307. try:
  308. value = cast(T, next(it))
  309. except StopIteration:
  310. raise TypeError(
  311. "reduce() of empty sequence with no initial value"
  312. ) from None
  313. else:
  314. value = cast(T, initial)
  315. for element in it:
  316. value = await function(value, element)
  317. function_called = True
  318. else:
  319. raise TypeError("reduce() argument 2 must be an iterable or async iterable")
  320. # Make sure there is at least one checkpoint, even if an empty iterable and an
  321. # initial value were given
  322. if not function_called:
  323. await checkpoint()
  324. return value