Repository: maxfischer2781/asyncstdlib

Issue:
Add itertools.pairwise
[Python 3.10 will add ``itertools.pairwise``](https://docs.python.org/3.10/library/itertools.html#itertools.pairwise), which is basically a more specialised cousin of ``itertools.tee``.

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

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

```diff
@@           Coverage Diff           @@
##           master      #25   +/-   ##
=======================================
  Coverage   99.52%   99.52%           
=======================================
  Files           9        9           
  Lines         841      847    +6     
  Branches      139      140    +1     
=======================================
+ Hits          837      843    +6     
  Misses          4        4           
```


| [Impacted Files](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/25?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [asyncstdlib/\_\_init\_\_.py](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/25/diff?src=pr&el=tree#diff-YXN5bmNzdGRsaWIvX19pbml0X18ucHk=) | `100.00% <ø> (ø)` | |
| [asyncstdlib/itertools.py](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/25/diff?src=pr&el=tree#diff-YXN5bmNzdGRsaWIvaXRlcnRvb2xzLnB5) | `99.01% <100.00%> (+0.03%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/25?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/25?src=pr&el=footer). Last update [b07ce35...0703c0a](https://codecov.io/gh/maxfischer2781/asyncstdlib/pull/25?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/asyncstdlib/__init__.py b/asyncstdlib/__init__.py
index 14b2178..8a39c89 100644
--- a/asyncstdlib/__init__.py
+++ b/asyncstdlib/__init__.py
@@ -29,6 +29,7 @@ from .itertools import (
     takewhile,
     starmap,
     tee,
+    pairwise,
     zip_longest,
     groupby,
 )
@@ -73,6 +74,7 @@ __all__ = [
     "islice",
     "starmap",
     "tee",
+    "pairwise",
     "zip_longest",
     "groupby",
     # asynctools
diff --git a/asyncstdlib/itertools.py b/asyncstdlib/itertools.py
index b24c4c5..987f39a 100644
--- a/asyncstdlib/itertools.py
+++ b/asyncstdlib/itertools.py
@@ -365,6 +365,21 @@ class Tee(Generic[T]):
 tee = Tee
 
 
+async def pairwise(iterable: AnyIterable[T]) -> AsyncIterator[Tuple[T, T]]:
+    """
+    Yield successive, overlapping pairs of items from ``iterable``
+
+    Pairs are yielded as the newest item is available from ``iterable``. No pair
+    is emitted if ``iterable`` has one or zero items; however, if there is one item
+    ``pairwise`` will wait for and consume it before finishing.
+    """
+    async with ScopedIter(iterable) as async_iter:
+        prev = await anext(async_iter, None)  # any default is fine – we never yield it
+        async for current in async_iter:
+            yield prev, current
+            prev = current
+
+
 async def _repeat(value):
     while True:
         yield value
diff --git a/docs/source/api/itertools.rst b/docs/source/api/itertools.rst
index 483f810..0a140ea 100644
--- a/docs/source/api/itertools.rst
+++ b/docs/source/api/itertools.rst
@@ -60,6 +60,11 @@ Iterator splitting
 .. autofunction:: tee(iterable: (async) iter T, n: int = 2)
     :for: :(async iter T, ...)
 
+.. autofunction:: pairwise(iterable: (async) iter T)
+    :for: :(T, T)
+
+    .. versionadded:: 3.10.0
+
 .. py:function:: groupby(iterable: (async) iter T)
     :async-for: :(T, async iter T)
     :noindex:


Test patch:
diff --git a/unittests/test_itertools.py b/unittests/test_itertools.py
index 02d7040..507c76e 100644
--- a/unittests/test_itertools.py
+++ b/unittests/test_itertools.py
@@ -200,6 +200,16 @@ async def test_tee():
             assert await a.list(iterator) == iterable
 
 
+@sync
+async def test_pairwise():
+    assert await a.list(a.pairwise(range(5))) == [(0, 1), (1, 2), (2, 3), (3, 4)]
+    assert await a.list(a.pairwise(range(2))) == [(0, 1)]
+    assert await a.list(a.pairwise(range(1))) == []
+    assert await a.list(a.pairwise(range(0))) == []
+    for count in range(5):
+        assert await a.list(a.pairwise([0] * count)) == [(0, 0)] * (count - 1)
+
+
 @sync
 async def test_zip_longest():
     async for va, vb in a.zip_longest(asyncify(range(5)), range(5)):