Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 52 additions & 11 deletions asyncstdlib/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
Optional,
Dict,
Any,
overload,
)
import builtins as _sync_builtins

from ._typing import T, R, HK, LT, AnyIterable
from ._typing import T, T1, R, HK, LT, AnyIterable
from ._core import (
aiter,
ScopedIter,
Expand Down Expand Up @@ -241,6 +242,23 @@ async def map(
__MIN_MAX_DEFAULT = Sentinel("<no default>")


@overload
async def max(
iterable: AnyIterable[T],
*,
key: Optional[Callable[[T], Any]] = None,
) -> T: ...


@overload
async def max(
iterable: AnyIterable[T],
*,
key: Optional[Callable[[T], Any]] = None,
default: T1,
) -> Union[T, T1]: ...


async def max(
iterable: AnyIterable[Any],
*,
Expand All @@ -255,7 +273,9 @@ async def max(
The ``key`` argument specifies a one-argument ordering function like that used
for :py:meth:`list.sort`. It may be a regular or async callable and defaults to
the identity function. The ``default`` argument specifies an object to return
if the provided ``iterable`` is empty. If the ``iterable`` is empty and
if the provided ``iterable`` is empty. It is returned unchanged without
applying ``key`` and may have a different type from the iterable items.
If the ``iterable`` is empty and
``default`` is not provided, a :py:exc:`ValueError` is raised.

.. note::
Expand All @@ -267,6 +287,23 @@ async def max(
return await _min_max(iterable, key, True, default)


@overload
async def min(
iterable: AnyIterable[T],
*,
key: Optional[Callable[[T], Any]] = None,
) -> T: ...


@overload
async def min(
iterable: AnyIterable[T],
*,
key: Optional[Callable[[T], Any]] = None,
default: T1,
) -> Union[T, T1]: ...


async def min(
iterable: AnyIterable[Any],
*,
Expand All @@ -281,7 +318,9 @@ async def min(
The ``key`` argument specifies a one-argument ordering function like that used
for :py:meth:`list.sort`. It may be a regular or async callable and defaults to
the identity function. The ``default`` argument specifies an object to return
if the provided ``iterable`` is empty. If the ``iterable`` is empty and
if the provided ``iterable`` is empty. It is returned unchanged without
applying ``key`` and may have a different type from the iterable items.
If the ``iterable`` is empty and
``default`` is not provided, a :py:exc:`ValueError` is raised.

.. note::
Expand All @@ -297,20 +336,22 @@ async def _min_max(
iterable: AnyIterable[LT],
key: Optional[Callable[[LT], Any]],
invert: bool,
default: LT = __MIN_MAX_DEFAULT, # type: ignore
) -> LT:
default: T1 = __MIN_MAX_DEFAULT, # type: ignore
) -> Union[LT, T1]:
"""
Implementation of ``min``/``max``

:param invert: compute ``max`` if ``True`` and ``min`` otherwise
"""
async with ScopedIter(iterable) as item_iter:
best = await anext(item_iter, default=default)
# this implies that item_iter is empty and default is __MIN_MAX_DEFAULT
if best is __MIN_MAX_DEFAULT: # type: ignore
name = "max" if invert else "min"
raise ValueError(f"{name}() arg is an empty sequence")
elif key is None:
try:
best = await anext(item_iter)
except StopAsyncIteration:
if default is __MIN_MAX_DEFAULT: # type: ignore
name = "max" if invert else "min"
raise ValueError(f"{name}() arg is an empty sequence") from None
return default
if key is None:
async for item in item_iter:
if invert ^ bool(item < best):
best = item
Expand Down
26 changes: 25 additions & 1 deletion typetests/test_builtins.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import TypeVar
from typing import TypeVar, Optional, Union, List, Dict
from typing_extensions import assert_type
from asyncstdlib import builtins

T = TypeVar("T")
Expand All @@ -15,3 +16,26 @@ async def async_identity(v: T) -> T:
async def test_min_asyncneutral() -> None:
await builtins.min([1, 2, 3], key=identity)
await builtins.min([1, 2, 3], key=async_identity)


async def test_extrema_independent_default_type() -> None:
records: List[Dict[str, int]] = [{"priority": 1}]

def priority(record: Dict[str, int]) -> int:
return record["priority"]

async def async_priority(record: Dict[str, int]) -> int:
return record["priority"]

assert_type(await builtins.min(records, key=priority), Dict[str, int])
assert_type(await builtins.max(records, key=async_priority), Dict[str, int])
assert_type(
await builtins.min(records, key=priority, default=None),
Optional[Dict[str, int]],
)
assert_type(
await builtins.max(records, key=async_priority, default=None),
Optional[Dict[str, int]],
)
assert_type(await builtins.min([1], default="empty"), Union[int, str])
assert_type(await builtins.max([1], default="empty"), Union[int, str])
36 changes: 36 additions & 0 deletions unittests/test_builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,39 @@ def collision_key(x: int) -> int:
assert await a.sorted(values, key=awaitify(collision_key)) == sorted(
values, key=collision_key
)


@pytest.mark.parametrize("function", [a.min, a.max])
@pytest.mark.parametrize("async_input", [False, True])
@pytest.mark.parametrize("async_key", [False, True])
@sync
async def test_extrema_return_empty_default_without_applying_key(
function, async_input, async_key
):
fallback = object()

def priority(record):
return record["priority"]

assert (
await function(
asyncify([]) if async_input else [],
key=awaitify(priority) if async_key else priority,
default=fallback,
)
is fallback
)


@pytest.mark.parametrize("function", [a.min, a.max])
@sync
async def test_extrema_apply_key_to_item_identical_to_default(function):
fallback = {"priority": 1}
calls = []

def priority(record):
calls.append(record)
return record["priority"]

assert await function([fallback], key=priority, default=fallback) is fallback
assert calls == [fallback]