Skip to content

Conversation

rhettinger
Copy link
Contributor

@rhettinger rhettinger commented May 3, 2024

Fixes the corner case errors reported by F H T Mitchell, "This does not correctly handle stop == 0, overwriting it to be sys.maxsize and also assumes its input is an iterator, not any iterable."

The elegant and readable code rough equivalent was crafted by Stefan Pochmann.

This mostly passes all the tests for the C version of islice() except:

  • Negative indices fail to raise ValueError and wrong answers are given.
  • A zero stop argument fails to raise and a wrong answer is returned.
  • Invalid input types raise a different exception.

This is deemed good enough for a rough equivalent that aims to convey how the stream of indices is created when correct inputs are supplied.


Here are the tests I applied to the rough equivalent (taken from Lib/test/test_itertools.py and from the bug report):

from itertools import count
import unittest
import weakref
from test import support
from functools import total_ordering

maxsize = support.MAX_Py_ssize_t

def islice(iterable, *args):
    s = slice(*args)
    start, stop, step = s.start or 0, s.stop, s.step or 1
    indices = count() if stop is None else range(max(stop, start))
    nexti = start
    for i, element in zip(indices, iterable):
        if i == nexti:
            yield element
            nexti += step

class TestItertools(unittest.TestCase):

    def test_islice(self):
        for args in [          # islice(args) should agree with range(args)
                (10, 20, 3),
                (10, 3, 20),
                (10, 20),
                (10, 10),
                (10, 3),
                (20,)
                ]:
            self.assertEqual(list(islice(range(100), *args)),
                             list(range(*args)))

        for args, tgtargs in [  # Stop when seqn is exhausted
                ((10, 110, 3), ((10, 100, 3))),
                ((10, 110), ((10, 100))),
                ((110,), (100,))
                ]:
            self.assertEqual(list(islice(range(100), *args)),
                             list(range(*tgtargs)))

        # Test stop=None
        self.assertEqual(list(islice(range(10), None)), list(range(10)))
        self.assertEqual(list(islice(range(10), None, None)), list(range(10)))
        self.assertEqual(list(islice(range(10), None, None, None)), list(range(10)))
        self.assertEqual(list(islice(range(10), 2, None)), list(range(2, 10)))
        self.assertEqual(list(islice(range(10), 1, None, 2)), list(range(1, 10, 2)))

        # Test number of items consumed     SF #1171417
        it = iter(range(10))
        self.assertEqual(list(islice(it, 3)), list(range(3)))
        self.assertEqual(list(it), list(range(3, 10)))

        it = iter(range(10))
        self.assertEqual(list(islice(it, 3, 3)), [])
        self.assertEqual(list(it), list(range(3, 10)))

        # Test invalid arguments
        ra = range(10)
        with self.assertRaises(TypeError):
            list(islice(ra))
        with self.assertRaises(TypeError):
            list(islice(ra, 1, 2, 3, 4))
        # with self.assertRaises(ValueError):
        #    list(islice(ra, -5, 10, 1))
        # with self.assertRaises(ValueError):
        #    list(islice(ra, 1, -5, -1))
        # with self.assertRaises(ValueError):
        #    list(islice(ra, 1, 10, -1))
        # with self.assertRaises(ValueError):
        #    list(islice(ra, 1, 10, 0))
        with self.assertRaises((ValueError, TypeError)):
            list(islice(ra, 'a'))
        with self.assertRaises((ValueError, TypeError)):
            list(islice(ra, 'a', 1))
        with self.assertRaises((ValueError, TypeError)):
            list(islice(ra, 1, 'a'))
        with self.assertRaises((ValueError, TypeError)):
            list(islice(ra, 'a', 1, 1))
        with self.assertRaises((ValueError, TypeError)):
            list(islice(ra, 1, 'a', 1))
        self.assertEqual(len(list(islice(count(), 1, 10, maxsize))), 1)

        # Issue #10323:  Less islice in a predictable state
        c = count()
        self.assertEqual(list(islice(c, 1, 3, 50)), [1])
        self.assertEqual(next(c), 3)

        # Issue #21321: check source iterator is not referenced
        # from islice() after the latter has been exhausted
        it = (x for x in (1, 2))
        wr = weakref.ref(it)
        it = islice(it, 1)
        self.assertIsNotNone(wr())
        list(it) # exhaust the iterator
        support.gc_collect()
        self.assertIsNone(wr())

        # Issue #30537: islice can accept integer-like objects as
        # arguments
        @total_ordering
        class IntLike(object):
            def __init__(self, val):
                self.val = val
            def __index__(self):
                return self.val
            def __eq__(self, other):
                return int(self) == int(other)
            def __lt__(self, other):
                return int(self) < int(other)
            def __add__(self, other):
                return int(self) + int(other)
            __radd__ = __add__

        self.assertEqual(list(islice(range(100), IntLike(10))), list(range(10)))
        self.assertEqual(list(islice(range(100), IntLike(10), IntLike(50))),
                         list(range(10, 50)))
        self.assertEqual(list(islice(range(100), IntLike(10), IntLike(50), IntLike(5))),
                         list(range(10,50,5)))


if __name__ == '__main__':

    import itertools

    for start in itertools.chain(range(6), [None]):
        for stop in itertools.chain(range(6), [None]):
            for step in itertools.chain(range(1, 6), [None]):
                for r in [range(0), range(4)]:
                    it = iter(r)
                    myx = list(islice(it, start, stop, step))
                    myy = list(it)
                    it = iter(r)
                    pyx = list(itertools.islice(it, start, stop, step))
                    pyy = list(it)
                    msg = "ERR" if myx != pyx or myy != pyy else "ok"
                    if msg != 'ok':
                        print(f"{msg}: {r=}, {start=}, {stop=}, {step=}, {myx=}, {pyx=}, {myy=}, {pyy=}")
                    assert msg == 'ok'
    else:
        print('Done')

    unittest.main()


📚 Documentation preview 📚: https://cpython-previews--118559.org.readthedocs.build/

@rhettinger rhettinger added type-bug An unexpected behavior, bug, or error docs Documentation in the Doc dir needs backport to 3.11 only security fixes needs backport to 3.12 only security fixes labels May 3, 2024
@rhettinger rhettinger self-assigned this May 3, 2024
@rhettinger
Copy link
Contributor Author

Now passes these tests which were commented out above:

        with self.assertRaises(ValueError):
            list(islice(ra, -5, 10, 1))
        with self.assertRaises(ValueError):
            list(islice(ra, 1, -5, -1))
        with self.assertRaises(ValueError):
            list(islice(ra, 1, 10, -1))
        with self.assertRaises(ValueError):
            list(islice(ra, 1, 10, 0))

@rhettinger rhettinger merged commit c7c9b91 into python:main May 5, 2024
@miss-islington-app
Copy link

Thanks @rhettinger for the PR 🌮🎉.. I'm working now to backport this PR to: 3.11, 3.12.
🐍🍒⛏🤖

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request May 5, 2024
…onGh-118559)

(cherry picked from commit c7c9b91)

Co-authored-by: Raymond Hettinger <rhettinger@users.noreply.github.com>
@rhettinger rhettinger deleted the islice_equivalent branch May 5, 2024 06:42
miss-islington pushed a commit to miss-islington/cpython that referenced this pull request May 5, 2024
…onGh-118559)

(cherry picked from commit c7c9b91)

Co-authored-by: Raymond Hettinger <rhettinger@users.noreply.github.com>
@bedevere-app
Copy link

bedevere-app bot commented May 5, 2024

GH-118587 is a backport of this pull request to the 3.12 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.12 only security fixes label May 5, 2024
@bedevere-app
Copy link

bedevere-app bot commented May 5, 2024

GH-118588 is a backport of this pull request to the 3.11 branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Documentation in the Doc dir skip news type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant