diff --git a/DOCS.md b/DOCS.md index 3cfec2e06..67ce130c2 100644 --- a/DOCS.md +++ b/DOCS.md @@ -3022,6 +3022,45 @@ count()$[10**100] |> print **Python:** _Can't be done quickly without Coconut's iterator slicing, which requires many complicated pieces. The necessary definitions in Python can be found in the Coconut header._ +### `cycle` + +**cycle**(_iterable_, _times_=`None`) + +Coconut's `cycle` is a modified version of `itertools.cycle` with a `times` parameter that controls the number of times to cycle through _iterable_ before stopping. `cycle` also supports `in`, slicing, `len`, `reversed`, `.count()`, `.index()`, and `repr`. + +##### Python Docs + +**cycle**(_iterable_) + +Make an iterator returning elements from the iterable and saving a copy of each. When the iterable is exhausted, return elements from the saved copy. Repeats indefinitely. Roughly equivalent to: + +```coconut_python +def cycle(iterable): + # cycle('ABCD') --> A B C D A B C D A B C D ... + saved = [] + for element in iterable: + yield element + saved.append(element) + while saved: + for element in saved: + yield element +``` + +Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable). + +##### Example + +**Coconut:** +```coconut +cycle(range(2), 2) |> list |> print +``` + +**Python:** +```coconut_python +from itertools import cycle, islice +print(list(islice(cycle(range(2)), 4))) +``` + ### `makedata` **makedata**(_data\_type_, *_args_) diff --git a/__coconut__/__init__.pyi b/__coconut__/__init__.pyi index caccc2583..89e2532fb 100644 --- a/__coconut__/__init__.pyi +++ b/__coconut__/__init__.pyi @@ -592,13 +592,31 @@ class _count(_t.Iterable[_T]): def __getitem__(self, index: slice) -> _t.Iterable[_T]: ... def __hash__(self) -> int: ... - def count(self, elem: _T) -> int: ... + def count(self, elem: _T) -> int | float: ... def index(self, elem: _T) -> int: ... def __fmap__(self, func: _t.Callable[[_T], _Uco]) -> _count[_Uco]: ... def __copy__(self) -> _count[_T]: ... count = _coconut_count = _count # necessary since we define .count() +class cycle(_t.Iterable[_T]): + def __new__(self, iterable: _t.Iterable[_T], times: _t.Optional[int]=None) -> cycle[_T]: ... + def __iter__(self) -> _t.Iterator[_T]: ... + def __contains__(self, elem: _T) -> bool: ... + + @_t.overload + def __getitem__(self, index: int) -> _T: ... + @_t.overload + def __getitem__(self, index: slice) -> _t.Iterable[_T]: ... + + def __hash__(self) -> int: ... + def count(self, elem: _T) -> int | float: ... + def index(self, elem: _T) -> int: ... + def __fmap__(self, func: _t.Callable[[_T], _Uco]) -> _t.Iterable[_Uco]: ... + def __copy__(self) -> cycle[_T]: ... + def __len__(self) -> int: ... + + class flatten(_t.Iterable[_T]): def __new__(self, iterable: _t.Iterable[_t.Iterable[_T]]) -> flatten[_T]: ... diff --git a/coconut/compiler/compiler.py b/coconut/compiler/compiler.py index c2e0a96b4..750d4373b 100644 --- a/coconut/compiler/compiler.py +++ b/coconut/compiler/compiler.py @@ -3065,10 +3065,11 @@ def op_match_funcdef_handle(self, original, loc, tokens): def set_literal_handle(self, tokens): """Converts set literals to the right form for the target Python.""" internal_assert(len(tokens) == 1 and len(tokens[0]) == 1, "invalid set literal tokens", tokens) - if self.target_info < (2, 7): - return "_coconut.set(" + set_to_tuple(tokens[0]) + ")" + contents, = tokens + if self.target_info < (2, 7) or "testlist_star_expr" in contents: + return "_coconut.set(" + set_to_tuple(contents) + ")" else: - return "{" + tokens[0][0] + "}" + return "{" + contents[0] + "}" def set_letter_literal_handle(self, tokens): """Process set literals with set letters.""" diff --git a/coconut/compiler/templates/header.py_template b/coconut/compiler/templates/header.py_template index 1245ae936..4094fd0ad 100644 --- a/coconut/compiler/templates/header.py_template +++ b/coconut/compiler/templates/header.py_template @@ -146,6 +146,8 @@ class _coconut_has_iter(_coconut_base_hashable): with self.lock: self.iter = _coconut_reiterable(self.iter) return self.iter + def __fmap__(self, func): + return _coconut_map(func, self) class reiterable(_coconut_has_iter): """Allow an iterator to be iterated over multiple times with the same results.""" __slots__ = () @@ -166,8 +168,6 @@ class reiterable(_coconut_has_iter): return (self.__class__, (self.iter,)) def __copy__(self): return self.__class__(self.get_new_iter()) - def __fmap__(self, func): - return _coconut_map(func, self) def __getitem__(self, index): return _coconut_iter_getitem(self.get_new_iter(), index) def __reversed__(self): @@ -432,8 +432,6 @@ class scan(_coconut_has_iter): if not _coconut.isinstance(self.iter, _coconut.abc.Sized): return _coconut.NotImplemented return _coconut.len(self.iter) - def __fmap__(self, func): - return _coconut_map(func, self) class reversed(_coconut_has_iter): __slots__ = () __doc__ = getattr(_coconut.reversed, "__doc__", "") @@ -838,8 +836,6 @@ class multi_enumerate(_coconut_has_iter): __slots__ = () def __repr__(self): return "multi_enumerate(%s)" % (_coconut.repr(self.iter),) - def __fmap__(self, func): - return _coconut_map(func, self) def __reduce__(self): return (self.__class__, (self.iter,)) def __copy__(self): @@ -882,19 +878,22 @@ class multi_enumerate(_coconut_has_iter): return self.iter.size return _coconut.NotImplemented class count(_coconut_base_hashable): - """count(start, step) returns an infinite iterator starting at start and increasing by step. - - If step is set to 0, count will infinitely repeat its first argument. - """ __slots__ = ("start", "step") + __doc__ = getattr(_coconut.itertools.count, "__doc__", "count(start, step) returns an infinite iterator starting at start and increasing by step.") def __init__(self, start=0, step=1): self.start = start self.step = step + def __reduce__(self): + return (self.__class__, (self.start, self.step)) + def __repr__(self): + return "count(%s, %s)" % (_coconut.repr(self.start), _coconut.repr(self.step)) def __iter__(self): while True: yield self.start if self.step: self.start += self.step + def __fmap__(self, func): + return _coconut_map(func, self) def __contains__(self, elem): if not self.step: return elem == self.start @@ -932,12 +931,51 @@ class count(_coconut_base_hashable): if not self.step: return self raise _coconut.TypeError(_coconut.repr(self) + " object is not reversible") - def __repr__(self): - return "count(%s, %s)" % (_coconut.repr(self.start), _coconut.repr(self.step)) +class cycle(_coconut_has_iter): + __slots__ = ("times",) + def __new__(cls, iterable, times=None): + self = _coconut_has_iter.__new__(cls, iterable) + self.times = times + return self def __reduce__(self): - return (self.__class__, (self.start, self.step)) - def __fmap__(self, func): - return _coconut_map(func, self) + return (self.__class__, (self.iter, self.times)) + def __copy__(self): + return self.__class__(self.get_new_iter(), self.times) + def __repr__(self): + return "cycle(%s, %r)" % (_coconut.repr(self.iter), self.times) + def __iter__(self): + i = 0 + while self.times is None or i < self.times: + for x in self.get_new_iter(): + yield x + i += 1 + def __contains__(self, elem): + return elem in self.iter + def __getitem__(self, index): + if not _coconut.isinstance(index, _coconut.slice): + if self.times is not None and index // _coconut.len(self.iter) >= self.times: + raise _coconut.IndexError("cycle index out of range") + return self.iter[index % _coconut.len(self.iter)] + if self.times is None: + return _coconut_map(self.__getitem__, _coconut_count()[index]) + else: + return _coconut_map(self.__getitem__, _coconut_range(0, _coconut.len(self))[index]) + def __len__(self): + if self.times is None: + return _coconut.NotImplemented + return _coconut.len(self.iter) * self.times + def __reversed__(self): + if self.times is None: + raise _coconut.TypeError(_coconut.repr(self) + " object is not reversible") + return self.__class__(_coconut_reversed(self.get_new_iter()), self.times) + def count(self, elem): + """Count the number of times elem appears in the cycle.""" + return self.iter.count(elem) * (float("inf") if self.times is None else self.times) + def index(self, elem): + """Find the index of elem in the cycle.""" + if elem not in self.iter: + raise _coconut.ValueError(_coconut.repr(elem) + " not in " + _coconut.repr(self)) + return self.iter.index(elem) class groupsof(_coconut_has_iter): """groupsof(n, iterable) splits iterable into groups of size n. @@ -973,8 +1011,6 @@ class groupsof(_coconut_has_iter): return (self.__class__, (self.group_size, self.iter)) def __copy__(self): return self.__class__(self.group_size, self.get_new_iter()) - def __fmap__(self, func): - return _coconut_map(func, self) class recursive_iterator(_coconut_base_hashable): """Decorator that optimizes a recursive function that returns an iterator (e.g. a recursive generator).""" __slots__ = ("func", "reit_store", "backup_reit_store") @@ -1102,7 +1138,6 @@ _coconut_addpattern = addpattern {def_prepattern} class _coconut_partial(_coconut_base_hashable): __slots__ = ("func", "_argdict", "_arglen", "_pos_kwargs", "_stargs", "keywords") - __doc__ = getattr(_coconut.functools.partial, "__doc__", "Partial application of a function.") def __init__(self, _coconut_func, _coconut_argdict, _coconut_arglen, _coconut_pos_kwargs, *args, **kwargs): self.func = _coconut_func self._argdict = _coconut_argdict @@ -1551,4 +1586,4 @@ def _coconut_multi_dim_arr(arrs, dim): max_arr_dim = _coconut.max(arr_dims) return _coconut_concatenate(arrs, max_arr_dim - dim) _coconut_self_match_types = {self_match_types} -_coconut_Expected, _coconut_MatchError, _coconut_count, _coconut_enumerate, _coconut_flatten, _coconut_filter, _coconut_ident, _coconut_map, _coconut_multiset, _coconut_reiterable, _coconut_reversed, _coconut_starmap, _coconut_tee, _coconut_zip, TYPE_CHECKING, reduce, takewhile, dropwhile = Expected, MatchError, count, enumerate, flatten, filter, ident, map, multiset, reiterable, reversed, starmap, tee, zip, False, _coconut.functools.reduce, _coconut.itertools.takewhile, _coconut.itertools.dropwhile +_coconut_Expected, _coconut_MatchError, _coconut_count, _coconut_enumerate, _coconut_flatten, _coconut_filter, _coconut_ident, _coconut_map, _coconut_multiset, _coconut_range, _coconut_reiterable, _coconut_reversed, _coconut_starmap, _coconut_tee, _coconut_zip, TYPE_CHECKING, reduce, takewhile, dropwhile = Expected, MatchError, count, enumerate, flatten, filter, ident, map, multiset, range, reiterable, reversed, starmap, tee, zip, False, _coconut.functools.reduce, _coconut.itertools.takewhile, _coconut.itertools.dropwhile diff --git a/coconut/constants.py b/coconut/constants.py index ac16865d4..eaaaf2109 100644 --- a/coconut/constants.py +++ b/coconut/constants.py @@ -625,6 +625,7 @@ def get_bool_env_var(env_var, default=False): "multi_enumerate", "cartesian_product", "multiset", + "cycle", "py_chr", "py_hex", "py_input", diff --git a/coconut/root.py b/coconut/root.py index dc369d25c..476d769fd 100644 --- a/coconut/root.py +++ b/coconut/root.py @@ -26,7 +26,7 @@ VERSION = "2.1.1" VERSION_NAME = "The Spanish Inquisition" # False for release, int >= 1 for develop -DEVELOP = 25 +DEVELOP = 26 ALPHA = False # for pre releases rather than post releases # ----------------------------------------------------------------------------------------------------------------------- diff --git a/coconut/tests/src/cocotest/agnostic/main.coco b/coconut/tests/src/cocotest/agnostic/main.coco index 0f8b739ba..471302742 100644 --- a/coconut/tests/src/cocotest/agnostic/main.coco +++ b/coconut/tests/src/cocotest/agnostic/main.coco @@ -1333,6 +1333,14 @@ def main_test() -> bool: assert not (m{1} == {1:1, 2:0}) assert s{1, 2, *(2, 3, 4), *(4, 5)} == s{1, 2, 3, 4, 5} assert m{1, 2, *(2, 3, 4), *(4, 5)} == m{1, 2, 2, 3, 4, 4, 5} + assert {*(1, 2)} == {1, 2} + assert cycle(range(3))[:5] |> list == [0, 1, 2, 0, 1] == cycle(range(3)) |> iter |> .$[:5] |> list + assert cycle(range(2), 2)[:5] |> list == [0, 1, 0, 1] == cycle(range(2), 2) |> iter |> .$[:5] |> list + assert 2 in cycle(range(3)) + assert reversed(cycle(range(2), 2)) |> list == [1, 0, 1, 0] + assert cycle((_ for _ in range(2)), 2) |> list == [0, 1, 0, 1] + assert cycle(range(3)).count(0) == float("inf") + assert cycle(range(3), 3).index(2) == 2 return True def test_asyncio() -> bool: