From 563c47eb327697cdf1417b67823a2b555ddc18ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Fri, 12 Dec 2025 19:36:39 +0100 Subject: [PATCH 1/9] fix SupportsXYZ definition --- asyncstdlib/_typing.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/asyncstdlib/_typing.py b/asyncstdlib/_typing.py index 9f45ae4..7efb39e 100644 --- a/asyncstdlib/_typing.py +++ b/asyncstdlib/_typing.py @@ -55,12 +55,18 @@ #: Hashable Key HK = TypeVar("HK", bound=Hashable) + +# bool(...) +class SupportsBool(Protocol): + def __bool__(self) -> bool: ... + + # LT < LT LT = TypeVar("LT", bound="SupportsLT") class SupportsLT(Protocol): - def __lt__(self: LT, other: LT) -> bool: + def __lt__(self, __other: Any) -> SupportsBool: raise NotImplementedError @@ -69,7 +75,7 @@ def __lt__(self: LT, other: LT) -> bool: class SupportsAdd(Protocol): - def __add__(self: ADD, other: ADD, /) -> ADD: + def __add__(self, __other: Any, /) -> Any: raise NotImplementedError From 422466bae993aff65a4410bc16f5f8ef7128a46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Fri, 12 Dec 2025 19:37:16 +0100 Subject: [PATCH 2/9] fix async neutral callable type hints --- asyncstdlib/builtins.py | 5 ++--- asyncstdlib/builtins.pyi | 45 ++++++++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/asyncstdlib/builtins.py b/asyncstdlib/builtins.py index 48f2553..e70ed01 100644 --- a/asyncstdlib/builtins.py +++ b/asyncstdlib/builtins.py @@ -55,7 +55,7 @@ async def anext( def iter( - subject: Union[AnyIterable[T], Callable[[], Awaitable[T]]], + subject: Union[AnyIterable[T], Callable[[], Awaitable[T]], Callable[[], T]], sentinel: Union[Sentinel, T] = __ITER_DEFAULT, ) -> AsyncIterator[T]: """ @@ -84,13 +84,12 @@ def iter( raise TypeError("iter(v, w): v must be callable") else: assert not isinstance(sentinel, Sentinel) - return acallable_iterator(subject, sentinel) + return acallable_iterator(_awaitify(subject), sentinel) async def acallable_iterator( subject: Callable[[], Awaitable[T]], sentinel: T ) -> AsyncIterator[T]: - subject = _awaitify(subject) value = await subject() while value != sentinel: yield value diff --git a/asyncstdlib/builtins.pyi b/asyncstdlib/builtins.pyi index e4cf345..13d22a9 100644 --- a/asyncstdlib/builtins.pyi +++ b/asyncstdlib/builtins.pyi @@ -2,7 +2,7 @@ from typing import Any, AsyncIterator, Awaitable, Callable, overload from typing_extensions import TypeGuard import builtins -from ._typing import ADD, AnyIterable, HK, LT, R, T, T1, T2, T3, T4, T5 +from ._typing import ADD, AnyIterable, HK, LT, R, T, T1, T2, T3, T4, T5, SupportsLT @overload async def anext(iterator: AsyncIterator[T]) -> T: ... @@ -16,6 +16,10 @@ def iter( ) -> AsyncIterator[T]: ... @overload def iter(subject: Callable[[], Awaitable[T]], sentinel: T) -> AsyncIterator[T]: ... +@overload +def iter(subject: Callable[[], T | None], sentinel: None) -> AsyncIterator[T]: ... +@overload +def iter(subject: Callable[[], T], sentinel: T) -> AsyncIterator[T]: ... async def all(iterable: AnyIterable[Any]) -> bool: ... async def any(iterable: AnyIterable[Any]) -> bool: ... @overload @@ -180,20 +184,42 @@ async def max(iterable: AnyIterable[LT], *, key: None = ...) -> LT: ... @overload async def max(iterable: AnyIterable[LT], *, key: None = ..., default: T) -> LT | T: ... @overload -async def max(iterable: AnyIterable[T1], *, key: Callable[[T1], LT] = ...) -> T1: ... +async def max( + iterable: AnyIterable[T1], *, key: Callable[[T1], Awaitable[SupportsLT]] +) -> T1: ... +@overload +async def max( + iterable: AnyIterable[T1], + *, + key: Callable[[T1], Awaitable[SupportsLT]], + default: T2, +) -> T1 | T2: ... +@overload +async def max(iterable: AnyIterable[T1], *, key: Callable[[T1], SupportsLT]) -> T1: ... @overload async def max( - iterable: AnyIterable[T1], *, key: Callable[[T1], LT] = ..., default: T2 + iterable: AnyIterable[T1], *, key: Callable[[T1], SupportsLT], default: T2 ) -> T1 | T2: ... @overload async def min(iterable: AnyIterable[LT], *, key: None = ...) -> LT: ... @overload async def min(iterable: AnyIterable[LT], *, key: None = ..., default: T) -> LT | T: ... @overload -async def min(iterable: AnyIterable[T1], *, key: Callable[[T1], LT] = ...) -> T1: ... +async def min( + iterable: AnyIterable[T1], *, key: Callable[[T1], Awaitable[SupportsLT]] +) -> T1: ... @overload async def min( - iterable: AnyIterable[T1], *, key: Callable[[T1], LT] = ..., default: T2 + iterable: AnyIterable[T1], + *, + key: Callable[[T1], Awaitable[SupportsLT]], + default: T2, +) -> T1 | T2: ... +@overload +async def min(iterable: AnyIterable[T1], *, key: Callable[[T1], SupportsLT]) -> T1: ... +@overload +async def min( + iterable: AnyIterable[T1], *, key: Callable[[T1], SupportsLT], default: T2 ) -> T1 | T2: ... @overload def filter( @@ -247,5 +273,12 @@ async def sorted( ) -> builtins.list[LT]: ... @overload async def sorted( - iterable: AnyIterable[T], *, key: Callable[[T], LT], reverse: bool = ... + iterable: AnyIterable[T], + *, + key: Callable[[T], Awaitable[SupportsLT]], + reverse: bool = ..., +) -> builtins.list[T]: ... +@overload +async def sorted( + iterable: AnyIterable[T], *, key: Callable[[T], SupportsLT], reverse: bool = ... ) -> builtins.list[T]: ... From ce07e61802eb2d6fff6bb49f1e0622bf9d610918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Fri, 12 Dec 2025 19:39:51 +0100 Subject: [PATCH 3/9] add tests for builtins --- typetests/test_builtins.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 typetests/test_builtins.py diff --git a/typetests/test_builtins.py b/typetests/test_builtins.py new file mode 100644 index 0000000..f411a5f --- /dev/null +++ b/typetests/test_builtins.py @@ -0,0 +1,17 @@ +from typing import TypeVar +from asyncstdlib import builtins + +T = TypeVar("T") + + +def identity(v: T) -> T: + return v + + +async def async_identity(v: T) -> T: + return v + + +async def test_min_asyncneutral() -> None: + await builtins.min([1, 2, 3], key=identity) + await builtins.min([1, 2, 3], key=async_identity) From 88cd9ed1cbcd5c4919de084a4e42294f00b86c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Fri, 12 Dec 2025 19:46:41 +0100 Subject: [PATCH 4/9] fix async neutral callable type hints --- asyncstdlib/functools.pyi | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/asyncstdlib/functools.pyi b/asyncstdlib/functools.pyi index aff4538..4bedfdc 100644 --- a/asyncstdlib/functools.pyi +++ b/asyncstdlib/functools.pyi @@ -33,6 +33,14 @@ def cached_property( asynccontextmanager_type: type[AsyncContextManager[Any]], / ) -> Callable[[Callable[[T], Awaitable[R]]], CachedProperty[T, R]]: ... @overload +async def reduce( + function: Callable[[T1, T2], Awaitable[T1]], iterable: AnyIterable[T2], initial: T1 +) -> T1: ... +@overload +async def reduce( + function: Callable[[T, T], Awaitable[T]], iterable: AnyIterable[T] +) -> T: ... +@overload async def reduce( function: Callable[[T1, T2], T1], iterable: AnyIterable[T2], initial: T1 ) -> T1: ... From b27dc79b05ab6af1d8d015e69874c890ee025e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Fri, 12 Dec 2025 19:51:05 +0100 Subject: [PATCH 5/9] add tests for reduce --- typetests/test_functools.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/typetests/test_functools.py b/typetests/test_functools.py index 9b56bfd..b96c425 100644 --- a/typetests/test_functools.py +++ b/typetests/test_functools.py @@ -1,7 +1,7 @@ -from asyncstdlib import lru_cache +from asyncstdlib import functools -@lru_cache() +@functools.lru_cache() async def lru_function(a: int) -> int: return a @@ -16,7 +16,7 @@ class TestLRUMethod: Test that `lru_cache` works on methods """ - @lru_cache() + @functools.lru_cache() async def cached(self, a: int = 0) -> int: return a @@ -26,3 +26,12 @@ async def test_implicit_self(self) -> int: async def test_method_parameters(self) -> int: await self.cached("wrong parameter type") # type: ignore[arg-type] return await self.cached(12) + + +async def aadd(a: int, b: int) -> int: + return a + b + + +async def test_reduce(): + await functools.reduce(aadd, [1, 2, 3, 4]) + await functools.reduce(aadd, [1, 2, 3, 4], initial=1) From 1c3d74350419904441884c2178aad8ca49667edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Mon, 15 Dec 2025 19:33:18 +0100 Subject: [PATCH 6/9] add missing annotation --- typetests/test_functools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typetests/test_functools.py b/typetests/test_functools.py index b96c425..736a5a0 100644 --- a/typetests/test_functools.py +++ b/typetests/test_functools.py @@ -32,6 +32,6 @@ async def aadd(a: int, b: int) -> int: return a + b -async def test_reduce(): +async def test_reduce() -> None: await functools.reduce(aadd, [1, 2, 3, 4]) await functools.reduce(aadd, [1, 2, 3, 4], initial=1) From df08c29f9c5458e65298ec70532837d0900f77ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Mon, 15 Dec 2025 22:43:09 +0100 Subject: [PATCH 7/9] ignore false positive (https://github.com/microsoft/pyright/issues/11025) --- asyncstdlib/functools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asyncstdlib/functools.py b/asyncstdlib/functools.py index 07ff6fd..100c011 100644 --- a/asyncstdlib/functools.py +++ b/asyncstdlib/functools.py @@ -257,7 +257,7 @@ async def data(self): if iscoroutinefunction(type_or_getter): return CachedProperty(type_or_getter) elif isinstance(type_or_getter, type) and issubclass( - type_or_getter, AsyncContextManager + type_or_getter, AsyncContextManager # pyright: ignore[reportGeneralTypeIssues] ): def decorator( From 4e6f4325b68735350eb4fc9c56b516b5d3574d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Mon, 15 Dec 2025 22:53:08 +0100 Subject: [PATCH 8/9] support any bool'ish types --- asyncstdlib/builtins.py | 2 +- asyncstdlib/heapq.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/asyncstdlib/builtins.py b/asyncstdlib/builtins.py index e70ed01..1387f35 100644 --- a/asyncstdlib/builtins.py +++ b/asyncstdlib/builtins.py @@ -305,7 +305,7 @@ async def _min_max( raise ValueError(f"{name}() arg is an empty sequence") elif key is None: async for item in item_iter: - if invert ^ (item < best): + if invert ^ bool(item < best): best = item else: key = _awaitify(key) diff --git a/asyncstdlib/heapq.py b/asyncstdlib/heapq.py index e6db934..fee5959 100644 --- a/asyncstdlib/heapq.py +++ b/asyncstdlib/heapq.py @@ -92,7 +92,7 @@ async def pull_head(self) -> bool: return True def __lt__(self, other: _KeyIter[LT]) -> bool: - return self.reverse ^ (self.head_key < other.head_key) + return self.reverse ^ bool(self.head_key < other.head_key) def __eq__(self, other: _KeyIter[LT]) -> bool: # type: ignore[override] return not (self.head_key < other.head_key or other.head_key < self.head_key) @@ -161,7 +161,7 @@ def __init__(self, key: LT): self.key = key def __lt__(self, other: ReverseLT[LT]) -> bool: - return other.key < self.key + return bool(other.key < self.key) # Python's heapq provides a *min*-heap From 6c1255fc7819eba6ceaf981f85e534b65d010041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Wed, 17 Dec 2025 10:38:21 +0100 Subject: [PATCH 9/9] consistent protcol definition --- asyncstdlib/_typing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/asyncstdlib/_typing.py b/asyncstdlib/_typing.py index 7efb39e..806a2be 100644 --- a/asyncstdlib/_typing.py +++ b/asyncstdlib/_typing.py @@ -58,7 +58,8 @@ # bool(...) class SupportsBool(Protocol): - def __bool__(self) -> bool: ... + def __bool__(self) -> bool: + raise NotImplementedError # LT < LT