Skip to content

Commit 924531f

Browse files
committed
Add coro equivalent for taskcache
1 parent fd36783 commit 924531f

File tree

2 files changed

+140
-5
lines changed

2 files changed

+140
-5
lines changed

async_utils/corofunc_cache.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Copyright 2020-present Michael Hall
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import asyncio
18+
from collections.abc import Callable, Coroutine, Hashable
19+
from functools import partial, wraps
20+
from typing import Any, ParamSpec, TypeVar
21+
22+
from ._cpython_stuff import make_key
23+
24+
__all__ = ("corocache", "lrucorocache")
25+
26+
27+
P = ParamSpec("P")
28+
T = TypeVar("T")
29+
K = TypeVar("K")
30+
V = TypeVar("V")
31+
32+
33+
type CoroFunc[**P, T] = Callable[P, Coroutine[Any, Any, T]]
34+
35+
36+
class LRU[K, V]:
37+
def __init__(self, maxsize: int, /):
38+
self.cache: dict[K, V] = {}
39+
self.maxsize = maxsize
40+
41+
def get(self, key: K, default: T, /) -> V | T:
42+
if key not in self.cache:
43+
return default
44+
self.cache[key] = self.cache.pop(key)
45+
return self.cache[key]
46+
47+
def __getitem__(self, key: K, /) -> V:
48+
self.cache[key] = self.cache.pop(key)
49+
return self.cache[key]
50+
51+
def __setitem__(self, key: K, value: V, /):
52+
self.cache[key] = value
53+
if len(self.cache) > self.maxsize:
54+
self.cache.pop(next(iter(self.cache)))
55+
56+
def remove(self, key: K) -> None:
57+
self.cache.pop(key, None)
58+
59+
60+
def corocache(
61+
ttl: float | None = None,
62+
) -> Callable[[CoroFunc[P, T]], CoroFunc[P, T]]:
63+
"""Decorator to cache coroutine functions.
64+
65+
This is less powerful than the version in task_cache.py but may work better for
66+
some cases where typing of libraries this interacts with is too restrictive.
67+
68+
Note: This uses the args and kwargs of the original coroutine function as a cache key.
69+
This includes instances (self) when wrapping methods.
70+
Consider not wrapping instance methods, but what those methods call when feasible in cases where this may matter.
71+
72+
The ordering of args and kwargs matters."""
73+
74+
def wrapper(coro: CoroFunc[P, T]) -> CoroFunc[P, T]:
75+
internal_cache: dict[Hashable, asyncio.Task[T]] = {}
76+
77+
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
78+
key = make_key(args, kwargs)
79+
try:
80+
return await internal_cache[key]
81+
except KeyError:
82+
internal_cache[key] = task = asyncio.create_task(coro(*args, **kwargs))
83+
if ttl is not None:
84+
# This results in internal_cache.pop(key, task) later
85+
# while avoiding a late binding issue with a lambda instead
86+
call_after_ttl = partial(
87+
asyncio.get_running_loop().call_later,
88+
ttl,
89+
internal_cache.pop,
90+
key,
91+
)
92+
task.add_done_callback(call_after_ttl)
93+
return await task
94+
95+
return wrapped
96+
97+
return wrapper
98+
99+
100+
def _lru_evict(ttl: float, cache: LRU[Hashable, Any], key: Hashable, _ignored_task: object) -> None:
101+
asyncio.get_running_loop().call_later(ttl, cache.remove, key)
102+
103+
104+
def lrucorocache(ttl: float | None = None, maxsize: int = 1024) -> Callable[[CoroFunc[P, T]], CoroFunc[P, T]]:
105+
"""Decorator to cache coroutine functions.
106+
107+
This is less powerful than the version in task_cache.py but may work better for
108+
some cases where typing of libraries this interacts with is too restrictive.
109+
110+
Note: This uses the args and kwargs of the original coroutine function as a cache key.
111+
This includes instances (self) when wrapping methods.
112+
Consider not wrapping instance methods, but what those methods call when feasible in cases where this may matter.
113+
114+
The ordering of args and kwargs matters.
115+
116+
tasks are evicted by LRU and ttl.
117+
"""
118+
119+
def wrapper(coro: CoroFunc[P, T]) -> CoroFunc[P, T]:
120+
internal_cache: LRU[Hashable, asyncio.Task[T]] = LRU(maxsize)
121+
122+
@wraps(coro)
123+
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
124+
key = make_key(args, kwargs)
125+
try:
126+
return await internal_cache[key]
127+
except KeyError:
128+
internal_cache[key] = task = asyncio.create_task(coro(*args, **kwargs))
129+
if ttl is not None:
130+
task.add_done_callback(partial(_lru_evict, ttl, internal_cache, key))
131+
return await task
132+
133+
return wrapped
134+
135+
return wrapper

async_utils/task_cache.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
import asyncio
1818
from collections.abc import Callable, Coroutine, Hashable
19-
from functools import partial
20-
from typing import Any, Generic, ParamSpec, TypeVar
19+
from functools import partial, wraps
20+
from typing import Any, ParamSpec, TypeVar
2121

2222
from ._cpython_stuff import make_key
2323

@@ -26,11 +26,9 @@
2626

2727
P = ParamSpec("P")
2828
T = TypeVar("T")
29-
K = TypeVar("K")
30-
V = TypeVar("V")
3129

3230

33-
class LRU(Generic[K, V]):
31+
class LRU[K, V]:
3432
def __init__(self, maxsize: int, /):
3533
self.cache: dict[K, V] = {}
3634
self.maxsize = maxsize
@@ -71,6 +69,7 @@ def taskcache(
7169
def wrapper(coro: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, asyncio.Task[T]]:
7270
internal_cache: dict[Hashable, asyncio.Task[T]] = {}
7371

72+
@wraps(coro, assigned=("__module__", "__name__", "__qualname__", "__doc__"))
7473
def wrapped(*args: P.args, **kwargs: P.kwargs) -> asyncio.Task[T]:
7574
key = make_key(args, kwargs)
7675
try:
@@ -118,6 +117,7 @@ def lrutaskcache(
118117
def wrapper(coro: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, asyncio.Task[T]]:
119118
internal_cache: LRU[Hashable, asyncio.Task[T]] = LRU(maxsize)
120119

120+
@wraps(coro, assigned=("__module__", "__name__", "__qualname__", "__doc__"))
121121
def wrapped(*args: P.args, **kwargs: P.kwargs) -> asyncio.Task[T]:
122122
key = make_key(args, kwargs)
123123
try:

0 commit comments

Comments
 (0)