From 2532cc3e547cb6dbeccc4f597a435cc7137f3d76 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Sun, 14 Sep 2025 05:11:09 +0000 Subject: [PATCH 1/5] feat: dont reorganize __cache if unbounded --- async_lru/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/async_lru/__init__.py b/async_lru/__init__.py index 447e9cd..e70ec8e 100644 --- a/async_lru/__init__.py +++ b/async_lru/__init__.py @@ -162,7 +162,8 @@ def cache_parameters(self) -> _CacheParameters: def _cache_hit(self, key: Hashable) -> None: self.__hits += 1 - self.__cache.move_to_end(key) + if self.__maxsize is not None: + self.__cache.move_to_end(key) def _cache_miss(self, key: Hashable) -> None: self.__misses += 1 From 0dae56958244e90883c24861dc46c5fdfc364f43 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Fri, 7 Nov 2025 06:38:03 -0400 Subject: [PATCH 2/5] Update __init__.py --- async_lru/__init__.py | 47 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/async_lru/__init__.py b/async_lru/__init__.py index e70ec8e..a3029e2 100644 --- a/async_lru/__init__.py +++ b/async_lru/__init__.py @@ -63,7 +63,6 @@ def cancel(self) -> None: self.later_call = None -@final class _LRUCacheWrapper(Generic[_R]): def __init__( self, @@ -162,8 +161,7 @@ def cache_parameters(self) -> _CacheParameters: def _cache_hit(self, key: Hashable) -> None: self.__hits += 1 - if self.__maxsize is not None: - self.__cache.move_to_end(key) + self.__cache.move_to_end(key) def _cache_miss(self, key: Hashable) -> None: self.__misses += 1 @@ -233,6 +231,11 @@ def __get__( else: return _LRUCacheWrapperInstanceMethod(self, instance) +@final +class _LRUCacheWrapperUnbounded(_LRUCacheWrapperBase[_R]): + def _cache_hit(self, key: Hashable) -> None: + self.__hits += 1 + @final class _LRUCacheWrapperInstanceMethod(Generic[_R, _T]): @@ -294,11 +297,28 @@ async def __call__(self, /, *fn_args: Any, **fn_kwargs: Any) -> _R: return await self.__wrapper(self.__instance, *fn_args, **fn_kwargs) +@overload +def _make_wrapper( + maxsize: int, + typed: bool, + ttl: Optional[float] = None, +) -> Callable[[_CBP[_R]], _LRUCacheWrapper[_R]]: ... + +@overload +def _make_wrapper( + maxsize: Literal[None], + typed: bool, + ttl: Optional[float] = None, +) -> Callable[[_CBP[_R]], _LRUCacheWrapperUnbounded[_R]]: ... + def _make_wrapper( maxsize: Optional[int], typed: bool, ttl: Optional[float] = None, -) -> Callable[[_CBP[_R]], _LRUCacheWrapper[_R]]: +) -> Union[ + Callable[[_CBP[_R]], _LRUCacheWrapper[_R]], + Callable[[_CBP[_R]], _LRUCacheWrapperUnbounded[_R]], +]: def wrapper(fn: _CBP[_R]) -> _LRUCacheWrapper[_R]: origin = fn @@ -312,7 +332,8 @@ def wrapper(fn: _CBP[_R]) -> _LRUCacheWrapper[_R]: if hasattr(fn, "_make_unbound_method"): fn = fn._make_unbound_method() - wrapper = _LRUCacheWrapper(cast(_CB[_R], fn), maxsize, typed, ttl) + wrapper_cls = _LRUCacheWrapperUnbounded if maxsize is None else _LRUCacheWrapper + wrapper = wrapper_cls(cast(_CB[_R], fn), maxsize, typed, ttl) if sys.version_info >= (3, 12): wrapper = inspect.markcoroutinefunction(wrapper) return wrapper @@ -320,6 +341,16 @@ def wrapper(fn: _CBP[_R]) -> _LRUCacheWrapper[_R]: return wrapper +@overload +def alru_cache( + maxsize: Literal[None], + typed: bool = False, + *, + ttl: Optional[float] = None, +) -> Callable[[_CBP[_R]], _LRUCacheWrapperUnbounded[_R]]: + ... + + @overload def alru_cache( maxsize: Optional[int] = 128, @@ -343,7 +374,11 @@ def alru_cache( typed: bool = False, *, ttl: Optional[float] = None, -) -> Union[Callable[[_CBP[_R]], _LRUCacheWrapper[_R]], _LRUCacheWrapper[_R]]: +) -> Union[ + Callable[[_CBP[_R]], _LRUCacheWrapper[_R]], + Callable[[_CBP[_R]], _LRUCacheWrapperUnbounded[_R]], + _LRUCacheWrapper[_R], +]: if maxsize is None or isinstance(maxsize, int): return _make_wrapper(maxsize, typed, ttl) else: From e9b31f617c4151553bc1a4dfcea868dd04662f46 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Fri, 7 Nov 2025 06:38:30 -0400 Subject: [PATCH 3/5] Update __init__.py --- async_lru/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/async_lru/__init__.py b/async_lru/__init__.py index a3029e2..c741ef6 100644 --- a/async_lru/__init__.py +++ b/async_lru/__init__.py @@ -9,6 +9,7 @@ Coroutine, Generic, Hashable, + Literal, Optional, OrderedDict, Set, From 953969708b93fa01dbbd7df5f8a55df31dff306b Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Fri, 7 Nov 2025 06:43:07 -0400 Subject: [PATCH 4/5] Update __init__.py --- async_lru/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async_lru/__init__.py b/async_lru/__init__.py index c741ef6..9d51f73 100644 --- a/async_lru/__init__.py +++ b/async_lru/__init__.py @@ -233,7 +233,7 @@ def __get__( return _LRUCacheWrapperInstanceMethod(self, instance) @final -class _LRUCacheWrapperUnbounded(_LRUCacheWrapperBase[_R]): +class _LRUCacheWrapperUnbounded(_LRUCacheWrapper[_R]): def _cache_hit(self, key: Hashable) -> None: self.__hits += 1 From c8d678ea3f7e8785288f809f63aca8e89c2e7993 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Fri, 7 Nov 2025 06:46:43 -0400 Subject: [PATCH 5/5] Update __init__.py --- async_lru/__init__.py | 75 ++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/async_lru/__init__.py b/async_lru/__init__.py index 9d51f73..1ebbdcf 100644 --- a/async_lru/__init__.py +++ b/async_lru/__init__.py @@ -64,7 +64,7 @@ def cancel(self) -> None: self.later_call = None -class _LRUCacheWrapper(Generic[_R]): +class _LRUCacheWrapperBase(Generic[_R]): def __init__( self, fn: _CB[_R], @@ -161,8 +161,7 @@ def cache_parameters(self) -> _CacheParameters: ) def _cache_hit(self, key: Hashable) -> None: - self.__hits += 1 - self.__cache.move_to_end(key) + raise NotImplementedError("must be implemented by subclass") def _cache_miss(self, key: Hashable) -> None: self.__misses += 1 @@ -192,6 +191,23 @@ def _task_done_callback( fut.set_result(task.result()) + async def __call__(self, /, *fn_args: Any, **fn_kwargs: Any) -> _R: + raise NotImplementedError("must be implemented by subclass") + + def __get__( + self, instance: _T, owner: Optional[Type[_T]] + ) -> Union[Self, "_LRUCacheWrapperInstanceMethod[_R, _T]"]: + if owner is None: + return self + else: + return _LRUCacheWrapperInstanceMethod(self, instance) + +@final +class _LRUCacheWrapper(_LRUCacheWrapperBase[_R]): + def _cache_hit(self, key: Hashable) -> None: + self.__hits += 1 + self.__cache.move_to_end(key) + async def __call__(self, /, *fn_args: Any, **fn_kwargs: Any) -> _R: if self.__closed: raise RuntimeError(f"alru_cache is closed for {self}") @@ -217,26 +233,45 @@ async def __call__(self, /, *fn_args: Any, **fn_kwargs: Any) -> _R: self.__cache[key] = _CacheItem(fut, None) - if self.__maxsize is not None and len(self.__cache) > self.__maxsize: + if len(self.__cache) > self.__maxsize: dropped_key, cache_item = self.__cache.popitem(last=False) cache_item.cancel() self._cache_miss(key) return await asyncio.shield(fut) - - def __get__( - self, instance: _T, owner: Optional[Type[_T]] - ) -> Union[Self, "_LRUCacheWrapperInstanceMethod[_R, _T]"]: - if owner is None: - return self - else: - return _LRUCacheWrapperInstanceMethod(self, instance) - + @final -class _LRUCacheWrapperUnbounded(_LRUCacheWrapper[_R]): +class _LRUCacheWrapperUnbounded(_LRUCacheWrapperBase[_R]): def _cache_hit(self, key: Hashable) -> None: self.__hits += 1 + async def __call__(self, /, *fn_args: Any, **fn_kwargs: Any) -> _R: + if self.__closed: + raise RuntimeError(f"alru_cache is closed for {self}") + + loop = asyncio.get_running_loop() + + key = _make_key(fn_args, fn_kwargs, self.__typed) + + cache_item = self.__cache.get(key) + + if cache_item is not None: + self._cache_hit(key) + if not cache_item.fut.done(): + return await asyncio.shield(cache_item.fut) + + return cache_item.fut.result() + + fut = loop.create_future() + coro = self.__wrapped__(*fn_args, **fn_kwargs) + task: asyncio.Task[_R] = loop.create_task(coro) + self.__tasks.add(task) + task.add_done_callback(partial(self._task_done_callback, fut, key)) + + self.__cache[key] = _CacheItem(fut, None) + self._cache_miss(key) + return await asyncio.shield(fut) + @final class _LRUCacheWrapperInstanceMethod(Generic[_R, _T]): @@ -303,15 +338,19 @@ def _make_wrapper( maxsize: int, typed: bool, ttl: Optional[float] = None, -) -> Callable[[_CBP[_R]], _LRUCacheWrapper[_R]]: ... - +) -> Callable[[_CBP[_R]], _LRUCacheWrapper[_R]]: + ... + + @overload def _make_wrapper( maxsize: Literal[None], typed: bool, ttl: Optional[float] = None, -) -> Callable[[_CBP[_R]], _LRUCacheWrapperUnbounded[_R]]: ... - +) -> Callable[[_CBP[_R]], _LRUCacheWrapperUnbounded[_R]]: + ... + + def _make_wrapper( maxsize: Optional[int], typed: bool,