Repository: maxfischer2781/asyncstdlib

Issue:
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.

Discussion:
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).


Patch:
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(


Test patch:
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)]),