diff --git a/asyncstdlib/__init__.py b/asyncstdlib/__init__.py index 9152f01..00f45e6 100644 --- a/asyncstdlib/__init__.py +++ b/asyncstdlib/__init__.py @@ -25,6 +25,7 @@ chain, compress, dropwhile, + filterfalse, islice, takewhile, starmap, @@ -71,6 +72,7 @@ "chain", "compress", "dropwhile", + "filterfalse", "takewhile", "islice", "starmap", diff --git a/asyncstdlib/itertools.py b/asyncstdlib/itertools.py index f6f06ee..ccd98e9 100644 --- a/asyncstdlib/itertools.py +++ b/asyncstdlib/itertools.py @@ -242,6 +242,27 @@ async def dropwhile( yield item +async def filterfalse( + predicate: Union[Callable[[T], bool], Callable[[T], Awaitable[bool]], None], + iterable: AnyIterable[T], +) -> AsyncIterator[T]: + """ + Yield items from ``iterable`` for which ``predicate(item)`` is false. + + If ``predicate`` is ``None``, return items which are false. + + Lazily iterates over ``iterable``, yielding only items for which + ``predicate`` of the current item is false. + """ + async with ScopedIter(iterable) as async_iter: + if predicate is None: + predicate = bool + predicate = _awaitify(predicate) + async for item in async_iter: + if not await predicate(item): + yield item + + async def islice(iterable: AnyIterable[T], *args: Optional[int]) -> AsyncIterator[T]: """ An :term:`asynchronous iterator` over items from ``iterable`` in a :py:class:`slice` diff --git a/docs/source/api/itertools.rst b/docs/source/api/itertools.rst index 2610ed0..582bdec 100644 --- a/docs/source/api/itertools.rst +++ b/docs/source/api/itertools.rst @@ -43,6 +43,9 @@ Iterator filtering .. autofunction:: dropwhile(predicate: (T) → (await) bool, iterable: (async) iter T) :async-for: :T +.. autofunction:: filterfalse(predicate: None | (T) → (await) bool, iterable: (async) iter T) + :async-for: :T + .. autofunction:: takewhile(predicate: (T) → (await) bool, iterable: (async) iter T) :async-for: :T diff --git a/unittests/test_itertools.py b/unittests/test_itertools.py index 43ce466..897af09 100644 --- a/unittests/test_itertools.py +++ b/unittests/test_itertools.py @@ -178,6 +178,35 @@ async def test_dropwhile(iterable, predicate): ) +filterfalse_cases = ( + (lambda x: True, [0, 1] * 5), + (lambda x: False, [0, 1] * 5), + (lambda x: x, [0, 1] * 5), + (lambda x: x < 5, range(20)), + (lambda x: x > 5, range(20)), +) + + +@pytest.mark.parametrize("predicate, iterable", filterfalse_cases) +@sync +async def test_filterfalse(predicate, iterable): + expected = list(itertools.filterfalse(predicate, iterable)) + assert await a.list(a.filterfalse(predicate, iterable)) == expected + assert await a.list(a.filterfalse(awaitify(predicate), iterable)) == expected + assert await a.list(a.filterfalse(predicate, asyncify(iterable))) == expected + assert ( + await a.list(a.filterfalse(awaitify(predicate), asyncify(iterable))) == expected + ) + + +@pytest.mark.parametrize("predicate, iterable", filterfalse_cases) +@sync +async def test_filterfalse_predicate_none(predicate, iterable): + expected = list(itertools.filterfalse(None, iterable)) + assert await a.list(a.filterfalse(None, iterable)) == expected + assert await a.list(a.filterfalse(None, asyncify(iterable))) == expected + + @pytest.mark.parametrize("iterable, predicate", droptakewhile_cases) @sync async def test_takewhile(iterable, predicate):