Skip to content

Commit

Permalink
Add flow.Reverse iterator. Fix a bug in ISlice (strangely not found b…
Browse files Browse the repository at this point in the history
…y Hypothesis yesterday). Improve docs.
  • Loading branch information
ynikitenko committed Apr 26, 2021
1 parent f80efbb commit ed5d74d
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 16 deletions.
2 changes: 1 addition & 1 deletion lena/flow/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __init__(self, filename, method="cPickle", protocol=2):
You can give it *.pkl* extension.
*method* can be *pickle* or *cPickle* (faster pickle).
For Python3 they are same.
For Python 3 they are same.
*protocol* is pickle protocol.
Version 2 is the highest supported by Python 2.
Expand Down
55 changes: 43 additions & 12 deletions lena/flow/iterators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Adapters to iterators from ``itertools``."""
"""Iterators allow to transform a data flow
or create a new one.
"""
try:
from future_builtins import zip
except ModuleNotFoundError:
Expand Down Expand Up @@ -84,6 +86,12 @@ def __init__(self, *args):
For example, to discard the last 200 elements
one has to a) read the whole flow, b) store 200 elements
during each iteration.
It is not possible to use negative indices with
:meth:`fill_into`, because it doesn't control the flow
and doesn't know when it is finished.
To obtain a negative step,
use a composition with :class:`Reverse`.
"""
# todo: rename to Slice in the next release.
from itertools import islice
Expand All @@ -101,9 +109,9 @@ def __init__(self, *args):
# negative indices
s = slice(*args)
self._start, self._stop, step = s.start, s.stop, s.step
# if step is None, it is 1 by default.
step = step or 1
if step <= 0:
if step is None:
step = 1
if step <= 0 or int(step) != step:
raise lena.core.LenaValueError(
"step must be a natural number (integer >= 1)"
)
Expand All @@ -118,7 +126,8 @@ def __init__(self, *args):
def fill_into(self, element, value):
"""Fill *element* with *value*.
Element must have a ``fill(value)`` method.
Values are filled in the order defined by *(start, stop, step)*.
*Element* must have a ``fill(value)`` method.
"""
if self._index > self._next_index:
try:
Expand Down Expand Up @@ -155,10 +164,7 @@ def fill_deque(flow, maxlen):
# skip *start* values
for _ in zip(range(start), flow):
pass
if stop is None:
for val in flow:
yield val
return
# stop=None is handled in islice
# stop is negative
d = fill_deque(flow, -stop)
if len(d) < -stop:
Expand All @@ -179,14 +185,15 @@ def fill_deque(flow, maxlen):
if stop <= start:
return
if stop < 0:
# will exhaust all flow and fill the deque
# exhaust all flow and fill the deque
# with last maxlen elements
d = deque(flow, maxlen=-start)
ind = 0
# imitate
# for val in d[:stop-start]:
# which is not possible with a deque.
while ind < len(d) + stop:
len_d = len(d)
while ind < len_d + stop:
yield d.popleft()
ind += 1
else:
Expand All @@ -213,5 +220,29 @@ def fill_deque(flow, maxlen):


def run(self, flow):
"""Yield values from *start* to *stop* with *step*."""
"""Yield values from *flow* from *start* to *stop* with *step*.
"""
return self._islice(flow)


class Reverse():
"""Reverse the flow (yield values from last to first).
Warning
-------
This element will consume the whole flow.
"""

def __init__(self):
# no ideas yet. Maybe allow maxsize?
# However, that is not implemented in list.__init__ .
pass

def run(self, flow):
"""Consume the *flow* and yield values in reverse order."""
all_huge_flow = list(flow)
while 1:
try:
yield all_huge_flow.pop()
except IndexError:
return
29 changes: 26 additions & 3 deletions tests/flow/test_iterators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
import lena.flow
from lena.core import Source, LenaStopFill
from lena.flow import DropContext, CountFrom
from lena.flow.iterators import ISlice
from lena.flow.iterators import ISlice, Reverse
from tests.examples.fill import StoreFilled

from hypothesis import strategies as s
from hypothesis import given
# don't think anything would change with other numbers
hypo_int_max = 200
# all bugs converged to at most 3.
hypo_int_max = 20


def test_chain():
Expand Down Expand Up @@ -139,6 +139,19 @@ def test_negative_islice():
# zero step raises
with pytest.raises(lena.core.LenaValueError):
ISlice(None, None, 0)
# it but raises
with pytest.raises(lena.core.LenaValueError):
ISlice(-1, -1, 0)
# it should raise also for non-integer numbers.
with pytest.raises(lena.core.LenaValueError):
ISlice(-1, -1, 1.5)

# found by hypothesis, but yesterday it was not found...
# Pytest for the whole package doesn't show any error!
# Is hypothesis reliable?..
isl = ISlice(-3, -1, 1)
data = list(range(3))
assert list(isl.run(iter(data))) == [0, 1]


start_stop_s = s.one_of(s.none(), s.integers(-hypo_int_max, hypo_int_max))
Expand All @@ -150,3 +163,13 @@ def test_islice_hypothesis(start, stop, step, data_len):
data = list(range(data_len))
isl = ISlice(start, stop, step)
assert list(isl.run(iter(data))) == data[start:stop:step]


def test_reverse():
r = Reverse()
assert list(r.run(iter([]))) == []
assert list(r.run(iter([1]))) == [1]
# it really works!
assert list(r.run(iter([1, 2]))) == [2, 1]
# just in case
assert list(r.run(iter([1, 2, 3]))) == [3, 2, 1]

0 comments on commit ed5d74d

Please sign in to comment.