Skip to content

Commit a881623

Browse files
committed
100% spec compliance, works with mypy and pyright
1 parent 0ad5f31 commit a881623

File tree

5 files changed

+44
-23
lines changed

5 files changed

+44
-23
lines changed

_misc/_type_assertions.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,23 @@
2626

2727

2828
@lrutaskcache(ttl=60, maxsize=512)
29-
async def t1(a: int, b: str, *, x: bytes) -> str: ...
29+
async def t1(a: int, b: str, *, x: bytes) -> str:
30+
return ""
3031

3132

3233
assert_type(t1(1, "a", x=b""), asyncio.Task[str])
3334

3435

3536
@lrucorocache(ttl=60, maxsize=512)
36-
async def t2(a: int, b: str, *, x: bytes) -> str: ...
37+
async def t2(a: int, b: str, *, x: bytes) -> str:
38+
return ""
3739

3840

39-
_ = assert_type(t2(1, "a", x=b""), Coroutine[Any, Any, str])
41+
_t2_type: Coroutine[Any, Any, str] = assert_type(t2(1, "a", x=b""), Coroutine[Any, Any, str])
4042

4143

4244
def gen() -> Generator[int]:
4345
yield from range(10)
4446

4547

46-
_ = assert_type(sync_to_async_gen_noctx(gen), AsyncGenerator[int])
48+
_gen_assert_type: AsyncGenerator[int] = assert_type(sync_to_async_gen_noctx(gen), AsyncGenerator[int])

readme.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,6 @@ The project currently uses pyright for development, and both pyright and mypy
9393
when ensuring the public api surface is well-typed and compatible with strict
9494
use of typechecking. The configurations used are in pyproject.toml.
9595

96-
Mypy is known to be broken currently, for the cache decorators
97-
[discards type information in Callable aliases](https://github.com/python/mypy/issues/18842)
98-
9996
Use of Any in a few places is *intentional* for internals.
10097

10198
### 5. Threading and multiple event loops

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.04.9b"
12+
__version__ = "2025.04.24b"
1313

1414
import os
1515
import sys

src/async_utils/corofunc_cache.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,18 @@
3636
#: Warning: Mutations will impact callsite, return new objects as needed.
3737
type CacheTransformer = Callable[[tuple[t.Any, ...], dict[str, t.Any]], _CT_RET]
3838

39-
type Deco[**P, R] = Callable[[CoroLike[P, R]], CoroFunc[P, R]]
39+
# This is the only way to return a generic function not dependent on
40+
# type-checker specific behavior where the generic is deferred until application
41+
# of the function.
42+
TYPE_CHECKING = False
43+
if TYPE_CHECKING:
44+
from typing import Protocol
45+
class Deco(Protocol):
46+
def __call__[**P, R](self, c: CoroLike[P, R], /) -> CoroFunc[P, R]: ...
47+
else:
48+
# This branch is here for something reasonable to exist at runtime
49+
# Not importing typing at runtime
50+
type Deco[**P, R] = Callable[[CoroLike[P, R]], CoroFunc[P, R]]
4051

4152

4253
def _chain_fut[R](c_fut: cf.Future[R], a_fut: asyncio.Future[R]) -> None:
@@ -48,11 +59,11 @@ def _chain_fut[R](c_fut: cf.Future[R], a_fut: asyncio.Future[R]) -> None:
4859
c_fut.set_result(a_fut.result())
4960

5061

51-
def corocache[**P, R](
62+
def corocache(
5263
ttl: float | None = None,
5364
*,
5465
cache_transform: CacheTransformer | None = None,
55-
) -> Deco[P, R]:
66+
) -> Deco:
5667
"""Cache the results of the decorated coroutine.
5768
5869
This is less powerful than the version in task_cache.py but may work better
@@ -85,7 +96,7 @@ def corocache[**P, R](
8596
def key_func(args: tuple[t.Any, ...], kwds: dict[t.Any, t.Any], /) -> Hashable:
8697
return make_key(*cache_transform(args, kwds))
8798

88-
def wrapper(coro: CoroLike[P, R]) -> CoroFunc[P, R]:
99+
def wrapper[**P, R](coro: CoroLike[P, R], /) -> CoroFunc[P, R]:
89100
internal_cache: dict[Hashable, cf.Future[R]] = {}
90101
internal_taskset: set[asyncio.Task[R]] = set()
91102

@@ -120,12 +131,12 @@ async def wrapped(*args: P.args, **kwargs: P.kwargs) -> R:
120131
return wrapper
121132

122133

123-
def lrucorocache[**P, R](
134+
def lrucorocache(
124135
ttl: float | None = None,
125136
maxsize: int = 1024,
126137
*,
127138
cache_transform: CacheTransformer | None = None,
128-
) -> Deco[P, R]:
139+
) -> Deco:
129140
"""Cache the results of the decorated coroutine.
130141
131142
This is less powerful than the version in task_cache.py but may work better
@@ -164,7 +175,7 @@ def lrucorocache[**P, R](
164175
def key_func(args: tuple[t.Any, ...], kwds: dict[t.Any, t.Any], /) -> Hashable:
165176
return make_key(*cache_transform(args, kwds))
166177

167-
def wrapper(coro: CoroLike[P, R]) -> CoroFunc[P, R]:
178+
def wrapper[**P, R](coro: CoroLike[P, R], /) -> CoroFunc[P, R]:
168179
internal_cache: LRU[Hashable, cf.Future[R]] = LRU(maxsize)
169180
internal_taskset: set[asyncio.Task[R]] = set()
170181

src/async_utils/task_cache.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,18 @@
3838
#: Warning: Mutations will impact callsite, return new objects as needed.
3939
type CacheTransformer = Callable[[tuple[t.Any, ...], dict[str, t.Any]], _CT_RET]
4040

41-
type Deco[**P, R] = Callable[[TaskCoroFunc[P, R]], TaskFunc[P, R]]
41+
# This is the only way to return a generic function not dependent on
42+
# type-checker specific behavior where the generic is deferred until application
43+
# of the function.
44+
TYPE_CHECKING = False
45+
if TYPE_CHECKING:
46+
from typing import Protocol
47+
class Deco(Protocol):
48+
def __call__[**P, R](self, c: TaskCoroFunc[P, R], /) -> TaskFunc[P, R]: ...
49+
else:
50+
# This branch is here for something reasonable to exist at runtime
51+
# Not importing typing at runtime
52+
type Deco[**P, R] = Callable[[TaskCoroFunc[P, R]], TaskFunc[P, R]]
4253

4354
# Non-annotation assignments for transformed functions
4455
_WRAP_ASSIGN = ("__module__", "__name__", "__qualname__", "__doc__")
@@ -58,7 +69,7 @@ class _WrappedSignature[**P, R]:
5869
# as func.__signature__
5970
# Known working: py 3.12.0 - py3.14a6 range inclusive
6071
def __init__(self, f: TaskCoroFunc[P, R], w: TaskFunc[P, R]) -> None:
61-
self._f = f
72+
self._f: Callable[..., t.Any] = f # anotation needed for inspect use below....
6273
self._w = w
6374
self._sig: t.Any | None = None
6475

@@ -102,11 +113,11 @@ async def _await[R](fut: asyncio.Future[R]) -> R:
102113
return await fut
103114

104115

105-
def taskcache[**P, R](
116+
def taskcache(
106117
ttl: float | None = None,
107118
*,
108119
cache_transform: CacheTransformer | None = None,
109-
) -> Deco[P, R]:
120+
) -> Deco:
110121
"""Cache the results of the decorated coroutine.
111122
112123
Decorator to modify coroutine functions to instead act as functions
@@ -145,7 +156,7 @@ def taskcache[**P, R](
145156
def key_func(args: tuple[t.Any, ...], kwds: dict[t.Any, t.Any], /) -> Hashable:
146157
return make_key(*cache_transform(args, kwds))
147158

148-
def wrapper(coro: TaskCoroFunc[P, R]) -> TaskFunc[P, R]:
159+
def wrapper[**P, R](coro: TaskCoroFunc[P, R], /) -> TaskFunc[P, R]:
149160
internal_cache: dict[Hashable, cf.Future[R]] = {}
150161

151162
def _internal_cache_evict(key: Hashable, _ignored_task: object) -> None:
@@ -180,12 +191,12 @@ def wrapped(*args: P.args, **kwargs: P.kwargs) -> asyncio.Task[R]:
180191
return wrapper
181192

182193

183-
def lrutaskcache[**P, R](
194+
def lrutaskcache(
184195
ttl: float | None = None,
185196
maxsize: int = 1024,
186197
*,
187198
cache_transform: CacheTransformer | None = None,
188-
) -> Deco[P, R]:
199+
) -> Deco:
189200
"""Cache the results of the decorated coroutine.
190201
191202
Decorator to modify coroutine functions to instead act as functions
@@ -231,7 +242,7 @@ def lrutaskcache[**P, R](
231242
def key_func(args: tuple[t.Any, ...], kwds: dict[t.Any, t.Any], /) -> Hashable:
232243
return make_key(*cache_transform(args, kwds))
233244

234-
def wrapper(coro: TaskCoroFunc[P, R]) -> TaskFunc[P, R]:
245+
def wrapper[**P, R](coro: TaskCoroFunc[P, R], /) -> TaskFunc[P, R]:
235246
internal_cache: LRU[Hashable, cf.Future[R]] = LRU(maxsize)
236247

237248
def _internal_cache_evict(key: Hashable, _ignored_task: object) -> None:

0 commit comments

Comments
 (0)