asyncstdlib 3.13.0__tar.gz → 3.13.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/PKG-INFO +4 -2
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/__init__.py +1 -1
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/_lrucache.pyi +7 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/_typing.py +9 -2
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/asynctools.py +2 -3
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/builtins.py +5 -7
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/builtins.pyi +40 -7
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/contextlib.py +1 -1
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/contextlib.pyi +2 -2
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/functools.py +5 -6
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/functools.pyi +8 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/heapq.py +2 -2
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/itertools.py +93 -72
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/itertools.pyi +6 -2
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/pyproject.toml +4 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/unittests/test_asynctools.py +0 -1
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/unittests/test_functools_lru.py +5 -2
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/unittests/test_heapq.py +0 -1
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/unittests/test_itertools.py +95 -1
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/unittests/utility.py +0 -1
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/LICENSE +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/README.rst +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/_core.py +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/_lrucache.py +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/_utility.py +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/heapq.pyi +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/asyncstdlib/py.typed +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/unittests/__init__.py +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/unittests/test_builtins.py +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/unittests/test_contextlib.py +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/unittests/test_functools.py +0 -0
- {asyncstdlib-3.13.0 → asyncstdlib-3.13.2}/unittests/test_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: asyncstdlib
|
|
3
|
-
Version: 3.13.
|
|
3
|
+
Version: 3.13.2
|
|
4
4
|
Summary: The missing async toolbox
|
|
5
5
|
Keywords: async,enumerate,itertools,builtins,functools,contextlib
|
|
6
6
|
Author-email: Max Kühn <maxfischer2781@gmail.com>
|
|
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
License-File: LICENSE
|
|
20
22
|
Requires-Dist: sphinx ; extra == "doc"
|
|
21
23
|
Requires-Dist: sphinxcontrib-trio ; extra == "doc"
|
|
22
24
|
Requires-Dist: pytest ; extra == "test"
|
|
@@ -9,6 +9,7 @@ from typing import (
|
|
|
9
9
|
overload,
|
|
10
10
|
Protocol,
|
|
11
11
|
)
|
|
12
|
+
from types import CoroutineType
|
|
12
13
|
from typing_extensions import ParamSpec, Concatenate
|
|
13
14
|
|
|
14
15
|
from ._typing import AC, TypedDict
|
|
@@ -42,6 +43,12 @@ class LRUAsyncCallable(Protocol[AC]):
|
|
|
42
43
|
owner: type | None = ...,
|
|
43
44
|
) -> LRUAsyncBoundCallable[S, P, R]: ...
|
|
44
45
|
@overload
|
|
46
|
+
def __get__(
|
|
47
|
+
self: LRUAsyncCallable[Callable[Concatenate[S, P], CoroutineType[Any, Any, R]]],
|
|
48
|
+
instance: S,
|
|
49
|
+
owner: type | None = ...,
|
|
50
|
+
) -> LRUAsyncBoundCallable[S, P, R]: ...
|
|
51
|
+
@overload
|
|
45
52
|
def __get__(
|
|
46
53
|
self: LRUAsyncCallable[Callable[Concatenate[S, P], Awaitable[R]]],
|
|
47
54
|
instance: S,
|
|
@@ -55,12 +55,19 @@ AC = TypeVar("AC", bound=Callable[..., Awaitable[Any]])
|
|
|
55
55
|
#: Hashable Key
|
|
56
56
|
HK = TypeVar("HK", bound=Hashable)
|
|
57
57
|
|
|
58
|
+
|
|
59
|
+
# bool(...)
|
|
60
|
+
class SupportsBool(Protocol):
|
|
61
|
+
def __bool__(self) -> bool:
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
|
|
58
65
|
# LT < LT
|
|
59
66
|
LT = TypeVar("LT", bound="SupportsLT")
|
|
60
67
|
|
|
61
68
|
|
|
62
69
|
class SupportsLT(Protocol):
|
|
63
|
-
def __lt__(self
|
|
70
|
+
def __lt__(self, __other: Any) -> SupportsBool:
|
|
64
71
|
raise NotImplementedError
|
|
65
72
|
|
|
66
73
|
|
|
@@ -69,7 +76,7 @@ ADD = TypeVar("ADD", bound="SupportsAdd")
|
|
|
69
76
|
|
|
70
77
|
|
|
71
78
|
class SupportsAdd(Protocol):
|
|
72
|
-
def __add__(self
|
|
79
|
+
def __add__(self, __other: Any, /) -> Any:
|
|
73
80
|
raise NotImplementedError
|
|
74
81
|
|
|
75
82
|
|
|
@@ -20,7 +20,6 @@ from ._typing import T, T1, T2, T3, T4, T5, AnyIterable
|
|
|
20
20
|
from ._core import aiter
|
|
21
21
|
from .contextlib import nullcontext
|
|
22
22
|
|
|
23
|
-
|
|
24
23
|
S = TypeVar("S")
|
|
25
24
|
|
|
26
25
|
|
|
@@ -35,7 +34,7 @@ class _BorrowedAsyncIterator(AsyncGenerator[T, S]):
|
|
|
35
34
|
__slots__ = "__wrapped__", "__anext__", "asend", "athrow", "_wrapper"
|
|
36
35
|
|
|
37
36
|
# Type checker does not understand `__slot__` definitions
|
|
38
|
-
__anext__: Callable[
|
|
37
|
+
__anext__: Callable[..., Coroutine[Any, Any, T]]
|
|
39
38
|
asend: Any
|
|
40
39
|
athrow: Any
|
|
41
40
|
|
|
@@ -49,7 +48,7 @@ class _BorrowedAsyncIterator(AsyncGenerator[T, S]):
|
|
|
49
48
|
# An async *iterator* (e.g. `async def: yield`) must return
|
|
50
49
|
# itself from __aiter__. If we do not shadow this then
|
|
51
50
|
# running aiter(self).aclose closes the underlying iterator.
|
|
52
|
-
self.__anext__ = self._wrapper.__anext__
|
|
51
|
+
self.__anext__ = self._wrapper.__anext__
|
|
53
52
|
if hasattr(iterator, "asend"):
|
|
54
53
|
self.asend = (
|
|
55
54
|
iterator.asend # pyright: ignore[reportUnknownMemberType,reportAttributeAccessIssue]
|
|
@@ -22,7 +22,6 @@ from ._core import (
|
|
|
22
22
|
Sentinel,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
|
|
26
25
|
__ANEXT_DEFAULT = Sentinel("<no default>")
|
|
27
26
|
|
|
28
27
|
|
|
@@ -55,7 +54,7 @@ __ITER_DEFAULT = Sentinel("<no default>")
|
|
|
55
54
|
|
|
56
55
|
|
|
57
56
|
def iter(
|
|
58
|
-
subject: Union[AnyIterable[T], Callable[[], Awaitable[T]]],
|
|
57
|
+
subject: Union[AnyIterable[T], Callable[[], Awaitable[T]], Callable[[], T]],
|
|
59
58
|
sentinel: Union[Sentinel, T] = __ITER_DEFAULT,
|
|
60
59
|
) -> AsyncIterator[T]:
|
|
61
60
|
"""
|
|
@@ -84,13 +83,12 @@ def iter(
|
|
|
84
83
|
raise TypeError("iter(v, w): v must be callable")
|
|
85
84
|
else:
|
|
86
85
|
assert not isinstance(sentinel, Sentinel)
|
|
87
|
-
return acallable_iterator(subject, sentinel)
|
|
86
|
+
return acallable_iterator(_awaitify(subject), sentinel)
|
|
88
87
|
|
|
89
88
|
|
|
90
89
|
async def acallable_iterator(
|
|
91
90
|
subject: Callable[[], Awaitable[T]], sentinel: T
|
|
92
91
|
) -> AsyncIterator[T]:
|
|
93
|
-
subject = _awaitify(subject)
|
|
94
92
|
value = await subject()
|
|
95
93
|
while value != sentinel:
|
|
96
94
|
yield value
|
|
@@ -164,7 +162,7 @@ async def zip(
|
|
|
164
162
|
|
|
165
163
|
|
|
166
164
|
async def _zip_inner(
|
|
167
|
-
aiters: Tuple[AsyncIterator[T], ...]
|
|
165
|
+
aiters: Tuple[AsyncIterator[T], ...],
|
|
168
166
|
) -> AsyncIterator[Tuple[T, ...]]:
|
|
169
167
|
"""Direct zip transposing tuple-of-iterators to iterator-of-tuples"""
|
|
170
168
|
try:
|
|
@@ -175,7 +173,7 @@ async def _zip_inner(
|
|
|
175
173
|
|
|
176
174
|
|
|
177
175
|
async def _zip_inner_strict(
|
|
178
|
-
aiters: Tuple[AsyncIterator[T], ...]
|
|
176
|
+
aiters: Tuple[AsyncIterator[T], ...],
|
|
179
177
|
) -> AsyncIterator[Tuple[T, ...]]:
|
|
180
178
|
"""Length aware zip checking that all iterators are equal length"""
|
|
181
179
|
# track index of the last iterator we tried to anext
|
|
@@ -306,7 +304,7 @@ async def _min_max(
|
|
|
306
304
|
raise ValueError(f"{name}() arg is an empty sequence")
|
|
307
305
|
elif key is None:
|
|
308
306
|
async for item in item_iter:
|
|
309
|
-
if invert ^ (item < best):
|
|
307
|
+
if invert ^ bool(item < best):
|
|
310
308
|
best = item
|
|
311
309
|
else:
|
|
312
310
|
key = _awaitify(key)
|
|
@@ -2,7 +2,7 @@ from typing import Any, AsyncIterator, Awaitable, Callable, overload
|
|
|
2
2
|
from typing_extensions import TypeGuard
|
|
3
3
|
import builtins
|
|
4
4
|
|
|
5
|
-
from ._typing import ADD, AnyIterable, HK, LT, R, T, T1, T2, T3, T4, T5
|
|
5
|
+
from ._typing import ADD, AnyIterable, HK, LT, R, T, T1, T2, T3, T4, T5, SupportsLT
|
|
6
6
|
|
|
7
7
|
@overload
|
|
8
8
|
async def anext(iterator: AsyncIterator[T]) -> T: ...
|
|
@@ -16,6 +16,10 @@ def iter(
|
|
|
16
16
|
) -> AsyncIterator[T]: ...
|
|
17
17
|
@overload
|
|
18
18
|
def iter(subject: Callable[[], Awaitable[T]], sentinel: T) -> AsyncIterator[T]: ...
|
|
19
|
+
@overload
|
|
20
|
+
def iter(subject: Callable[[], T | None], sentinel: None) -> AsyncIterator[T]: ...
|
|
21
|
+
@overload
|
|
22
|
+
def iter(subject: Callable[[], T], sentinel: T) -> AsyncIterator[T]: ...
|
|
19
23
|
async def all(iterable: AnyIterable[Any]) -> bool: ...
|
|
20
24
|
async def any(iterable: AnyIterable[Any]) -> bool: ...
|
|
21
25
|
@overload
|
|
@@ -180,20 +184,42 @@ async def max(iterable: AnyIterable[LT], *, key: None = ...) -> LT: ...
|
|
|
180
184
|
@overload
|
|
181
185
|
async def max(iterable: AnyIterable[LT], *, key: None = ..., default: T) -> LT | T: ...
|
|
182
186
|
@overload
|
|
183
|
-
async def max(
|
|
187
|
+
async def max(
|
|
188
|
+
iterable: AnyIterable[T1], *, key: Callable[[T1], Awaitable[SupportsLT]]
|
|
189
|
+
) -> T1: ...
|
|
190
|
+
@overload
|
|
191
|
+
async def max(
|
|
192
|
+
iterable: AnyIterable[T1],
|
|
193
|
+
*,
|
|
194
|
+
key: Callable[[T1], Awaitable[SupportsLT]],
|
|
195
|
+
default: T2,
|
|
196
|
+
) -> T1 | T2: ...
|
|
197
|
+
@overload
|
|
198
|
+
async def max(iterable: AnyIterable[T1], *, key: Callable[[T1], SupportsLT]) -> T1: ...
|
|
184
199
|
@overload
|
|
185
200
|
async def max(
|
|
186
|
-
iterable: AnyIterable[T1], *, key: Callable[[T1],
|
|
201
|
+
iterable: AnyIterable[T1], *, key: Callable[[T1], SupportsLT], default: T2
|
|
187
202
|
) -> T1 | T2: ...
|
|
188
203
|
@overload
|
|
189
204
|
async def min(iterable: AnyIterable[LT], *, key: None = ...) -> LT: ...
|
|
190
205
|
@overload
|
|
191
206
|
async def min(iterable: AnyIterable[LT], *, key: None = ..., default: T) -> LT | T: ...
|
|
192
207
|
@overload
|
|
193
|
-
async def min(
|
|
208
|
+
async def min(
|
|
209
|
+
iterable: AnyIterable[T1], *, key: Callable[[T1], Awaitable[SupportsLT]]
|
|
210
|
+
) -> T1: ...
|
|
194
211
|
@overload
|
|
195
212
|
async def min(
|
|
196
|
-
iterable: AnyIterable[T1],
|
|
213
|
+
iterable: AnyIterable[T1],
|
|
214
|
+
*,
|
|
215
|
+
key: Callable[[T1], Awaitable[SupportsLT]],
|
|
216
|
+
default: T2,
|
|
217
|
+
) -> T1 | T2: ...
|
|
218
|
+
@overload
|
|
219
|
+
async def min(iterable: AnyIterable[T1], *, key: Callable[[T1], SupportsLT]) -> T1: ...
|
|
220
|
+
@overload
|
|
221
|
+
async def min(
|
|
222
|
+
iterable: AnyIterable[T1], *, key: Callable[[T1], SupportsLT], default: T2
|
|
197
223
|
) -> T1 | T2: ...
|
|
198
224
|
@overload
|
|
199
225
|
def filter(
|
|
@@ -231,7 +257,7 @@ async def tuple(iterable: AnyIterable[T]) -> builtins.tuple[T, ...]: ...
|
|
|
231
257
|
async def dict() -> builtins.dict[Any, Any]: ...
|
|
232
258
|
@overload
|
|
233
259
|
async def dict(
|
|
234
|
-
iterable: AnyIterable[builtins.tuple[HK, T]]
|
|
260
|
+
iterable: AnyIterable[builtins.tuple[HK, T]],
|
|
235
261
|
) -> builtins.dict[HK, T]: ...
|
|
236
262
|
@overload
|
|
237
263
|
async def dict(
|
|
@@ -247,5 +273,12 @@ async def sorted(
|
|
|
247
273
|
) -> builtins.list[LT]: ...
|
|
248
274
|
@overload
|
|
249
275
|
async def sorted(
|
|
250
|
-
iterable: AnyIterable[T],
|
|
276
|
+
iterable: AnyIterable[T],
|
|
277
|
+
*,
|
|
278
|
+
key: Callable[[T], Awaitable[SupportsLT]],
|
|
279
|
+
reverse: bool = ...,
|
|
280
|
+
) -> builtins.list[T]: ...
|
|
281
|
+
@overload
|
|
282
|
+
async def sorted(
|
|
283
|
+
iterable: AnyIterable[T], *, key: Callable[[T], SupportsLT], reverse: bool = ...
|
|
251
284
|
) -> builtins.list[T]: ...
|
|
@@ -28,7 +28,7 @@ AbstractContextManager = AsyncContextManager
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def contextmanager(
|
|
31
|
-
func: Callable[..., AsyncGenerator[T, None]]
|
|
31
|
+
func: Callable[..., AsyncGenerator[T, None]],
|
|
32
32
|
) -> Callable[..., AsyncContextManager[T]]:
|
|
33
33
|
r"""
|
|
34
34
|
Create an asynchronous context manager out of an asynchronous generator function
|
|
@@ -55,12 +55,12 @@ class ContextDecorator(AsyncContextManager[T], metaclass=ABCMeta):
|
|
|
55
55
|
P = ParamSpec("P")
|
|
56
56
|
|
|
57
57
|
def contextmanager(
|
|
58
|
-
func: Callable[P, AsyncGenerator[T, None]]
|
|
58
|
+
func: Callable[P, AsyncGenerator[T, None]],
|
|
59
59
|
) -> Callable[P, ContextDecorator[T]]: ...
|
|
60
60
|
|
|
61
61
|
class closing(Generic[AClose]):
|
|
62
62
|
def __init__(self, thing: AClose) -> None: ...
|
|
63
|
-
async def __aenter__(self: Self) ->
|
|
63
|
+
async def __aenter__(self: Self) -> AClose: ...
|
|
64
64
|
async def __aexit__(
|
|
65
65
|
self,
|
|
66
66
|
exc_type: type[BaseException] | None,
|
|
@@ -254,8 +254,10 @@ def cached_property(
|
|
|
254
254
|
Instances on which a value is to be cached must have a
|
|
255
255
|
``__dict__`` attribute that is a mutable mapping.
|
|
256
256
|
"""
|
|
257
|
-
if
|
|
258
|
-
type_or_getter
|
|
257
|
+
if iscoroutinefunction(type_or_getter):
|
|
258
|
+
return CachedProperty(type_or_getter)
|
|
259
|
+
elif isinstance(type_or_getter, type) and issubclass(
|
|
260
|
+
type_or_getter, AsyncContextManager # pyright: ignore[reportGeneralTypeIssues]
|
|
259
261
|
):
|
|
260
262
|
|
|
261
263
|
def decorator(
|
|
@@ -269,12 +271,9 @@ def cached_property(
|
|
|
269
271
|
)
|
|
270
272
|
|
|
271
273
|
return decorator
|
|
272
|
-
|
|
273
|
-
if not iscoroutinefunction(type_or_getter):
|
|
274
|
+
else:
|
|
274
275
|
raise ValueError("cached_property can only be used with a coroutine function")
|
|
275
276
|
|
|
276
|
-
return CachedProperty(type_or_getter)
|
|
277
|
-
|
|
278
277
|
|
|
279
278
|
__REDUCE_SENTINEL = Sentinel("<no default>")
|
|
280
279
|
|
|
@@ -33,6 +33,14 @@ def cached_property(
|
|
|
33
33
|
asynccontextmanager_type: type[AsyncContextManager[Any]], /
|
|
34
34
|
) -> Callable[[Callable[[T], Awaitable[R]]], CachedProperty[T, R]]: ...
|
|
35
35
|
@overload
|
|
36
|
+
async def reduce(
|
|
37
|
+
function: Callable[[T1, T2], Awaitable[T1]], iterable: AnyIterable[T2], initial: T1
|
|
38
|
+
) -> T1: ...
|
|
39
|
+
@overload
|
|
40
|
+
async def reduce(
|
|
41
|
+
function: Callable[[T, T], Awaitable[T]], iterable: AnyIterable[T]
|
|
42
|
+
) -> T: ...
|
|
43
|
+
@overload
|
|
36
44
|
async def reduce(
|
|
37
45
|
function: Callable[[T1, T2], T1], iterable: AnyIterable[T2], initial: T1
|
|
38
46
|
) -> T1: ...
|
|
@@ -92,7 +92,7 @@ class _KeyIter(Generic[LT]):
|
|
|
92
92
|
return True
|
|
93
93
|
|
|
94
94
|
def __lt__(self, other: _KeyIter[LT]) -> bool:
|
|
95
|
-
return self.reverse ^ (self.head_key < other.head_key)
|
|
95
|
+
return self.reverse ^ bool(self.head_key < other.head_key)
|
|
96
96
|
|
|
97
97
|
def __eq__(self, other: _KeyIter[LT]) -> bool: # type: ignore[override]
|
|
98
98
|
return not (self.head_key < other.head_key or other.head_key < self.head_key)
|
|
@@ -161,7 +161,7 @@ class ReverseLT(Generic[LT]):
|
|
|
161
161
|
self.key = key
|
|
162
162
|
|
|
163
163
|
def __lt__(self, other: ReverseLT[LT]) -> bool:
|
|
164
|
-
return other.key < self.key
|
|
164
|
+
return bool(other.key < self.key)
|
|
165
165
|
|
|
166
166
|
|
|
167
167
|
# Python's heapq provides a *min*-heap
|
|
@@ -8,7 +8,6 @@ from typing import (
|
|
|
8
8
|
Union,
|
|
9
9
|
Callable,
|
|
10
10
|
Optional,
|
|
11
|
-
Deque,
|
|
12
11
|
Generic,
|
|
13
12
|
Iterable,
|
|
14
13
|
Iterator,
|
|
@@ -17,14 +16,13 @@ from typing import (
|
|
|
17
16
|
overload,
|
|
18
17
|
AsyncGenerator,
|
|
19
18
|
)
|
|
20
|
-
from
|
|
19
|
+
from typing_extensions import TypeAlias
|
|
21
20
|
|
|
22
21
|
from ._typing import ACloseable, R, T, AnyIterable, ADD
|
|
23
22
|
from ._utility import public_module
|
|
24
23
|
from ._core import (
|
|
25
24
|
ScopedIter,
|
|
26
25
|
awaitify as _awaitify,
|
|
27
|
-
Sentinel,
|
|
28
26
|
borrow as _borrow,
|
|
29
27
|
)
|
|
30
28
|
from .builtins import (
|
|
@@ -33,6 +31,7 @@ from .builtins import (
|
|
|
33
31
|
enumerate as aenumerate,
|
|
34
32
|
iter as aiter,
|
|
35
33
|
)
|
|
34
|
+
from itertools import count as _counter
|
|
36
35
|
|
|
37
36
|
S = TypeVar("S")
|
|
38
37
|
T_co = TypeVar("T_co", covariant=True)
|
|
@@ -64,9 +63,6 @@ async def cycle(iterable: AnyIterable[T]) -> AsyncIterator[T]:
|
|
|
64
63
|
yield item
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
__ACCUMULATE_SENTINEL = Sentinel("<no default>")
|
|
68
|
-
|
|
69
|
-
|
|
70
66
|
async def add(x: ADD, y: ADD) -> ADD:
|
|
71
67
|
"""The default reduction of :py:func:`~.accumulate`"""
|
|
72
68
|
return x + y
|
|
@@ -78,7 +74,7 @@ async def accumulate(
|
|
|
78
74
|
Callable[[Any, Any], Any], Callable[[Any, Any], Awaitable[Any]]
|
|
79
75
|
] = add,
|
|
80
76
|
*,
|
|
81
|
-
initial: Any =
|
|
77
|
+
initial: Any = None,
|
|
82
78
|
) -> AsyncIterator[Any]:
|
|
83
79
|
"""
|
|
84
80
|
An :term:`asynchronous iterator` on the running reduction of ``iterable``
|
|
@@ -105,11 +101,7 @@ async def accumulate(
|
|
|
105
101
|
"""
|
|
106
102
|
async with ScopedIter(iterable) as item_iter:
|
|
107
103
|
try:
|
|
108
|
-
value = (
|
|
109
|
-
initial
|
|
110
|
-
if initial is not __ACCUMULATE_SENTINEL
|
|
111
|
-
else await anext(item_iter)
|
|
112
|
-
)
|
|
104
|
+
value = initial if initial is not None else await anext(item_iter)
|
|
113
105
|
except StopAsyncIteration:
|
|
114
106
|
raise TypeError(
|
|
115
107
|
"accumulate() of empty sequence with no initial value"
|
|
@@ -354,57 +346,79 @@ class NoLock:
|
|
|
354
346
|
return None
|
|
355
347
|
|
|
356
348
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
349
|
+
_get_tee_index = _counter().__next__
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
_TeeNode: TypeAlias = "list[T | _TeeNode[T]]"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class TeePeer(Generic[T]):
|
|
356
|
+
def __init__(
|
|
357
|
+
self,
|
|
358
|
+
iterator: AsyncIterator[T],
|
|
359
|
+
buffer: "_TeeNode[T]",
|
|
360
|
+
lock: AsyncContextManager[Any],
|
|
361
|
+
tee_peers: "set[int]",
|
|
362
|
+
) -> None:
|
|
363
|
+
self._iterator = iterator
|
|
364
|
+
self._lock = lock
|
|
365
|
+
self._buffer: _TeeNode[T] = buffer
|
|
366
|
+
self._tee_peers = tee_peers
|
|
367
|
+
self._tee_idx = _get_tee_index()
|
|
368
|
+
self._tee_peers.add(self._tee_idx)
|
|
369
|
+
|
|
370
|
+
def __aiter__(self):
|
|
371
|
+
return self
|
|
372
|
+
|
|
373
|
+
async def __anext__(self) -> T:
|
|
374
|
+
# the buffer is a singly-linked list as [value, [value, [...]]] | []
|
|
375
|
+
next_node = self._buffer
|
|
376
|
+
value: T
|
|
377
|
+
# for any most advanced TeePeer, the node is just []
|
|
378
|
+
# fetch the next value so we can mutate the node to [value, [...]]
|
|
379
|
+
if not next_node:
|
|
380
|
+
async with self._lock:
|
|
381
|
+
# Check if another peer produced an item while we were waiting for the lock
|
|
382
|
+
if not next_node:
|
|
383
|
+
await self._extend_buffer(next_node)
|
|
384
|
+
# for any other TeePeer, the node is already some [value, [...]]
|
|
385
|
+
value, self._buffer = next_node # type: ignore
|
|
386
|
+
return value
|
|
387
|
+
|
|
388
|
+
async def _extend_buffer(self, next_node: "_TeeNode[T]") -> None:
|
|
389
|
+
"""Extend the buffer by fetching a new item from the iterable"""
|
|
390
|
+
try:
|
|
391
|
+
# another peer may fill the buffer while we wait here
|
|
392
|
+
next_value = await self._iterator.__anext__()
|
|
393
|
+
except StopAsyncIteration:
|
|
394
|
+
# no one else managed to fetch a value either
|
|
395
|
+
if not next_node:
|
|
396
|
+
raise
|
|
397
|
+
else:
|
|
398
|
+
# skip nodes that were filled in the meantime
|
|
399
|
+
while next_node:
|
|
400
|
+
_, next_node = next_node # type: ignore
|
|
401
|
+
next_node[:] = next_value, []
|
|
402
|
+
|
|
403
|
+
async def aclose(self) -> None:
|
|
404
|
+
self._tee_peers.discard(self._tee_idx)
|
|
405
|
+
if not self._tee_peers and isinstance(self._iterator, ACloseable):
|
|
406
|
+
await self._iterator.aclose()
|
|
407
|
+
|
|
408
|
+
def __del__(self) -> None:
|
|
409
|
+
self._tee_peers.discard(self._tee_idx)
|
|
396
410
|
|
|
397
411
|
|
|
398
412
|
@public_module(__name__, "tee")
|
|
399
413
|
class Tee(Generic[T]):
|
|
400
|
-
"""
|
|
414
|
+
r"""
|
|
401
415
|
Create ``n`` separate asynchronous iterators over ``iterable``
|
|
402
416
|
|
|
403
417
|
This splits a single ``iterable`` into multiple iterators, each providing
|
|
404
418
|
the same items in the same order.
|
|
405
419
|
All child iterators may advance separately but share the same items
|
|
406
420
|
from ``iterable`` -- when the most advanced iterator retrieves an item,
|
|
407
|
-
it is buffered until
|
|
421
|
+
it is buffered until all other iterators have yielded it as well.
|
|
408
422
|
A ``tee`` works lazily and can handle an infinite ``iterable``, provided
|
|
409
423
|
that all iterators advance.
|
|
410
424
|
|
|
@@ -415,16 +429,9 @@ class Tee(Generic[T]):
|
|
|
415
429
|
await a.anext(previous) # advance one iterator
|
|
416
430
|
return a.map(operator.sub, previous, current)
|
|
417
431
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
immediately closes all children, and it can be used in an ``async with`` context
|
|
422
|
-
for the same effect.
|
|
423
|
-
|
|
424
|
-
If ``iterable`` is an iterator and read elsewhere, ``tee`` will *not*
|
|
425
|
-
provide these items. Also, ``tee`` must internally buffer each item until the
|
|
426
|
-
last iterator has yielded it; if the most and least advanced iterator differ
|
|
427
|
-
by most data, using a :py:class:`list` is more efficient (but not lazy).
|
|
432
|
+
If ``iterable`` is an iterator and read elsewhere, ``tee`` will generally *not*
|
|
433
|
+
provide these items. However, a ``tee`` of a ``tee`` shares its buffer with parent,
|
|
434
|
+
sibling and child ``tee``\ s so that each sees the same items.
|
|
428
435
|
|
|
429
436
|
If the underlying iterable is concurrency safe (``anext`` may be awaited
|
|
430
437
|
concurrently) the resulting iterators are concurrency safe as well. Otherwise,
|
|
@@ -432,9 +439,15 @@ class Tee(Generic[T]):
|
|
|
432
439
|
To enforce sequential use of ``anext``, provide a ``lock``
|
|
433
440
|
- e.g. an :py:class:`asyncio.Lock` instance in an :py:mod:`asyncio` application -
|
|
434
441
|
and access is automatically synchronised.
|
|
442
|
+
|
|
443
|
+
Unlike :py:func:`itertools.tee`, :py:func:`~.tee` returns a custom type instead
|
|
444
|
+
of a :py:class:`tuple`. Like a tuple, it can be indexed, iterated and unpacked
|
|
445
|
+
to get the child iterators. In addition, its :py:meth:`~.tee.aclose` method
|
|
446
|
+
immediately closes all children, and it can be used in an ``async with`` context
|
|
447
|
+
for the same effect.
|
|
435
448
|
"""
|
|
436
449
|
|
|
437
|
-
__slots__ = ("
|
|
450
|
+
__slots__ = ("_children",)
|
|
438
451
|
|
|
439
452
|
def __init__(
|
|
440
453
|
self,
|
|
@@ -443,16 +456,24 @@ class Tee(Generic[T]):
|
|
|
443
456
|
*,
|
|
444
457
|
lock: Optional[AsyncContextManager[Any]] = None,
|
|
445
458
|
):
|
|
446
|
-
|
|
447
|
-
|
|
459
|
+
buffer: _TeeNode[T]
|
|
460
|
+
peers: set[int]
|
|
461
|
+
if not isinstance(iterable, TeePeer):
|
|
462
|
+
iterator = aiter(iterable)
|
|
463
|
+
buffer = []
|
|
464
|
+
peers = set()
|
|
465
|
+
else:
|
|
466
|
+
iterator = iterable._iterator # pyright: ignore[reportPrivateUsage]
|
|
467
|
+
buffer = iterable._buffer # pyright: ignore[reportPrivateUsage]
|
|
468
|
+
peers = iterable._tee_peers # pyright: ignore[reportPrivateUsage]
|
|
448
469
|
self._children = tuple(
|
|
449
|
-
|
|
450
|
-
iterator
|
|
451
|
-
buffer
|
|
452
|
-
|
|
453
|
-
|
|
470
|
+
TeePeer(
|
|
471
|
+
iterator,
|
|
472
|
+
buffer,
|
|
473
|
+
lock if lock is not None else NoLock(),
|
|
474
|
+
peers,
|
|
454
475
|
)
|
|
455
|
-
for
|
|
476
|
+
for _ in range(n)
|
|
456
477
|
)
|
|
457
478
|
|
|
458
479
|
def __len__(self) -> int:
|
|
@@ -16,13 +16,17 @@ from ._typing import AnyIterable, ADD, T, T1, T2, T3, T4, T5
|
|
|
16
16
|
|
|
17
17
|
def cycle(iterable: AnyIterable[T]) -> AsyncIterator[T]: ...
|
|
18
18
|
@overload
|
|
19
|
-
def accumulate(
|
|
19
|
+
def accumulate(
|
|
20
|
+
iterable: AnyIterable[ADD], *, initial: None = ...
|
|
21
|
+
) -> AsyncIterator[ADD]: ...
|
|
20
22
|
@overload
|
|
21
23
|
def accumulate(iterable: AnyIterable[ADD], *, initial: ADD) -> AsyncIterator[ADD]: ...
|
|
22
24
|
@overload
|
|
23
25
|
def accumulate(
|
|
24
26
|
iterable: AnyIterable[T],
|
|
25
27
|
function: Callable[[T, T], T] | Callable[[T, T], Awaitable[T]],
|
|
28
|
+
*,
|
|
29
|
+
initial: None = ...,
|
|
26
30
|
) -> AsyncIterator[T]: ...
|
|
27
31
|
@overload
|
|
28
32
|
def accumulate(
|
|
@@ -76,7 +80,7 @@ def filterfalse(
|
|
|
76
80
|
predicate: Callable[[T], Any] | None, iterable: AnyIterable[T]
|
|
77
81
|
) -> AsyncIterator[T]: ...
|
|
78
82
|
@overload
|
|
79
|
-
def islice(iterable: AnyIterable[T],
|
|
83
|
+
def islice(iterable: AnyIterable[T], stop: int | None, /) -> AsyncIterator[T]: ...
|
|
80
84
|
@overload
|
|
81
85
|
def islice(
|
|
82
86
|
iterable: AnyIterable[T],
|
|
@@ -21,6 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.11",
|
|
22
22
|
"Programming Language :: Python :: 3.12",
|
|
23
23
|
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
24
25
|
]
|
|
25
26
|
license = {"file" = "LICENSE"}
|
|
26
27
|
keywords = ["async", "enumerate", "itertools", "builtins", "functools", "contextlib"]
|
|
@@ -80,3 +81,6 @@ verboseOutput = true
|
|
|
80
81
|
testpaths = [
|
|
81
82
|
"unittests",
|
|
82
83
|
]
|
|
84
|
+
|
|
85
|
+
[tool.black]
|
|
86
|
+
target-version = ["py38", "py39","py310", "py311", "py312", "py313", "py314"]
|
|
@@ -2,6 +2,7 @@ from typing import Callable, Any
|
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
+
from typing_extensions import get_annotations, Format
|
|
5
6
|
|
|
6
7
|
import asyncstdlib as a
|
|
7
8
|
|
|
@@ -175,5 +176,7 @@ def test_wrapper_attributes(size: "int | None"):
|
|
|
175
176
|
if name != "method":
|
|
176
177
|
continue
|
|
177
178
|
# test direct and literal annotation styles
|
|
178
|
-
assert Bar.method.
|
|
179
|
-
assert
|
|
179
|
+
assert get_annotations(Bar.method, format=Format.STRING)["int_arg"] == "int"
|
|
180
|
+
assert (
|
|
181
|
+
get_annotations(Bar().method, format=Format.STRING)["int_arg"] == "int"
|
|
182
|
+
)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from typing import AsyncIterator
|
|
1
2
|
import itertools
|
|
2
3
|
import sys
|
|
3
4
|
import platform
|
|
@@ -34,6 +35,7 @@ async def test_accumulate():
|
|
|
34
35
|
|
|
35
36
|
@sync
|
|
36
37
|
async def test_accumulate_default():
|
|
38
|
+
"""Test the default function of accumulate"""
|
|
37
39
|
for itertype in (asyncify, list):
|
|
38
40
|
assert await a.list(a.accumulate(itertype([0, 1]))) == list(
|
|
39
41
|
itertools.accumulate([0, 1])
|
|
@@ -53,10 +55,21 @@ async def test_accumulate_default():
|
|
|
53
55
|
|
|
54
56
|
@sync
|
|
55
57
|
async def test_accumulate_misuse():
|
|
58
|
+
"""Test wrong arguments to accumulate"""
|
|
56
59
|
with pytest.raises(TypeError):
|
|
57
60
|
assert await a.list(a.accumulate([]))
|
|
58
61
|
|
|
59
62
|
|
|
63
|
+
@sync
|
|
64
|
+
async def test_accumulate_initial():
|
|
65
|
+
"""Test the `initial` argument to accumulate"""
|
|
66
|
+
assert (
|
|
67
|
+
await a.list(a.accumulate(asyncify([1, 2, 3]), initial=None))
|
|
68
|
+
== await a.list(a.accumulate(asyncify([1, 2, 3])))
|
|
69
|
+
== list(itertools.accumulate([1, 2, 3], initial=None))
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
60
73
|
batched_cases = [
|
|
61
74
|
(range(10), 2, [(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)]),
|
|
62
75
|
(range(10), 3, [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9,)]),
|
|
@@ -329,7 +342,7 @@ async def test_tee():
|
|
|
329
342
|
|
|
330
343
|
@sync
|
|
331
344
|
async def test_tee_concurrent_locked():
|
|
332
|
-
"""Test that properly uses a lock for synchronisation"""
|
|
345
|
+
"""Test that tee properly uses a lock for synchronisation"""
|
|
333
346
|
items = [1, 2, 3, -5, 12, 78, -1, 111]
|
|
334
347
|
|
|
335
348
|
async def iter_values():
|
|
@@ -348,6 +361,52 @@ async def test_tee_concurrent_locked():
|
|
|
348
361
|
assert results == items
|
|
349
362
|
|
|
350
363
|
|
|
364
|
+
@pytest.mark.parametrize("concurrency", (1, 2, 4, 7))
|
|
365
|
+
@sync
|
|
366
|
+
async def test_tee_share(concurrency: int) -> None:
|
|
367
|
+
"""Test that related tees share their buffer and see all items"""
|
|
368
|
+
items = [1, 2, 3, -5, 12, 78, -1, 111]
|
|
369
|
+
|
|
370
|
+
async def tee_test(tee_state: AsyncIterator[int]) -> None:
|
|
371
|
+
"""Asynchronously check that `tee_state` includes all `items`"""
|
|
372
|
+
for expected in items:
|
|
373
|
+
assert expected == await a.anext(tee_state)
|
|
374
|
+
await Switch(0, concurrency)
|
|
375
|
+
|
|
376
|
+
# create tees that are multiple times removed from an initial iterator
|
|
377
|
+
item_iter = a.iter(items)
|
|
378
|
+
for tee_peer in a.tee(item_iter, n=concurrency):
|
|
379
|
+
await Schedule(tee_test(a.tee(tee_peer)[0]))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@sync
|
|
383
|
+
async def test_tee_share_deep() -> None:
|
|
384
|
+
"""Test that related tees share their buffer and see all items no matter when spawned"""
|
|
385
|
+
items = [1, 2, 3, -5, 12, 78, -1, 111]
|
|
386
|
+
|
|
387
|
+
async def tee_spawn_walker(
|
|
388
|
+
tee_state: AsyncIterator[int], start_idx: int = 0
|
|
389
|
+
) -> None:
|
|
390
|
+
"""Walk and check `tee_state` elements and spawn new walkers on every step"""
|
|
391
|
+
for idx in range(start_idx, len(items)):
|
|
392
|
+
await Switch(0, 3)
|
|
393
|
+
assert await a.anext(tee_state) == items[idx]
|
|
394
|
+
tee_state, *child_states = a.tee(tee_state, n=3)
|
|
395
|
+
await Schedule(
|
|
396
|
+
*(
|
|
397
|
+
tee_spawn_walker(child_state, idx + 1)
|
|
398
|
+
for child_state in child_states
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
await Switch()
|
|
402
|
+
|
|
403
|
+
head_peer, *child_peers = a.tee(items, n=3)
|
|
404
|
+
await Schedule(*(tee_spawn_walker(child, 0) for child in child_peers))
|
|
405
|
+
await Switch(len(items) // 2)
|
|
406
|
+
results = [item async for item in head_peer]
|
|
407
|
+
assert results == items
|
|
408
|
+
|
|
409
|
+
|
|
351
410
|
# see https://github.com/python/cpython/issues/74956
|
|
352
411
|
@pytest.mark.skipif(
|
|
353
412
|
sys.version_info < (3, 8),
|
|
@@ -381,6 +440,41 @@ async def test_tee_concurrent_unlocked():
|
|
|
381
440
|
await test_peer(this)
|
|
382
441
|
|
|
383
442
|
|
|
443
|
+
@pytest.mark.parametrize("size", [2, 3, 5, 9, 12])
|
|
444
|
+
@sync
|
|
445
|
+
async def test_tee_concurrent_ordering(size: int):
|
|
446
|
+
"""Test that tee respects concurrent ordering for all peers"""
|
|
447
|
+
|
|
448
|
+
class ConcurrentInvertedIterable:
|
|
449
|
+
"""Helper that concurrently iterates with earlier items taking longer"""
|
|
450
|
+
|
|
451
|
+
def __init__(self, count: int) -> None:
|
|
452
|
+
self.count = count
|
|
453
|
+
self._counter = itertools.count()
|
|
454
|
+
|
|
455
|
+
def __aiter__(self):
|
|
456
|
+
return self
|
|
457
|
+
|
|
458
|
+
async def __anext__(self):
|
|
459
|
+
value = next(self._counter)
|
|
460
|
+
if value >= self.count:
|
|
461
|
+
raise StopAsyncIteration()
|
|
462
|
+
await Switch(self.count - value)
|
|
463
|
+
return value
|
|
464
|
+
|
|
465
|
+
async def test_peer(peer_tee: AsyncIterator[int]):
|
|
466
|
+
# consume items from the tee with a delay so that slower items can arrive
|
|
467
|
+
seen_items: list[int] = []
|
|
468
|
+
async for item in peer_tee:
|
|
469
|
+
seen_items.append(item)
|
|
470
|
+
await Switch()
|
|
471
|
+
assert seen_items == expected_items
|
|
472
|
+
|
|
473
|
+
expected_items = list(range(size)[::-1])
|
|
474
|
+
peers = a.tee(ConcurrentInvertedIterable(size), n=size)
|
|
475
|
+
await Schedule(*map(test_peer, peers))
|
|
476
|
+
|
|
477
|
+
|
|
384
478
|
@sync
|
|
385
479
|
async def test_pairwise():
|
|
386
480
|
assert await a.list(a.pairwise(range(5))) == [(0, 1), (1, 2), (2, 3), (3, 4)]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|