Repository: more-itertools/more-itertools (python)

Issue:
New recipe: before_and_after
This issue is to add the new `before_and_after` function from the 3.10 [itertools docs](https://github.com/python/cpython/blame/3.10/Doc/library/itertools.rst#L858).

Needed:
* The implementation from the recipe
* Type stub
* Tests

Pull request:
Add itertools recipes from 3.10 docs
This PR adds the new recipes from the 3.10 docs:
* before_and_after
* triplewise
* sliding_window

Closes #562, #563, and #564 

Patch:
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index b37aad4..387a2a4 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9.0, 3.10.0-rc.2, pypy3]
+        python-version: [3.6, 3.7, 3.8, 3.9.0, 3.10.0, pypy3]
 
     steps:
     - uses: actions/checkout@v2
diff --git a/docs/api.rst b/docs/api.rst
index 827c5e3..109b1da 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -70,6 +70,8 @@ These tools yield windows of items from an iterable.
 **Itertools recipes**
 
 .. autofunction:: pairwise
+.. autofunction:: triplewise
+.. autofunction:: sliding_window
 
 
 Augmenting
@@ -188,6 +190,7 @@ These tools yield certain items from an iterable.
 **Itertools recipes**
 
 .. autofunction:: nth
+.. autofunction:: before_and_after
 .. autofunction:: take
 .. autofunction:: tail
 .. autofunction:: unique_everseen
diff --git a/more_itertools/recipes.py b/more_itertools/recipes.py
index e470f3b..a259642 100644
--- a/more_itertools/recipes.py
+++ b/more_itertools/recipes.py
@@ -26,6 +26,7 @@ from random import randrange, sample, choice
 
 __all__ = [
     'all_equal',
+    'before_and_after',
     'consume',
     'convolve',
     'dotproduct',
@@ -49,9 +50,11 @@ __all__ = [
     'random_product',
     'repeatfunc',
     'roundrobin',
+    'sliding_window',
     'tabulate',
     'tail',
     'take',
+    'triplewise',
     'unique_everseen',
     'unique_justseen',
 ]
@@ -628,3 +631,68 @@ def convolve(signal, kernel):
     for x in chain(signal, repeat(0, n - 1)):
         window.append(x)
         yield sum(map(operator.mul, kernel, window))
+
+
+def before_and_after(predicate, it):
+    """A variant of :func:`takewhile` that allows complete access to the
+    remainder of the iterator.
+
+         >>> it = iter('ABCdEfGhI')
+         >>> all_upper, remainder = before_and_after(str.isupper, it)
+         >>> ''.join(all_upper)
+         'ABC'
+         >>> ''.join(remainder) # takewhile() would lose the 'd'
+         'dEfGhI'
+
+    Note that the first iterator must be fully consumed before the second
+    iterator can generate valid results.
+    """
+    it = iter(it)
+    transition = []
+
+    def true_iterator():
+        for elem in it:
+            if predicate(elem):
+                yield elem
+            else:
+                transition.append(elem)
+                return
+
+    def remainder_iterator():
+        yield from transition
+        yield from it
+
+    return true_iterator(), remainder_iterator()
+
+
+def triplewise(iterable):
+    """Return overlapping triplets from *iterable*.
+
+    >>> list(triplewise('ABCDE'))
+    [('A', 'B', 'C'), ('B', 'C', 'D'), ('C', 'D', 'E')]
+
+    """
+    for (a, _), (b, c) in pairwise(pairwise(iterable)):
+        yield a, b, c
+
+
+def sliding_window(iterable, n):
+    """Return a sliding window of width *n* over *iterable*.
+
+        >>> list(sliding_window(range(6), 4))
+        [(0, 1, 2, 3), (1, 2, 3, 4), (2, 3, 4, 5)]
+
+    If *iterable* has fewer than *n* items, then nothing is yielded:
+
+        >>> list(sliding_window(range(3), 4))
+        []
+
+    For a variant with more features, see :func:`windowed`.
+    """
+    it = iter(iterable)
+    window = deque(islice(it, n), maxlen=n)
+    if len(window) == n:
+        yield tuple(window)
+    for x in it:
+        window.append(x)
+        yield tuple(window)
diff --git a/more_itertools/recipes.pyi b/more_itertools/recipes.pyi
index 69ff32d..4648a41 100644
--- a/more_itertools/recipes.pyi
+++ b/more_itertools/recipes.pyi
@@ -103,3 +103,10 @@ def nth_combination(
 ) -> Tuple[_T, ...]: ...
 def prepend(value: _T, iterator: Iterable[_U]) -> Iterator[Union[_T, _U]]: ...
 def convolve(signal: Iterable[_T], kernel: Iterable[_T]) -> Iterator[_T]: ...
+def before_and_after(
+    predicate: Callable[[_T], bool], it: Iterable[_T]
+) -> Tuple[Iterator[_T], Iterator[_T]]: ...
+def triplewise(iterable: Iterable[_T]) -> Iterator[Tuple[_T, _T, _T]]: ...
+def sliding_window(
+    iterable: Iterable[_T], n: int
+) -> Iterator[Tuple[_T, ...]]: ...


Test patch:
diff --git a/tests/test_recipes.py b/tests/test_recipes.py
index b6ee67a..be40995 100644
--- a/tests/test_recipes.py
+++ b/tests/test_recipes.py
@@ -709,3 +709,57 @@ class Convolvetests(TestCase):
         actual = mi.take(5, mi.convolve(signal, kernel))
         expected = [0, 1, 1, 1, 1]
         self.assertEqual(actual, expected)
+
+
+class BeforeAndAfterTests(TestCase):
+    def test_empty(self):
+        before, after = mi.before_and_after(bool, [])
+        self.assertEqual(list(before), [])
+        self.assertEqual(list(after), [])
+
+    def test_never_true(self):
+        before, after = mi.before_and_after(bool, [0, False, None, ''])
+        self.assertEqual(list(before), [])
+        self.assertEqual(list(after), [0, False, None, ''])
+
+    def test_never_false(self):
+        before, after = mi.before_and_after(bool, [1, True, Ellipsis, ' '])
+        self.assertEqual(list(before), [1, True, Ellipsis, ' '])
+        self.assertEqual(list(after), [])
+
+    def test_some_true(self):
+        before, after = mi.before_and_after(bool, [1, True, 0, False])
+        self.assertEqual(list(before), [1, True])
+        self.assertEqual(list(after), [0, False])
+
+
+class TriplewiseTests(TestCase):
+    def test_basic(self):
+        for iterable, expected in [
+            ([0], []),
+            ([0, 1], []),
+            ([0, 1, 2], [(0, 1, 2)]),
+            ([0, 1, 2, 3], [(0, 1, 2), (1, 2, 3)]),
+            ([0, 1, 2, 3, 4], [(0, 1, 2), (1, 2, 3), (2, 3, 4)]),
+        ]:
+            with self.subTest(expected=expected):
+                actual = list(mi.triplewise(iterable))
+                self.assertEqual(actual, expected)
+
+
+class SlidingWindowTests(TestCase):
+    def test_basic(self):
+        for iterable, n, expected in [
+            ([], 0, [()]),
+            ([], 1, []),
+            ([0], 1, [(0,)]),
+            ([0, 1], 1, [(0,), (1,)]),
+            ([0, 1, 2], 2, [(0, 1), (1, 2)]),
+            ([0, 1, 2], 3, [(0, 1, 2)]),
+            ([0, 1, 2], 4, []),
+            ([0, 1, 2, 3], 4, [(0, 1, 2, 3)]),
+            ([0, 1, 2, 3, 4], 4, [(0, 1, 2, 3), (1, 2, 3, 4)]),
+        ]:
+            with self.subTest(expected=expected):
+                actual = list(mi.sliding_window(iterable, n))
+                self.assertEqual(actual, expected)