diff --git a/README.md b/README.md index b36d296..821622d 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Most `Itr` methods are **lazy transformations**, meaning they return a new `Itr` However, some methods are **eager consumers**. These methods iterate over and consume the underlying data, returning concrete values, collections, or aggregates. Examples include: * **Collection methods:** `collect`, `last`, `next`, `next_chunk`, `nth`, `position` -* **Aggregation methods:** `count`, `reduce`, `max`, `min`, `all`, `any`, `find`, `fold` +* **Aggregation methods:** `count`, `reduce`, `max`, `min`, `all`, `any`, `consume`, `find`, `fold` ### Important Considerations diff --git a/doc/apidoc.md b/doc/apidoc.md index fff51bd..08c9651 100644 --- a/doc/apidoc.md +++ b/doc/apidoc.md @@ -1,4 +1,4 @@ -# `Itr` v0.1.5 class documentation +# `Itr` v0.1.7 class documentation A generic iterator adaptor class inspired by Rust's Iterator trait, providing a composable API for functional-style iteration and transformation over Python iterables. ## Public methods @@ -23,7 +23,8 @@ Implement the next method of the Iterator protocol ### `accumulate` -Return an iterator over the accumulated results of applying the function (or sum by default) to the items. +Return an iterator over the accumulated results of applying the function (or sum by default) to the items. Does +not collapse the iterator like `reduce` or `fold` Args: func (Callable[[T, T], T] | None): A binary function to accumulate results. Defaults to addition. @@ -104,6 +105,16 @@ Returns: +### `consume` + +Exhaust the iterator. Useful when only the side effects are required (see inspect) + +Do not use on an open-ended iterator + +Returns: + None + + ### `copy` Splits the iterator at its *current state* into two independent iterators. @@ -238,11 +249,11 @@ Returns: Itr[T]: An iterator yielding the original items after applying the function. Example: - >>> Itr([1, 2, 3]).inspect(print).collect() + >>> Itr([1, 2, 3]).inspect(print).consume() 1 2 3 - (1, 2, 3) + >>> ### `interleave` diff --git a/doc/examples.ipynb b/doc/examples.ipynb index 6e513dc..0532352 100644 --- a/doc/examples.ipynb +++ b/doc/examples.ipynb @@ -190,7 +190,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "fd47677f", "metadata": {}, "outputs": [ @@ -250,7 +250,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "8bcc535f", "metadata": {}, "outputs": [ @@ -281,12 +281,12 @@ "id": "3c56d881", "metadata": {}, "source": [ - "Using `inspect` allows intermediate steps to be logged" + "Using `inspect` allows intermediate steps to be logged. As there's a side-effect you might even want to discard the output, using `consume`:" ] }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 8, "id": "4c33c79c", "metadata": {}, "outputs": [ @@ -302,20 +302,10 @@ "22\n", "29\n" ] - }, - { - "data": { - "text/plain": [ - "(484,)" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "Itr(collatz(19)).rev().step_by(3).inspect(print).skip(5).map(lambda x: x * x).filter(lambda x: x % 2 == 0).collect()" + "Itr(collatz(19)).rev().step_by(3).inspect(print).skip(5).map(lambda x: x * x).filter(lambda x: x % 2 == 0).consume()" ] }, { @@ -334,7 +324,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 9, "id": "d9b906a6", "metadata": {}, "outputs": [ @@ -344,7 +334,7 @@ "(5, 16, 8, 4, 2, 1, 6, 3, 10, 5, 16, 8, 4, 2, 1)" ] }, - "execution_count": 38, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -363,7 +353,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 10, "id": "377cd2c0", "metadata": {}, "outputs": [ @@ -373,7 +363,7 @@ "((5, 6), (16, 3), (8, 10), (4, 5), (2, 16), (1, 8))" ] }, - "execution_count": 39, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -393,7 +383,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 11, "id": "03e8e659", "metadata": {}, "outputs": [ @@ -403,7 +393,7 @@ "((5, 16, 8, 4, 2, 1), (6, 3, 10, 5, 16, 8))" ] }, - "execution_count": 40, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -423,7 +413,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 12, "id": "c3b77ea4", "metadata": {}, "outputs": [ @@ -433,7 +423,7 @@ "(8, 'abcde', 4, 'abcde', 2, 'abcde', 1)" ] }, - "execution_count": 41, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -452,7 +442,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 13, "id": "ed21cfb9", "metadata": {}, "outputs": [ @@ -462,7 +452,7 @@ "(5, 1, 16, 2, 8, 4, 4, 8, 2, 16, 1, 5)" ] }, - "execution_count": 42, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -483,7 +473,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 14, "id": "53be0e6a", "metadata": {}, "outputs": [ @@ -493,7 +483,7 @@ "((1, 5), (2, 16), (3, 8), (4, 4), (5, 2), (6, 1))" ] }, - "execution_count": 43, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -576,7 +566,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 17, "id": "6c9cb5c1", "metadata": {}, "outputs": [ @@ -586,7 +576,7 @@ "(5, 17, 9, 6, 5, 6)" ] }, - "execution_count": 44, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -614,7 +604,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 18, "id": "9d9bed75", "metadata": {}, "outputs": [ @@ -653,7 +643,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 19, "id": "ad83ad71", "metadata": {}, "outputs": [ @@ -665,7 +655,7 @@ " ())" ] }, - "execution_count": 46, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -687,7 +677,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 20, "id": "b52225da", "metadata": {}, "outputs": [ @@ -697,7 +687,7 @@ "(10, 5)" ] }, - "execution_count": 47, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -719,7 +709,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 21, "id": "7fcc7a00", "metadata": {}, "outputs": [ @@ -732,7 +722,7 @@ " (4, 2, 1))" ] }, - "execution_count": 48, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -752,7 +742,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 22, "id": "6f2d5115", "metadata": {}, "outputs": [ @@ -762,7 +752,7 @@ "((10, 5, 16), (5, 16, 8), (16, 8, 4), (8, 4, 2), (4, 2, 1))" ] }, - "execution_count": 49, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -781,7 +771,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 23, "id": "cce14801", "metadata": {}, "outputs": [ @@ -792,7 +782,7 @@ " 2: (29, 44, 11, 17, 26, 20, 5, 8, 2)}" ] }, - "execution_count": 50, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -817,7 +807,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 24, "id": "2ba56157", "metadata": {}, "outputs": [ @@ -827,7 +817,7 @@ "(46, 46)" ] }, - "execution_count": 51, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -856,7 +846,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 25, "id": "fab7b5c0", "metadata": {}, "outputs": [ @@ -902,7 +892,7 @@ "8" ] }, - "execution_count": 52, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -933,7 +923,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 26, "id": "2ac1c105", "metadata": {}, "outputs": [ @@ -952,7 +942,7 @@ "(19, 58, 29)" ] }, - "execution_count": 53, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -972,7 +962,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 27, "id": "3be2d7d0", "metadata": {}, "outputs": [ @@ -982,7 +972,7 @@ "(5, 16, 8, 4, 2, 1, 5, 16, 8, 4, 2, 1)" ] }, - "execution_count": 54, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -1004,7 +994,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 28, "id": "dac3efaa", "metadata": {}, "outputs": [ @@ -1014,7 +1004,7 @@ "(5, 16, 8, 4, 2, 1, 5, 16, 8, 4, 2, 1, 5, 16, 8, 4, 2, 1)" ] }, - "execution_count": 55, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -1033,7 +1023,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 29, "id": "1be027d4", "metadata": {}, "outputs": [ @@ -1054,7 +1044,7 @@ " (1, 1))" ] }, - "execution_count": 56, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -1062,14 +1052,6 @@ "source": [ "Itr(collatz(5)).product(collatz(2)).collect()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0c09d623", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -1088,7 +1070,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.2" + "version": "3.14.0" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml index 526b96a..b319e5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,18 @@ [project] name = "itrx" -version = "0.1.6" +version = "0.1.7" description = "A chainable iterator adapter" readme = "README.md" authors = [ { name = "virgesmith", email = "andrew@friarswood.net" } ] +license = "MIT" license-files = ["LICENCE.md"] requires-python = ">=3.12" classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] dependencies = [] diff --git a/src/itrx/itr.py b/src/itrx/itr.py index 8232eae..4d0fa1b 100644 --- a/src/itrx/itr.py +++ b/src/itrx/itr.py @@ -1,4 +1,5 @@ import itertools +from collections import deque from collections.abc import Callable, Generator, Iterable, Iterator from typing import TypeVar, overload @@ -126,6 +127,16 @@ def collect(self, container: type[_CollectT] = tuple) -> _CollectT: # type: ign """ return container(self._it) # type: ignore[call-arg] + def consume(self) -> None: + """Exhaust the iterator. Useful when only the side effects are required (see inspect) + + Do not use on an open-ended iterator + + Returns: + None + """ + deque(self._it, 0) + def copy(self) -> "Itr[T]": """Splits the iterator at its *current state* into two independent iterators. @@ -265,11 +276,11 @@ def inspect(self, func: Callable[[T], None]) -> "Itr[T]": Itr[T]: An iterator yielding the original items after applying the function. Example: - >>> Itr([1, 2, 3]).inspect(print).collect() + >>> Itr([1, 2, 3]).inspect(print).consume() 1 2 3 - (1, 2, 3) + >>> """ def impl(x: T) -> T: diff --git a/src/test/test_combine_split.py b/src/test/test_combine_split.py index 33fa155..203caeb 100644 --- a/src/test/test_combine_split.py +++ b/src/test/test_combine_split.py @@ -279,8 +279,7 @@ def log(x: int) -> None: nonlocal total total += x - a = Itr(range(10)).inspect(log).collect() - assert a == tuple(range(10)) # output is unchanged + Itr(range(10)).inspect(log).consume() assert total == 45 diff --git a/src/test/test_general.py b/src/test/test_general.py index 5deb072..92d6f8b 100644 --- a/src/test/test_general.py +++ b/src/test/test_general.py @@ -1,3 +1,5 @@ +from collections.abc import Iterator + import pytest from itrx import Itr @@ -27,3 +29,35 @@ def test_itr_iter_and_next_independent() -> None: assert it.__next__() == 20 with pytest.raises(StopIteration): it.__next__() + + +def test_consume_triggers_side_effects_and_consumes_all() -> None: + side = [] + + def gen() -> Iterator[int]: + for v in (10, 20, 30): + side.append(v) + yield v + + itr = Itr(gen()) + assert side == [] + result = itr.consume() # type: ignore[func-returns-value] + assert result is None + assert side == [10, 20, 30] + # iterator should now be exhausted + assert itr.collect() == () + + +def test_consume_consumes_remaining_only() -> None: + itr = Itr(iter([1, 2, 3, 4])) + first = itr.next() + assert first == 1 + itr.consume() + # remaining items were consumed, collect yields empty tuple + assert itr.collect() == () + + +def test_consume_on_empty_iterator_no_error() -> None: + itr: Itr[None] = Itr(()) + itr.consume() # should not raise + assert itr.collect() == ()