Skip to content

Commit 354b93a

Browse files
committed
Add cache transform
1 parent ecce07a commit 354b93a

File tree

3 files changed

+81
-21
lines changed

3 files changed

+81
-21
lines changed

src/async_utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
__author__ = "Michael Hall"
1010
__license__ = "Apache-2.0"
1111
__copyright__ = "Copyright 2020-Present Michael Hall"
12-
__version__ = "2025.01.06"
12+
__version__ = "2025.01.12"
1313

1414
import os
1515
import sys

src/async_utils/corofunc_cache.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,48 @@
2828
type CoroFunc[**P, R] = Callable[P, Coroutine[t.Any, t.Any, R]]
2929
type CoroLike[**P, R] = Callable[P, Awaitable[R]]
3030

31+
type _CT_RET = tuple[tuple[t.Any, ...], dict[t.str, t.Any]]
32+
type CacheTransformer = Callable[[tuple[t.Any, ...], dict[t.str, t.Any]], _CT_RET]
33+
3134
type Deco[**P, R] = Callable[[CoroLike[P, R]], CoroFunc[P, R]]
3235

3336

34-
def corocache[**P, R](ttl: float | None = None) -> Deco[P, R]:
37+
def corocache[**P, R](
38+
ttl: float | None = None,
39+
*,
40+
cache_transform: CacheTransformer | None = None,
41+
) -> Deco[P, R]:
3542
"""Cache the results of the decorated coroutine.
3643
3744
This is less powerful than the version in task_cache.py but may work better
3845
for some cases where typing of libraries this interacts with is too
3946
restrictive.
4047
41-
Note: This uses the args and kwargs of the original coroutine function as a
42-
cache key. This includes instances (self) when wrapping methods.
48+
Note: This by default uses the args and kwargs of the original coroutine
49+
function as a cache key. This includes instances (self) when wrapping methods.
4350
Consider not wrapping instance methods, but what those methods call when
44-
feasible in cases where this may matter.
51+
feasible in cases where this may matter, or using a cache transform.
4552
4653
The ordering of args and kwargs matters.
4754
4855
Parameters
4956
----------
5057
ttl: float | None
5158
The time to live in seconds for cached results. Defaults to None (forever)
59+
cache_transform: CacheTransformer | None
60+
An optional callable that transforms args and kwargs used
61+
as a cache key.
5262
5363
Returns
5464
-------
5565
A decorator which wraps coroutine-like functions with preemptive caching.
5666
"""
67+
if cache_transform is None:
68+
key_func = make_key
69+
else:
70+
71+
def key_func(args: tuple[t.Any, ...], kwds: dict[t.Any, t.Any]) -> Hashable:
72+
return make_key(*cache_transform(args, kwds))
5773

5874
def wrapper(coro: CoroLike[P, R]) -> CoroFunc[P, R]:
5975
internal_cache: dict[Hashable, asyncio.Future[R]] = {}
@@ -64,7 +80,7 @@ def _internal_cache_evict(key: Hashable, _ignored_task: object) -> None:
6480
loop.call_later(ttl, internal_cache.pop, key)
6581

6682
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
67-
key = make_key(args, kwargs)
83+
key = key_func(args, kwargs)
6884
if (cached := internal_cache.get(key)) is not None:
6985
return await cached
7086

@@ -79,17 +95,22 @@ async def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
7995
return wrapper
8096

8197

82-
def lrucorocache[**P, R](ttl: float | None = None, maxsize: int = 1024) -> Deco[P, R]:
98+
def lrucorocache[**P, R](
99+
ttl: float | None = None,
100+
maxsize: int = 1024,
101+
*,
102+
cache_transform: CacheTransformer | None = None,
103+
) -> Deco[P, R]:
83104
"""Cache the results of the decorated coroutine.
84105
85106
This is less powerful than the version in task_cache.py but may work better
86107
for some cases where typing of libraries this interacts with is too
87108
restrictive.
88109
89-
Note: This uses the args and kwargs of the original coroutine function as a
90-
cache key. This includes instances (self) when wrapping methods.
110+
Note: This by default uses the args and kwargs of the original coroutine
111+
function as a cache key. This includes instances (self) when wrapping methods.
91112
Consider not wrapping instance methods, but what those methods call when
92-
feasible in cases where this may matter.
113+
feasible in cases where this may matter, or using a cache transform.
93114
94115
The ordering of args and kwargs matters.
95116
@@ -103,11 +124,20 @@ def lrucorocache[**P, R](ttl: float | None = None, maxsize: int = 1024) -> Deco[
103124
The maximum number of items to retain no matter if they have reached
104125
expiration by ttl or not.
105126
Items evicted by this policy are evicted by least recent use.
127+
cache_transform: CacheTransformer | None
128+
An optional callable that transforms args and kwargs used
129+
as a cache key.
106130
107131
Returns
108132
-------
109133
A decorator which wraps coroutine-like functions with preemptive caching.
110134
"""
135+
if cache_transform is None:
136+
key_func = make_key
137+
else:
138+
139+
def key_func(args: tuple[t.Any, ...], kwds: dict[t.Any, t.Any]) -> Hashable:
140+
return make_key(*cache_transform(args, kwds))
111141

112142
def wrapper(coro: CoroLike[P, R]) -> CoroFunc[P, R]:
113143
internal_cache: LRU[Hashable, asyncio.Future[R]] = LRU(maxsize)
@@ -119,7 +149,7 @@ def _internal_cache_evict(key: Hashable, _ignored_task: object) -> None:
119149

120150
@wraps(coro)
121151
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
122-
key = make_key(args, kwargs)
152+
key = key_func(args, kwargs)
123153
if (cached := internal_cache.get(key, None)) is not None:
124154
return await cached
125155

src/async_utils/task_cache.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,20 @@
3131
type TaskFunc[**P, R] = CoroFunc[P, R] | Callable[P, asyncio.Task[R]]
3232
type TaskCoroFunc[**P, R] = CoroFunc[P, R] | TaskFunc[P, R]
3333

34+
type _CT_RET = tuple[tuple[t.Any, ...], dict[t.str, t.Any]]
35+
type CacheTransformer = Callable[[tuple[t.Any, ...], dict[t.str, t.Any]], _CT_RET]
36+
3437
type Deco[**P, R] = Callable[[TaskCoroFunc[P, R]], TaskFunc[P, R]]
3538

3639
# Non-annotation assignments for transformed functions
3740
_WRAP_ASSIGN = ("__module__", "__name__", "__qualname__", "__doc__")
3841

3942

40-
def taskcache[**P, R](ttl: float | None = None) -> Deco[P, R]:
43+
def taskcache[**P, R](
44+
ttl: float | None = None,
45+
*,
46+
cache_transform: CacheTransformer | None = None,
47+
) -> Deco[P, R]:
4148
"""Cache the results of the decorated coroutine.
4249
4350
Decorator to modify coroutine functions to instead act as functions
@@ -46,23 +53,32 @@ def taskcache[**P, R](ttl: float | None = None) -> Deco[P, R]:
4653
For general use, this leaves the end user API largely the same,
4754
while leveraging tasks to allow preemptive caching.
4855
49-
Note: This uses the args and kwargs of the original coroutine function as a
50-
cache key. This includes instances (self) when wrapping methods.
56+
Note: This by default uses the args and kwargs of the original coroutine
57+
function as a cache key. This includes instances (self) when wrapping methods.
5158
Consider not wrapping instance methods, but what those methods call when
52-
feasible in cases where this may matter.
59+
feasible in cases where this may matter, or using a cache transform.
5360
5461
The ordering of args and kwargs matters.
5562
5663
Parameters
5764
----------
5865
ttl: float | None
5966
The time to live in seconds for cached results. Defaults to None (forever)
67+
cache_transform: CacheTransformer | None
68+
An optional callable that transforms args and kwargs used
69+
as a cache key.
6070
6171
Returns
6272
-------
6373
A decorator which wraps coroutine-like objects in functions that return
6474
preemptively cached tasks.
6575
"""
76+
if cache_transform is None:
77+
key_func = make_key
78+
else:
79+
80+
def key_func(args: tuple[t.Any, ...], kwds: dict[t.Any, t.Any]) -> Hashable:
81+
return make_key(*cache_transform(args, kwds))
6682

6783
def wrapper(coro: TaskCoroFunc[P, R]) -> TaskFunc[P, R]:
6884
internal_cache: dict[Hashable, asyncio.Task[R]] = {}
@@ -74,7 +90,7 @@ def _internal_cache_evict(key: Hashable, _ignored_task: object) -> None:
7490

7591
@wraps(coro, assigned=_WRAP_ASSIGN)
7692
def wrapped(*args: P.args, **kwargs: P.kwargs) -> asyncio.Task[R]:
77-
key = make_key(args, kwargs)
93+
key = key_func(args, kwargs)
7894
if (cached := internal_cache.get(key)) is not None:
7995
return cached
8096

@@ -100,7 +116,12 @@ def wrapped(*args: P.args, **kwargs: P.kwargs) -> asyncio.Task[R]:
100116
return wrapper
101117

102118

103-
def lrutaskcache[**P, R](ttl: float | None = None, maxsize: int = 1024) -> Deco[P, R]:
119+
def lrutaskcache[**P, R](
120+
ttl: float | None = None,
121+
maxsize: int = 1024,
122+
*,
123+
cache_transform: CacheTransformer | None = None,
124+
) -> Deco[P, R]:
104125
"""Cache the results of the decorated coroutine.
105126
106127
Decorator to modify coroutine functions to instead act as functions
@@ -109,10 +130,10 @@ def lrutaskcache[**P, R](ttl: float | None = None, maxsize: int = 1024) -> Deco[
109130
For general use, this leaves the end user API largely the same,
110131
while leveraging tasks to allow preemptive caching.
111132
112-
Note: This uses the args and kwargs of the original coroutine function as a
113-
cache key. This includes instances (self) when wrapping methods.
133+
Note: This by default uses the args and kwargs of the original coroutine
134+
function as a cache key. This includes instances (self) when wrapping methods.
114135
Consider not wrapping instance methods, but what those methods call when
115-
feasible in cases where this may matter.
136+
feasible in cases where this may matter, or using a cache transform.
116137
117138
The ordering of args and kwargs matters.
118139
@@ -127,12 +148,21 @@ def lrutaskcache[**P, R](ttl: float | None = None, maxsize: int = 1024) -> Deco[
127148
The maximum number of items to retain no matter if they have reached
128149
expiration by ttl or not.
129150
Items evicted by this policy are evicted by least recent use.
151+
cache_transform: CacheTransformer | None
152+
An optional callable that transforms args and kwargs used
153+
as a cache key.
130154
131155
Returns
132156
-------
133157
A decorator which wraps coroutine-like objects in functions that return
134158
preemptively cached tasks.
135159
"""
160+
if cache_transform is None:
161+
key_func = make_key
162+
else:
163+
164+
def key_func(args: tuple[t.Any, ...], kwds: dict[t.Any, t.Any]) -> Hashable:
165+
return make_key(*cache_transform(args, kwds))
136166

137167
def wrapper(coro: TaskCoroFunc[P, R]) -> TaskFunc[P, R]:
138168
internal_cache: LRU[Hashable, asyncio.Task[R]] = LRU(maxsize)
@@ -144,7 +174,7 @@ def _internal_cache_evict(key: Hashable, _ignored_task: object) -> None:
144174

145175
@wraps(coro, assigned=_WRAP_ASSIGN)
146176
def wrapped(*args: P.args, **kwargs: P.kwargs) -> asyncio.Task[R]:
147-
key = make_key(args, kwargs)
177+
key = key_func(args, kwargs)
148178
if (cached := internal_cache.get(key, None)) is not None:
149179
return cached
150180

0 commit comments

Comments
 (0)