Skip to content

Commit 0c5e206

Browse files
Merge pull request #194 from maxfischer2781/pyver/v3.14
Feature parity with Python 3.14
2 parents 9d6490a + 74afce6 commit 0c5e206

9 files changed

Lines changed: 94 additions & 29 deletions

File tree

asyncstdlib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
from .asynctools import borrow, scoped_iter, await_each, any_iter, apply, sync
4646
from .heapq import merge, nlargest, nsmallest
4747

48-
__version__ = "3.13.3"
48+
__version__ = "3.14.0"
4949

5050
__all__ = [
5151
"anext",

asyncstdlib/asynctools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from asyncio import iscoroutinefunction
21
from functools import wraps
2+
from inspect import iscoroutinefunction
33
from typing import (
44
Union,
55
AsyncContextManager,

asyncstdlib/builtins.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,27 +204,35 @@ async def _zip_inner_strict(
204204

205205
async def map(
206206
function: Union[Callable[..., R], Callable[..., Awaitable[R]]],
207-
*iterable: AnyIterable[Any],
207+
iterable: AnyIterable[Any],
208+
/,
209+
*iterables: AnyIterable[Any],
210+
strict: bool = False,
208211
) -> AsyncIterator[R]:
209212
r"""
210213
An async iterator mapping an (async) function to items from (async) iterables
211214
215+
:raises ValueError: if the ``iterables`` are not equal length and ``strict`` is set
216+
212217
At each step, ``map`` collects the next item from each iterable and calls
213-
``function`` with all items; if ``function`` provides an awaitable,
218+
``function`` with these items; if ``function`` provides an awaitable,
214219
it is ``await``\ ed. The result is the next value of ``map``.
215220
Barring sync/async translation, ``map`` is equivalent to
216221
``(await function(*args) async for args in zip(iterables))``.
217222
218223
It is important that ``func`` receives *one* item from *each* iterable at
219-
every step. For *n* ``iterable``, ``func`` must take *n* positional arguments.
220-
Similar to :py:func:`~.zip`, ``map`` is exhausted as soon as its
221-
*first* argument is exhausted.
224+
every step. For *n* ``iterables``, ``func`` must take *n* positional arguments.
225+
Similar to :py:func:`~.zip`, ``map`` is exhausted as soon as any of its `iterables`
226+
is exhausted.
227+
When called with ``strict=True``, all ``iterables`` must be of same length;
228+
in this mode ``map`` raises :py:exc:`ValueError` if any ``iterables`` are not
229+
exhausted with the others.
222230
223231
The ``function`` may be a regular or async callable.
224-
Multiple ``iterable`` may be mixed regular and async iterables.
232+
Multiple ``iterables`` may be mixed regular and async iterables.
225233
"""
226234
function = _awaitify(function)
227-
async with ScopedIter(zip(*iterable)) as args_iter:
235+
async with ScopedIter(zip(iterable, *iterables, strict=strict)) as args_iter:
228236
async for args in args_iter:
229237
result = function(*args)
230238
yield await result

asyncstdlib/builtins.pyi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,26 +82,30 @@ def map(
8282
function: Callable[[T1], Awaitable[R]],
8383
__it1: AnyIterable[T1],
8484
/,
85+
strict: bool = ...,
8586
) -> AsyncIterator[R]: ...
8687
@overload
8788
def map(
8889
function: Callable[[T1], R],
8990
__it1: AnyIterable[T1],
9091
/,
92+
strict: bool = ...,
9193
) -> AsyncIterator[R]: ...
9294
@overload
9395
def map(
9496
function: Callable[[T1, T2], Awaitable[R]],
9597
__it1: AnyIterable[T1],
9698
__it2: AnyIterable[T2],
9799
/,
100+
strict: bool = ...,
98101
) -> AsyncIterator[R]: ...
99102
@overload
100103
def map(
101104
function: Callable[[T1, T2], R],
102105
__it1: AnyIterable[T1],
103106
__it2: AnyIterable[T2],
104107
/,
108+
strict: bool = ...,
105109
) -> AsyncIterator[R]: ...
106110
@overload
107111
def map(
@@ -110,6 +114,7 @@ def map(
110114
__it2: AnyIterable[T2],
111115
__it3: AnyIterable[T3],
112116
/,
117+
strict: bool = ...,
113118
) -> AsyncIterator[R]: ...
114119
@overload
115120
def map(
@@ -118,6 +123,7 @@ def map(
118123
__it2: AnyIterable[T2],
119124
__it3: AnyIterable[T3],
120125
/,
126+
strict: bool = ...,
121127
) -> AsyncIterator[R]: ...
122128
@overload
123129
def map(
@@ -127,6 +133,7 @@ def map(
127133
__it3: AnyIterable[T3],
128134
__it4: AnyIterable[T4],
129135
/,
136+
strict: bool = ...,
130137
) -> AsyncIterator[R]: ...
131138
@overload
132139
def map(
@@ -136,6 +143,7 @@ def map(
136143
__it3: AnyIterable[T3],
137144
__it4: AnyIterable[T4],
138145
/,
146+
strict: bool = ...,
139147
) -> AsyncIterator[R]: ...
140148
@overload
141149
def map(
@@ -146,6 +154,7 @@ def map(
146154
__it4: AnyIterable[T4],
147155
__it5: AnyIterable[T5],
148156
/,
157+
strict: bool = ...,
149158
) -> AsyncIterator[R]: ...
150159
@overload
151160
def map(
@@ -156,6 +165,7 @@ def map(
156165
__it4: AnyIterable[T4],
157166
__it5: AnyIterable[T5],
158167
/,
168+
strict: bool = ...,
159169
) -> AsyncIterator[R]: ...
160170
@overload
161171
def map(
@@ -167,6 +177,7 @@ def map(
167177
__it5: AnyIterable[Any],
168178
/,
169179
*iterable: AnyIterable[Any],
180+
strict: bool = ...,
170181
) -> AsyncIterator[R]: ...
171182
@overload
172183
def map(
@@ -178,6 +189,7 @@ def map(
178189
__it5: AnyIterable[Any],
179190
/,
180191
*iterable: AnyIterable[Any],
192+
strict: bool = ...,
181193
) -> AsyncIterator[R]: ...
182194
@overload
183195
async def max(iterable: AnyIterable[LT], *, key: None = ...) -> LT: ...

asyncstdlib/functools.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from asyncio import iscoroutinefunction
1+
from inspect import iscoroutinefunction
22
from typing import (
33
Callable,
44
Awaitable,
@@ -281,6 +281,7 @@ def decorator(
281281
async def reduce(
282282
function: Union[Callable[[T, T], T], Callable[[T, T], Awaitable[T]]],
283283
iterable: AnyIterable[T],
284+
/,
284285
initial: T = __REDUCE_SENTINEL, # type: ignore
285286
) -> T:
286287
"""

asyncstdlib/functools.pyi

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,27 @@ def cached_property(
3434
) -> Callable[[Callable[[T], Awaitable[R]]], CachedProperty[T, R]]: ...
3535
@overload
3636
async def reduce(
37-
function: Callable[[T1, T2], Awaitable[T1]], iterable: AnyIterable[T2], initial: T1
37+
function: Callable[[T1, T2], Awaitable[T1]],
38+
iterable: AnyIterable[T2],
39+
/,
40+
initial: T1,
3841
) -> T1: ...
3942
@overload
4043
async def reduce(
41-
function: Callable[[T, T], Awaitable[T]], iterable: AnyIterable[T]
44+
function: Callable[[T, T], Awaitable[T]],
45+
iterable: AnyIterable[T],
46+
/,
4247
) -> T: ...
4348
@overload
4449
async def reduce(
45-
function: Callable[[T1, T2], T1], iterable: AnyIterable[T2], initial: T1
50+
function: Callable[[T1, T2], T1],
51+
iterable: AnyIterable[T2],
52+
/,
53+
initial: T1,
4654
) -> T1: ...
4755
@overload
48-
async def reduce(function: Callable[[T, T], T], iterable: AnyIterable[T]) -> T: ...
56+
async def reduce(
57+
function: Callable[[T, T], T],
58+
iterable: AnyIterable[T],
59+
/,
60+
) -> T: ...

asyncstdlib/itertools.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
AsyncGenerator,
1818
TYPE_CHECKING,
1919
)
20+
2021
if TYPE_CHECKING:
2122
from typing_extensions import TypeAlias
2223

docs/source/api/builtins.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,13 @@ Iterator transforming
4545

4646
The ``strict`` parameter.
4747

48-
.. autofunction:: map(function: (T, ...) → (await) R, iterable: (async) iter T, ...)
48+
.. autofunction:: map(function: (T, ...) → (await) R, iterable: (async) iter T, ..., /, strict: bool = True)
4949
:async-for: :R
5050

51+
.. versionadded:: 3.14.0
52+
53+
The ``strict`` parameter.
54+
5155
.. autofunction:: enumerate(iterable: (async) iter T, start=0)
5256
:async-for: :(int, T)
5357

unittests/test_builtins.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import random
2+
from typing import Any, Callable, Coroutine, TypeVar
23

34
import pytest
45

56
import asyncstdlib as a
67

78
from .utility import sync, asyncify, awaitify
89

10+
COR = TypeVar("COR", bound=Callable[..., Coroutine[Any, Any, Any]])
911

10-
def hide_coroutine(corofunc):
11-
def wrapper(*args, **kwargs):
12+
13+
def hide_coroutine(corofunc: COR) -> COR:
14+
"""Make a coroutine function look like a regular function returning a coroutine"""
15+
16+
def wrapper(*args, **kwargs): # type: ignore
1217
return corofunc(*args, **kwargs)
1318

14-
return wrapper
19+
return wrapper # type: ignore
1520

1621

1722
@sync
@@ -94,7 +99,7 @@ async def __aiter__(self):
9499

95100
@sync
96101
async def test_map_as():
97-
async def map_op(value):
102+
async def map_op(value: int) -> int:
98103
return value * 2
99104

100105
assert [value async for value in a.map(map_op, range(5))] == list(range(0, 10, 2))
@@ -105,7 +110,7 @@ async def map_op(value):
105110

106111
@sync
107112
async def test_map_sa():
108-
def map_op(value):
113+
async def map_op(value: int) -> int:
109114
return value * 2
110115

111116
assert [value async for value in a.map(map_op, asyncify(range(5)))] == list(
@@ -118,7 +123,7 @@ def map_op(value):
118123

119124
@sync
120125
async def test_map_aa():
121-
async def map_op(value):
126+
async def map_op(value: int) -> int:
122127
return value * 2
123128

124129
assert [value async for value in a.map(map_op, asyncify(range(5)))] == list(
@@ -130,6 +135,28 @@ async def map_op(value):
130135
] == list(range(10, 20, 4))
131136

132137

138+
@pytest.mark.parametrize(
139+
"itrs",
140+
[
141+
(range(4), range(5), range(5)),
142+
(range(5), range(4), range(5)),
143+
(range(5), range(5), range(4)),
144+
],
145+
)
146+
@sync
147+
async def test_map_strict_unequal(itrs: "tuple[range, ...]"):
148+
def triple_sum(x: int, y: int, z: int) -> int:
149+
return x + y + z
150+
151+
# no error without strict
152+
async for _ in a.map(triple_sum, *itrs):
153+
pass
154+
# error with strict
155+
with pytest.raises(ValueError):
156+
async for _ in a.map(triple_sum, *itrs, strict=True):
157+
pass
158+
159+
133160
@sync
134161
async def test_max_default():
135162
assert await a.max((), default=3) == 3
@@ -142,7 +169,7 @@ async def test_max_default():
142169

143170
@sync
144171
async def test_max_sa():
145-
async def minus(x):
172+
async def minus(x: int) -> int:
146173
return -x
147174

148175
assert await a.max(asyncify((1, 2, 3, 4))) == 4
@@ -167,7 +194,7 @@ async def test_min_default():
167194

168195
@sync
169196
async def test_min_sa():
170-
async def minus(x):
197+
async def minus(x: int) -> int:
171198
return -x
172199

173200
assert await a.min(asyncify((1, 2, 3, 4))) == 1
@@ -180,7 +207,7 @@ async def minus(x):
180207

181208
@sync
182209
async def test_filter_as():
183-
async def map_op(value):
210+
async def map_op(value: int) -> bool:
184211
return value % 2 == 0
185212

186213
assert [value async for value in a.filter(map_op, range(5))] == list(range(0, 5, 2))
@@ -194,7 +221,7 @@ async def map_op(value):
194221

195222
@sync
196223
async def test_filter_sa():
197-
def map_op(value):
224+
def map_op(value: int) -> bool:
198225
return value % 2 == 0
199226

200227
assert [value async for value in a.filter(map_op, asyncify(range(5)))] == list(
@@ -208,7 +235,7 @@ def map_op(value):
208235

209236
@sync
210237
async def test_filter_aa():
211-
async def map_op(value):
238+
async def map_op(value: int) -> bool:
212239
return value % 2 == 0
213240

214241
assert [value async for value in a.filter(map_op, asyncify(range(5)))] == list(
@@ -286,7 +313,7 @@ async def test_types():
286313
@pytest.mark.parametrize("sortable", sortables)
287314
@pytest.mark.parametrize("reverse", [True, False])
288315
@sync
289-
async def test_sorted_direct(sortable, reverse):
316+
async def test_sorted_direct(sortable: "list[int] | list[float]", reverse: bool):
290317
assert await a.sorted(sortable, reverse=reverse) == sorted(
291318
sortable, reverse=reverse
292319
)
@@ -305,12 +332,12 @@ async def test_sorted_direct(sortable, reverse):
305332
async def test_sorted_stable():
306333
values = [-i for i in range(20)]
307334

308-
def collision_key(x):
335+
def collision_key(x: int) -> int:
309336
return x // 2
310337

311338
# test the test...
312339
assert sorted(values, key=collision_key) != [
313-
item for key, item in sorted([(collision_key(i), i) for i in values])
340+
item for _, item in sorted([(collision_key(i), i) for i in values])
314341
]
315342
# test the implementation
316343
assert await a.sorted(values, key=awaitify(collision_key)) == sorted(

0 commit comments

Comments
 (0)