From 1d6bd20c6e083acc515a0ed0a9ce97c35dfd059e Mon Sep 17 00:00:00 2001 From: KB Sriram Date: Fri, 23 Feb 2024 11:25:38 -0800 Subject: [PATCH 1/3] Update `islice` to align behavior closer to CP - added checks during iteration to handle "short" iterators - added tests to run some basic comparisons between CP and this library for `islice`. Fixes https://github.com/adafruit/Adafruit_CircuitPython_IterTools/issues/22 --- adafruit_itertools/__init__.py | 25 ++++++++++-- tests/README.rst | 24 +++++++++++ tests/test_itertools.py | 75 ++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 tests/README.rst create mode 100644 tests/test_itertools.py diff --git a/adafruit_itertools/__init__.py b/adafruit_itertools/__init__.py index b3ec117..fdb3743 100644 --- a/adafruit_itertools/__init__.py +++ b/adafruit_itertools/__init__.py @@ -342,17 +342,34 @@ def islice(p, start, stop=(), step=1): if stop == (): stop = start start = 0 + if stop is not None and stop < 0: + raise ValueError("Stop for islice must be None or a non-negative integer") + if start < 0: + raise ValueError("Start for islice must be a non-negative integer") + if step <= 0: + raise ValueError("Step for islice must be a positive integer") + # TODO: optimizing or breaking semantics? if stop is not None and start >= stop: return it = iter(p) for _ in range(start): - next(it) + try: + next(it) + except StopIteration: + return while True: - yield next(it) + try: + val = next(it) + except StopIteration: + return + yield val for _ in range(step - 1): - next(it) + try: + next(it) + except StopIteration: + return start += step if stop is not None and start >= stop: return @@ -459,7 +476,7 @@ def starmap( the iterable. Used instead of map() when argument parameters are already grouped in tuples from a single iterable (the data has been “pre-zipped”). The difference between map() and starmap() parallels the distinction between - function(a,b) and function(\*c). + function(a,b) and function(*c). :param function: the function to apply :param iterable: where groups of arguments come from diff --git a/tests/README.rst b/tests/README.rst new file mode 100644 index 0000000..ab25406 --- /dev/null +++ b/tests/README.rst @@ -0,0 +1,24 @@ +.. + SPDX-FileCopyrightText: KB Sriram + SPDX-License-Identifier: MIT +.. + +Itertools Tests +=============== + +These tests run under CPython, and are intended to verify that the +Adafruit library functions return the same outputs compared to ones in +the standard `itertools` module. + +These tests run automatically from the standard `circuitpython github +workflow `_. To run them manually, first install these packages +if necessary:: + + $ pip3 install pytest + +Then ensure you're in the *root* directory of the repository and run +the following command:: + + $ python -m pytest + +.. _wf: https://github.com/adafruit/workflows-circuitpython-libs/blob/6e1562eaabced4db1bd91173b698b1cc1dfd35ab/build/action.yml#L78-L84 diff --git a/tests/test_itertools.py b/tests/test_itertools.py new file mode 100644 index 0000000..9a52b5b --- /dev/null +++ b/tests/test_itertools.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: KB Sriram +# SPDX-License-Identifier: MIT + +from typing import Iterator, Optional, Sequence, TypeVar +import itertools as it +import pytest +import adafruit_itertools as ait + +_T = TypeVar("_T") + + +@pytest.mark.parametrize( + "seq, start", + [ + ("", 0), + ("", 2), + ("ABCDEFG", 0), + ("ABCDEFG", 2), + ("ABCDEFG", 20), + ], +) +def test_islice_start(seq: Sequence[_T], start: int) -> None: + x: Iterator[_T] = ait.islice(seq, start) + y: Iterator[_T] = it.islice(seq, start) + assert list(x) == list(y) + + +@pytest.mark.parametrize( + "seq, start, stop", + [ + ("", 0, 5), + ("", 2, 5), + ("", 0, 0), + ("ABCDEFG", 2, 2), + ("ABCDEFG", 2, 6), + ("ABCDEFG", 2, None), + ("ABCDEFG", 2, 17), + ("ABCDEFG", 20, 30), + ], +) +def test_islice_start_stop(seq: Sequence[_T], start: int, stop: Optional[int]) -> None: + x: Iterator[_T] = ait.islice(seq, start, stop) + y: Iterator[_T] = it.islice(seq, start, stop) + assert list(x) == list(y) + + +@pytest.mark.parametrize( + "seq, start, stop, step", + [ + ("", 0, 5, 3), + ("", 2, 5, 2), + ("", 0, 0, 1), + ("ABCDEFG", 2, 2, 2), + ("ABCDEFG", 2, 6, 3), + ("ABCDEFG", 2, 17, 2), + ("ABCDEFG", 0, None, 2), + ("ABCDEFG", 20, 30, 3), + ("ABCDEFG", 0, None, 3), + ], +) +def test_islice_start_stop_step( + seq: Sequence[_T], start: int, stop: Optional[int], step: int +) -> None: + x: Iterator[_T] = ait.islice(seq, start, stop, step) + y: Iterator[_T] = it.islice(seq, start, stop, step) + assert list(x) == list(y) + + +def test_islice_error() -> None: + with pytest.raises(ValueError): + list(ait.islice("abc", -1)) + with pytest.raises(ValueError): + list(ait.islice("abc", 0, -1)) + with pytest.raises(ValueError): + list(ait.islice("abc", 0, 0, 0)) From b61916abcca976ce06b6019158229077007818c2 Mon Sep 17 00:00:00 2001 From: KB Sriram Date: Fri, 23 Feb 2024 11:42:09 -0800 Subject: [PATCH 2/3] I thought pylint complained about an undesired backslash before. --- adafruit_itertools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_itertools/__init__.py b/adafruit_itertools/__init__.py index fdb3743..faba805 100644 --- a/adafruit_itertools/__init__.py +++ b/adafruit_itertools/__init__.py @@ -476,7 +476,7 @@ def starmap( the iterable. Used instead of map() when argument parameters are already grouped in tuples from a single iterable (the data has been “pre-zipped”). The difference between map() and starmap() parallels the distinction between - function(a,b) and function(*c). + function(a,b) and function(\*c). :param function: the function to apply :param iterable: where groups of arguments come from From 0d95ebcf47e4056e4ddf41b889568af04f31358b Mon Sep 17 00:00:00 2001 From: KB Sriram Date: Fri, 23 Feb 2024 12:33:51 -0800 Subject: [PATCH 3/3] Shorten ValueError messages --- adafruit_itertools/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adafruit_itertools/__init__.py b/adafruit_itertools/__init__.py index faba805..6617ba4 100644 --- a/adafruit_itertools/__init__.py +++ b/adafruit_itertools/__init__.py @@ -343,11 +343,11 @@ def islice(p, start, stop=(), step=1): stop = start start = 0 if stop is not None and stop < 0: - raise ValueError("Stop for islice must be None or a non-negative integer") + raise ValueError("stop must be None or >= 0") if start < 0: - raise ValueError("Start for islice must be a non-negative integer") + raise ValueError("start must be >= 0") if step <= 0: - raise ValueError("Step for islice must be a positive integer") + raise ValueError("step must be > 0") # TODO: optimizing or breaking semantics? if stop is not None and start >= stop: