codecov[bot]: # [Codecov](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/12?src=pr&el=h1) Report
> Merging [#12](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/12?src=pr&el=desc) into [master](https://codecov.io/gh/maxfischer2781/asyncstdlib/commit/7d45d075877433a2effb0b3aa9ec949816b892ac&el=desc) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/12/graphs/tree.svg?width=650&height=150&src=pr&token=frT6svSwUb)](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/12?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master      #12   +/-   ##
=======================================
  Coverage   99.47%   99.47%           
=======================================
  Files           8        8           
  Lines         764      766    +2     
  Branches      127      128    +1     
=======================================
+ Hits          760      762    +2     
  Misses          4        4           
```


| [Impacted Files](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/12?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [asyncstdlib/itertools.py](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/12/diff?src=pr&el=tree#diff-YXN5bmNzdGRsaWIvaXRlcnRvb2xzLnB5) | `98.98% <100.00%> (+0.01%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/12?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/12?src=pr&el=footer). Last update [7d45d07...549b511](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/12?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/asyncstdlib/itertools.py b/asyncstdlib/itertools.py
index b299840..dfdfd7f 100644
--- a/asyncstdlib/itertools.py
+++ b/asyncstdlib/itertools.py
@@ -199,22 +199,26 @@ async def islice(iterable: AnyIterable[T], *args: Optional[int]) -> AsyncIterato
     s = slice(*args)
     start, stop, step = s.start or 0, s.stop, s.step or 1
     async with ScopedIter(iterable) as async_iter:
-        # always consume the first ``start - 1`` items, even if the slice is empty
+        # always consume the first ``start`` items, even if the slice is empty
         if start > 0:
             async for _count, element in aenumerate(_borrow(async_iter), start=1):
                 if _count == start:
                     break
         if stop is None:
             async for idx, element in aenumerate(async_iter, start=0):
-                if idx % step == 0:
+                if not idx % step:
                     yield element
+        elif stop <= start:
+            return
         else:
-            stop -= start
+            # We would actually check ``idx >= stop -1`` later on.
+            # Since we do that for every ``idx``, we subtract ``1`` once here.
+            stop -= start + 1
             async for idx, element in aenumerate(async_iter, start=0):
-                if idx >= stop:
-                    return
                 if not idx % step:
                     yield element
+                if idx >= stop:
+                    return
 
 
 async def starmap(

islice consumes too many items
The ``stop`` condition of ``islice`` is evaluated by *peeking* at the next item, then discarding it:

https://github.com/maxfischer2781/asyncstdlib/blob/7d45d075877433a2effb0b3aa9ec949816b892ac/asyncstdlib/itertools.py#L213-L217

This results in two undesirable behaviours:
- if the next item is available, it is discarded. Consecutive use of ``islice`` drops items.
- if the next item is not available, ``isclice`` blocks indefinitely.
diff --git a/unittests/test_itertools.py b/unittests/test_itertools.py
index 21b1e8f..7585524 100644
--- a/unittests/test_itertools.py
+++ b/unittests/test_itertools.py
@@ -150,6 +150,28 @@ async def test_islice(iterable, slicing):
     assert await a.list(a.islice(asyncify(iterable), *slicing)) == expected
 
 
+async def ayield_exactly(count: int):
+    for item in range(count):
+        yield item
+    assert False, "Too many `anext` items requested"
+
+
+@sync
+@pytest.mark.parametrize(
+    "slicing", ((0,), (5,), (0, 20, 3), (5, 0, 1), (3, 50, 4)),
+)
+async def test_islice_exact(slicing):
+    """`isclice` consumes exactly as many items as needed"""
+    boundary = slice(*slicing) if len(slicing) > 1 else slice(0, slicing[0])
+    expected = list(range(boundary.stop)[boundary])
+    assert (
+        await a.list(
+            a.islice(ayield_exactly(max(boundary.start, boundary.stop)), *slicing)
+        )
+        == expected
+    )
+
+
 starmap_cases = [
     (lambda x, y: x + y, [(1, 2), (3, 4)]),
     (lambda *args: sum(args), [range(i) for i in range(1, 10)]),

pip install -e .[test]
parse_log_pytest
requirements.txt
pytest
3.8
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
-e git+https://github.com/maxfischer2781/asyncstdlib.git@7d45d075877433a2effb0b3aa9ec949816b892ac#egg=asyncstdlib
attrs==25.3.0
black==24.8.0
click==8.1.8
exceptiongroup==1.2.2
flake8==7.1.2
flake8-bugbear==24.12.12
iniconfig==2.1.0
mccabe==0.7.0
mypy-extensions==1.0.0
packaging==24.2
pathspec==0.12.1
platformdirs==4.3.6
pluggy==1.5.0
pycodestyle==2.12.1
pyflakes==3.2.0
pytest==8.3.5
tomli==2.2.1
typing_extensions==4.13.0

name: asyncstdlib
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=24.2=py38h06a4308_0
  - python=3.8.20=he870216_0
  - readline=8.2=h5eee18b_0
  - setuptools=75.1.0=py38h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.44.0=py38h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - asyncstdlib==1.1.0
      - attrs==25.3.0
      - black==24.8.0
      - click==8.1.8
      - exceptiongroup==1.2.2
      - flake8==7.1.2
      - flake8-bugbear==24.12.12
      - iniconfig==2.1.0
      - mccabe==0.7.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - pathspec==0.12.1
      - platformdirs==4.3.6
      - pluggy==1.5.0
      - pycodestyle==2.12.1
      - pyflakes==3.2.0
      - pytest==8.3.5
      - tomli==2.2.1
      - typing-extensions==4.13.0
prefix: /opt/conda/envs/asyncstdlib

unittests/test_itertools.py::test_islice_exact[slicing0] unittests/test_itertools.py::test_islice_exact[slicing1] unittests/test_itertools.py::test_islice_exact[slicing2] unittests/test_itertools.py::test_islice_exact[slicing3] unittests/test_itertools.py::test_islice_exact[slicing4]
unittests/test_itertools.py::test_accumulate unittests/test_itertools.py::test_accumulate_default unittests/test_itertools.py::test_accumulate_misuse unittests/test_itertools.py::test_cycle unittests/test_itertools.py::test_chain[iterables0] unittests/test_itertools.py::test_chain[iterables1] unittests/test_itertools.py::test_chain[iterables2] unittests/test_itertools.py::test_compress[data0-selectors0] unittests/test_itertools.py::test_compress[data1-selectors1] unittests/test_itertools.py::test_compress[data2-selectors2] unittests/test_itertools.py::test_compress[data3-selectors3] unittests/test_itertools.py::test_dropwhile[iterable0-<lambda>] unittests/test_itertools.py::test_dropwhile[iterable1-<lambda>] unittests/test_itertools.py::test_dropwhile[iterable2-<lambda>] unittests/test_itertools.py::test_dropwhile[iterable3-<lambda>] unittests/test_itertools.py::test_dropwhile[iterable4-<lambda>] unittests/test_itertools.py::test_dropwhile[iterable5-<lambda>] unittests/test_itertools.py::test_takewhile[iterable0-<lambda>] unittests/test_itertools.py::test_takewhile[iterable1-<lambda>] unittests/test_itertools.py::test_takewhile[iterable2-<lambda>] unittests/test_itertools.py::test_takewhile[iterable3-<lambda>] unittests/test_itertools.py::test_takewhile[iterable4-<lambda>] unittests/test_itertools.py::test_takewhile[iterable5-<lambda>] unittests/test_itertools.py::test_islice[slicing0-iterable0] unittests/test_itertools.py::test_islice[slicing0-iterable1] unittests/test_itertools.py::test_islice[slicing0-iterable2] unittests/test_itertools.py::test_islice[slicing0-iterable3] unittests/test_itertools.py::test_islice[slicing1-iterable0] unittests/test_itertools.py::test_islice[slicing1-iterable1] unittests/test_itertools.py::test_islice[slicing1-iterable2] unittests/test_itertools.py::test_islice[slicing1-iterable3] unittests/test_itertools.py::test_islice[slicing2-iterable0] unittests/test_itertools.py::test_islice[slicing2-iterable1] unittests/test_itertools.py::test_islice[slicing2-iterable2] unittests/test_itertools.py::test_islice[slicing2-iterable3] unittests/test_itertools.py::test_islice[slicing3-iterable0] unittests/test_itertools.py::test_islice[slicing3-iterable1] unittests/test_itertools.py::test_islice[slicing3-iterable2] unittests/test_itertools.py::test_islice[slicing3-iterable3] unittests/test_itertools.py::test_islice[slicing4-iterable0] unittests/test_itertools.py::test_islice[slicing4-iterable1] unittests/test_itertools.py::test_islice[slicing4-iterable2] unittests/test_itertools.py::test_islice[slicing4-iterable3] unittests/test_itertools.py::test_islice[slicing5-iterable0] unittests/test_itertools.py::test_islice[slicing5-iterable1] unittests/test_itertools.py::test_islice[slicing5-iterable2] unittests/test_itertools.py::test_islice[slicing5-iterable3] unittests/test_itertools.py::test_islice[slicing6-iterable0] unittests/test_itertools.py::test_islice[slicing6-iterable1] unittests/test_itertools.py::test_islice[slicing6-iterable2] unittests/test_itertools.py::test_islice[slicing6-iterable3] unittests/test_itertools.py::test_starmap[<lambda>-iterable0] unittests/test_itertools.py::test_starmap[<lambda>-iterable1] unittests/test_itertools.py::test_tee unittests/test_itertools.py::test_zip_longest unittests/test_itertools.py::test_groupby[keys-identity-iterable0] unittests/test_itertools.py::test_groupby[keys-identity-iterable1] unittests/test_itertools.py::test_groupby[keys-identity-iterable2] unittests/test_itertools.py::test_groupby[keys-identity-iterable3] unittests/test_itertools.py::test_groupby[keys-identity-iterable4] unittests/test_itertools.py::test_groupby[keys-modulo-iterable0] unittests/test_itertools.py::test_groupby[keys-modulo-iterable1] unittests/test_itertools.py::test_groupby[keys-modulo-iterable2] unittests/test_itertools.py::test_groupby[keys-modulo-iterable3] unittests/test_itertools.py::test_groupby[keys-modulo-iterable4] unittests/test_itertools.py::test_groupby[keys-divide-iterable0] unittests/test_itertools.py::test_groupby[keys-divide-iterable1] unittests/test_itertools.py::test_groupby[keys-divide-iterable2] unittests/test_itertools.py::test_groupby[keys-divide-iterable3] unittests/test_itertools.py::test_groupby[keys-divide-iterable4] unittests/test_itertools.py::test_groupby[values-identity-iterable0] unittests/test_itertools.py::test_groupby[values-identity-iterable1] unittests/test_itertools.py::test_groupby[values-identity-iterable2] unittests/test_itertools.py::test_groupby[values-identity-iterable3] unittests/test_itertools.py::test_groupby[values-identity-iterable4] unittests/test_itertools.py::test_groupby[values-modulo-iterable0] unittests/test_itertools.py::test_groupby[values-modulo-iterable1] unittests/test_itertools.py::test_groupby[values-modulo-iterable2] unittests/test_itertools.py::test_groupby[values-modulo-iterable3] unittests/test_itertools.py::test_groupby[values-modulo-iterable4] unittests/test_itertools.py::test_groupby[values-divide-iterable0] unittests/test_itertools.py::test_groupby[values-divide-iterable1] unittests/test_itertools.py::test_groupby[values-divide-iterable2] unittests/test_itertools.py::test_groupby[values-divide-iterable3] unittests/test_itertools.py::test_groupby[values-divide-iterable4]
asyncstdlib/itertools.py
asyncstdlib/itertools.py