commit ee14682486a21d3e8bab6036a67c9f4de5643d2d Author: upstream Date: Fri Sep 4 04:48:04 2026 +0200 Upstream Suor/funcy state of the touched files before 3a87dd4e11 diff --git a/funcy/seqs.py b/funcy/seqs.py new file mode 100644 index 0000000..760a18e --- /dev/null +++ b/funcy/seqs.py @@ -0,0 +1,504 @@ +import sys +from itertools import islice, chain, tee, groupby, filterfalse, accumulate, \ + takewhile as _takewhile, dropwhile as _dropwhile +from collections.abc import Sequence +from collections import defaultdict, deque +import operator + +from .primitives import EMPTY +from .types import is_seqcont +from .funcmakers import make_func, make_pred + + +__all__ = [ + 'count', 'cycle', 'repeat', 'repeatedly', 'iterate', + 'take', 'drop', 'first', 'second', 'nth', 'last', 'rest', 'butlast', 'ilen', + 'map', 'filter', 'lmap', 'lfilter', 'remove', 'lremove', 'keep', 'lkeep', 'without', 'lwithout', + 'concat', 'lconcat', 'chain', 'cat', 'lcat', 'flatten', 'lflatten', 'mapcat', 'lmapcat', + 'interleave', 'interpose', 'distinct', 'ldistinct', + 'dropwhile', 'takewhile', 'split', 'lsplit', 'split_at', 'lsplit_at', 'split_by', 'lsplit_by', + 'group_by', 'group_by_keys', 'group_values', 'count_by', 'count_reps', + 'partition', 'lpartition', 'chunks', 'lchunks', 'partition_by', 'lpartition_by', + 'with_prev', 'with_next', 'pairwise', 'lzip', + 'reductions', 'lreductions', 'sums', 'lsums', 'accumulate', +] + +_map, _filter = map, filter + +def _lmap(f, *seqs): + return list(map(f, *seqs)) + +def _lfilter(f, seq): + return list(filter(f, seq)) + + +# Re-export +from itertools import count, cycle, repeat + +def repeatedly(f, n=EMPTY): + """Takes a function of no args, presumably with side effects, + and returns an infinite (or length n) iterator of calls to it.""" + _repeat = repeat(None) if n is EMPTY else repeat(None, n) + return (f() for _ in _repeat) + +def iterate(f, x): + """Returns an infinite iterator of `x, f(x), f(f(x)), ...`""" + while True: + yield x + x = f(x) + + +def take(n, seq): + """Returns a list of first n items in the sequence, + or all items if there are fewer than n.""" + return list(islice(seq, n)) + +def drop(n, seq): + """Skips first n items in the sequence, yields the rest.""" + return islice(seq, n, None) + +def first(seq): + """Returns the first item in the sequence. + Returns None if the sequence is empty.""" + return next(iter(seq), None) + +def second(seq): + """Returns second item in the sequence. + Returns None if there are less than two items in it.""" + return first(rest(seq)) + +def nth(n, seq): + """Returns nth item in the sequence or None if no such item exists.""" + try: + return seq[n] + except IndexError: + return None + except TypeError: + return next(islice(seq, n, None), None) + +def last(seq): + """Returns the last item in the sequence or iterator. + Returns None if the sequence is empty.""" + try: + return seq[-1] + except IndexError: + return None + except TypeError: + item = None + for item in seq: + pass + return item + +def rest(seq): + """Skips first item in the sequence, yields the rest.""" + return drop(1, seq) + +def butlast(seq): + """Iterates over all elements of the sequence but last.""" + it = iter(seq) + try: + prev = next(it) + except StopIteration: + pass + else: + for item in it: + yield prev + prev = item + +def ilen(seq): + """Consumes an iterable not reading it into memory + and returns the number of items.""" + # NOTE: implementation borrowed from https://stackoverflow.com/a/15112059/753382 + counter = count() + deque(zip(seq, counter), maxlen=0) # (consume at C speed) + return next(counter) + + +# TODO: tree-seq equivalent + +def lmap(f, *seqs): + """An extended version of builtin map() returning a list. + Derives a mapper from string, int, slice, dict or set.""" + return _lmap(make_func(f), *seqs) + +def lfilter(pred, seq): + """An extended version of builtin filter() returning a list. + Derives a predicate from string, int, slice, dict or set.""" + return _lfilter(make_pred(pred), seq) + +def map(f, *seqs): + """An extended version of builtin map(). + Derives a mapper from string, int, slice, dict or set.""" + return _map(make_func(f), *seqs) + +def filter(pred, seq): + """An extended version of builtin filter(). + Derives a predicate from string, int, slice, dict or set.""" + return _filter(make_pred(pred), seq) + +def lremove(pred, seq): + """Creates a list if items passing given predicate.""" + return list(remove(pred, seq)) + +def remove(pred, seq): + """Iterates items passing given predicate.""" + return filterfalse(make_pred(pred), seq) + +def lkeep(f, seq=EMPTY): + """Maps seq with f and keeps only truthy results. + Simply lists truthy values in one argument version.""" + return list(keep(f, seq)) + +def keep(f, seq=EMPTY): + """Maps seq with f and iterates truthy results. + Simply iterates truthy values in one argument version.""" + if seq is EMPTY: + return _filter(bool, f) + else: + return _filter(bool, map(f, seq)) + +def without(seq, *items): + """Iterates over sequence skipping items.""" + for value in seq: + if value not in items: + yield value + +def lwithout(seq, *items): + """Removes items from sequence, preserves order.""" + return list(without(seq, *items)) + + +def lconcat(*seqs): + """Concatenates several sequences.""" + return list(chain(*seqs)) +concat = chain + +def lcat(seqs): + """Concatenates the sequence of sequences.""" + return list(cat(seqs)) +cat = chain.from_iterable + +def flatten(seq, follow=is_seqcont): + """Flattens arbitrary nested sequence. + Unpacks an item if follow(item) is truthy.""" + for item in seq: + if follow(item): + yield from flatten(item, follow) + else: + yield item + +def lflatten(seq, follow=is_seqcont): + """Iterates over arbitrary nested sequence. + Dives into when follow(item) is truthy.""" + return list(flatten(seq, follow)) + +def lmapcat(f, *seqs): + """Maps given sequence(s) and concatenates the results.""" + return lcat(map(f, *seqs)) + +def mapcat(f, *seqs): + """Maps given sequence(s) and chains the results.""" + return cat(map(f, *seqs)) + +def interleave(*seqs): + """Yields first item of each sequence, then second one and so on.""" + return cat(zip(*seqs)) + +def interpose(sep, seq): + """Yields items of the sequence alternating with sep.""" + return drop(1, interleave(repeat(sep), seq)) + +def takewhile(pred, seq=EMPTY): + """Yields sequence items until first predicate fail. + Stops on first falsy value in one argument version.""" + if seq is EMPTY: + pred, seq = bool, pred + else: + pred = make_pred(pred) + return _takewhile(pred, seq) + +def dropwhile(pred, seq=EMPTY): + """Skips the start of the sequence passing pred (or just truthy), + then iterates over the rest.""" + if seq is EMPTY: + pred, seq = bool, pred + else: + pred = make_pred(pred) + return _dropwhile(pred, seq) + + +def ldistinct(seq, key=EMPTY): + """Removes duplicates from sequences, preserves order.""" + return list(distinct(seq, key)) + +def distinct(seq, key=EMPTY): + """Iterates over sequence skipping duplicates""" + seen = set() + # check if key is supplied out of loop for efficiency + if key is EMPTY: + for item in seq: + if item not in seen: + seen.add(item) + yield item + else: + key = make_func(key) + for item in seq: + k = key(item) + if k not in seen: + seen.add(k) + yield item + + +def split(pred, seq): + """Lazily splits items which pass the predicate from the ones that don't. + Returns a pair (passed, failed) of respective iterators.""" + pred = make_pred(pred) + yes, no = deque(), deque() + splitter = (yes.append(item) if pred(item) else no.append(item) for item in seq) + + def _split(q): + while True: + while q: + yield q.popleft() + try: + next(splitter) + except StopIteration: + return + + return _split(yes), _split(no) + +def lsplit(pred, seq): + """Splits items which pass the predicate from the ones that don't. + Returns a pair (passed, failed) of respective lists.""" + pred = make_pred(pred) + yes, no = [], [] + for item in seq: + if pred(item): + yes.append(item) + else: + no.append(item) + return yes, no + + +def split_at(n, seq): + """Lazily splits the sequence at given position, + returning a pair of iterators over its start and tail.""" + a, b = tee(seq) + return islice(a, n), islice(b, n, None) + +def lsplit_at(n, seq): + """Splits the sequence at given position, + returning a tuple of its start and tail.""" + a, b = split_at(n, seq) + return list(a), list(b) + +def split_by(pred, seq): + """Lazily splits the start of the sequence, + consisting of items passing pred, from the rest of it.""" + a, b = tee(seq) + return takewhile(pred, a), dropwhile(pred, b) + +def lsplit_by(pred, seq): + """Splits the start of the sequence, + consisting of items passing pred, from the rest of it.""" + a, b = split_by(pred, seq) + return list(a), list(b) + + +def group_by(f, seq): + """Groups given sequence items into a mapping f(item) -> [item, ...].""" + f = make_func(f) + result = defaultdict(list) + for item in seq: + result[f(item)].append(item) + return result + + +def group_by_keys(get_keys, seq): + """Groups items having multiple keys into a mapping key -> [item, ...]. + Item might be repeated under several keys.""" + get_keys = make_func(get_keys) + result = defaultdict(list) + for item in seq: + for k in get_keys(item): + result[k].append(item) + return result + + +def group_values(seq): + """Takes a sequence of (key, value) pairs and groups values by keys.""" + result = defaultdict(list) + for key, value in seq: + result[key].append(value) + return result + + +def count_by(f, seq): + """Counts numbers of occurrences of values of f() + on elements of given sequence.""" + f = make_func(f) + result = defaultdict(int) + for item in seq: + result[f(item)] += 1 + return result + + +def count_reps(seq): + """Counts number occurrences of each value in the sequence.""" + result = defaultdict(int) + for item in seq: + result[item] += 1 + return result + + +# For efficiency we use separate implementation for cutting sequences (those capable of slicing) +def _cut_seq(drop_tail, n, step, seq): + limit = len(seq)-n+1 if drop_tail else len(seq) + return (seq[i:i+n] for i in range(0, limit, step)) + +def _cut_iter(drop_tail, n, step, seq): + it = iter(seq) + pool = take(n, it) + while True: + if len(pool) < n: + break + yield pool + pool = pool[step:] + pool.extend(islice(it, step)) + if not drop_tail: + for item in _cut_seq(drop_tail, n, step, pool): + yield item + +def _cut(drop_tail, n, step, seq=EMPTY): + if seq is EMPTY: + step, seq = n, step + if isinstance(seq, Sequence): + return _cut_seq(drop_tail, n, step, seq) + else: + return _cut_iter(drop_tail, n, step, seq) + +def partition(n, step, seq=EMPTY): + """Lazily partitions seq into parts of length n. + Skips step items between parts if passed. Non-fitting tail is ignored.""" + return _cut(True, n, step, seq) + +def lpartition(n, step, seq=EMPTY): + """Partitions seq into parts of length n. + Skips step items between parts if passed. Non-fitting tail is ignored.""" + return list(partition(n, step, seq)) + +def chunks(n, step, seq=EMPTY): + """Lazily chunks seq into parts of length n or less. + Skips step items between parts if passed.""" + return _cut(False, n, step, seq) + +def lchunks(n, step, seq=EMPTY): + """Chunks seq into parts of length n or less. + Skips step items between parts if passed.""" + return list(chunks(n, step, seq)) + +def partition_by(f, seq): + """Lazily partition seq into continuous chunks with constant value of f.""" + f = make_func(f) + for _, items in groupby(seq, f): + yield items + +def lpartition_by(f, seq): + """Partition seq into continuous chunks with constant value of f.""" + return _lmap(list, partition_by(f, seq)) + + +def with_prev(seq, fill=None): + """Yields each item paired with its preceding: (item, prev).""" + a, b = tee(seq) + return zip(a, chain([fill], b)) + +def with_next(seq, fill=None): + """Yields each item paired with its following: (item, next).""" + a, b = tee(seq) + next(b, None) + return zip(a, chain(b, [fill])) + +# An itertools recipe +# NOTE: this is the same as ipartition(2, 1, seq) only faster and with distinct name +def pairwise(seq): + """Yields all pairs of neighboring items in seq.""" + a, b = tee(seq) + next(b, None) + return zip(a, b) + +if sys.version_info >= (3, 10): + def lzip(*seqs, strict=False): + """List zip() version.""" + return list(zip(*seqs, strict=strict)) +else: + def lzip(*seqs, strict=False): + """List zip() version.""" + if strict and len(seqs) > 1: + return list(_zip_strict(*seqs)) + return list(zip(*seqs)) + + def _zip_strict(*seqs): + try: + # Try compare lens if they are available and use a fast zip() builtin + len_1 = len(seqs[0]) + for i, s in enumerate(seqs, start=1): + len_i = len(s) + if len_i != len_1: + short_i, long_i = (1, i) if len_1 < len_i else (i, 1) + raise _zip_strict_error(short_i, long_i) + except TypeError: + return _zip_strict_iters(*seqs) + else: + return zip(*seqs) + + def _zip_strict_iters(*seqs): + iters = [iter(s) for s in seqs] + while True: + values, stop_i, val_i = [], 0, 0 + for i, it in enumerate(iters, start=1): + try: + values.append(next(it)) + if not val_i: + val_i = i + except StopIteration: + if not stop_i: + stop_i = i + + if stop_i: + if val_i: + raise _zip_strict_error(stop_i, val_i) + break + yield tuple(values) + + def _zip_strict_error(short_i, long_i): + if short_i == 1: + return ValueError("zip() argument %d is longer than argument 1" % long_i) + else: + start = "argument 1" if short_i == 2 else "argument 1-%d" % (short_i - 1) + return ValueError("zip() argument %d is shorter than %s" % (short_i, start)) + + +def _reductions(f, seq, acc): + last = acc + for x in seq: + last = f(last, x) + yield last + +def reductions(f, seq, acc=EMPTY): + """Yields intermediate reductions of seq by f.""" + if acc is EMPTY: + return accumulate(seq) if f is operator.add else accumulate(seq, f) + return _reductions(f, seq, acc) + +def lreductions(f, seq, acc=EMPTY): + """Lists intermediate reductions of seq by f.""" + return list(reductions(f, seq, acc)) + +def sums(seq, acc=EMPTY): + """Yields partial sums of seq.""" + return reductions(operator.add, seq, acc) + +def lsums(seq, acc=EMPTY): + """Lists partial sums of seq.""" + return lreductions(operator.add, seq, acc) diff --git a/tests/test_seqs.py b/tests/test_seqs.py new file mode 100644 index 0000000..4a4c0e6 --- /dev/null +++ b/tests/test_seqs.py @@ -0,0 +1,219 @@ +from collections.abc import Iterator +from operator import add +import pytest +from whatever import _ + +from funcy import is_list +from funcy.seqs import * + + +def test_repeatedly(): + counter = count() + c = lambda: next(counter) + assert take(2, repeatedly(c)) == [0, 1] + +def test_iterate(): + assert take(4, iterate(_ * 2, 1)) == [1, 2, 4, 8] + + +def test_take(): + assert take(2, [3, 2, 1]) == [3, 2] + assert take(2, count(7)) == [7, 8] + +def test_drop(): + dropped = drop(2, [5, 4, 3, 2]) + assert isinstance(dropped, Iterator) + assert list(dropped) == [3, 2] + + assert take(2, drop(2, count())) == [2, 3] + +def test_first(): + assert first('xyz') == 'x' + assert first(count(7)) == 7 + assert first([]) is None + +def test_second(): + assert second('xyz') == 'y' + assert second(count(7)) == 8 + assert second('x') is None + +def test_last(): + assert last('xyz') == 'z' + assert last(range(1, 10)) == 9 + assert last([]) is None + assert last(x for x in 'xyz') == 'z' + +def test_nth(): + assert nth(0, 'xyz') == 'x' + assert nth(2, 'xyz') == 'z' + assert nth(3, 'xyz') is None + assert nth(3, count(7)) == 10 + +def test_butlast(): + assert list(butlast('xyz')) == ['x', 'y'] + assert list(butlast([])) == [] + +def test_ilen(): + assert ilen('xyz') == 3 + assert ilen(range(10)) == 10 + + +def test_lmap(): + assert lmap(_ * 2, [2, 3]) == [4, 6] + assert lmap(None, [2, 3]) == [2, 3] + assert lmap(_ + _, [1, 2], [4, 5]) == [5, 7] + + assert lmap(r'\d+', ['a2', '13b']) == ['2', '13'] + assert lmap({'a': 1, 'b': 2}, 'ab') == [1, 2] + assert lmap(set([1,2,3]), [0, 1, 2]) == [False, True, True] + assert lmap(1, ['abc', '123']) == ['b', '2'] + assert lmap(slice(2), ['abc', '123']) == ['ab', '12'] + + +def test_filter(): + assert lfilter(None, [2, 3, 0]) == [2, 3] + assert lfilter(r'\d+', ['a2', '13b', 'c']) == ['a2', '13b'] + assert lfilter(set([1,2,3]), [0, 1, 2, 4, 1]) == [1, 2, 1] + +def test_remove(): + assert lremove(_ > 3, range(10)) == [0, 1, 2, 3] + assert lremove('^a', ['a', 'b', 'ba']) == ['b', 'ba'] + +def test_keep(): + assert lkeep(_ % 3, range(5)) == [1, 2, 1] + assert lkeep(range(5)) == [1, 2, 3, 4] + assert lkeep(mapcat(range, range(4))) == [1, 1, 2] + +def test_concat(): + assert lconcat('ab', 'cd') == list('abcd') + assert lconcat() == [] + +def test_cat(): + assert lcat('abcd') == list('abcd') + assert lcat(range(x) for x in range(3)) == [0, 0, 1] + +def test_flatten(): + assert lflatten([1, [2, 3]]) == [1, 2, 3] + assert lflatten([[1, 2], 3]) == [1, 2, 3] + assert lflatten([(2, 3)]) == [2, 3] + assert lflatten([iter([2, 3])]) == [2, 3] + +def test_flatten_follow(): + assert lflatten([1, [2, 3]], follow=is_list) == [1, 2, 3] + assert lflatten([1, [(2, 3)]], follow=is_list) == [1, (2, 3)] + +def test_mapcat(): + assert lmapcat(lambda x: [x, x], 'abc') == list('aabbcc') + +def test_interleave(): + assert list(interleave('ab', 'cd')) == list('acbd') + assert list(interleave('ab_', 'cd')) == list('acbd') + +def test_iterpose(): + assert list(interpose('.', 'abc')) == list('a.b.c') + + +def test_takewhile(): + assert list(takewhile([1, 2, None, 3])) == [1, 2] + + +def test_distinct(): + assert ldistinct('abcbad') == list('abcd') + assert ldistinct([{}, {}, {'a': 1}, {'b': 2}], key=len) == [{}, {'a': 1}] + assert ldistinct(['ab', 'cb', 'ad'], key=0) == ['ab', 'cb'] + +# Separate test as lsplit() is not implemented via it. +def test_split(): + assert lmap(list, split(_ % 2, range(5))) == [[1, 3], [0, 2, 4]] + +def test_lsplit(): + assert lsplit(_ % 2, range(5)) == ([1, 3], [0, 2, 4]) + # This behaviour moved to split_at() + with pytest.raises(TypeError): lsplit(2, range(5)) + +def test_split_at(): + assert lsplit_at(2, range(5)) == ([0, 1], [2, 3, 4]) + +def test_split_by(): + assert lsplit_by(_ % 2, [1, 2, 3]) == ([1], [2, 3]) + +def test_group_by(): + assert group_by(_ % 2, range(5)) == {0: [0, 2, 4], 1: [1, 3]} + assert group_by(r'\d', ['a1', 'b2', 'c1']) == {'1': ['a1', 'c1'], '2': ['b2']} + +def test_group_by_keys(): + assert group_by_keys(r'(\d)(\d)', ['12', '23']) == {'1': ['12'], '2': ['12', '23'], '3': ['23']} + +def test_group_values(): + assert group_values(['ab', 'ac', 'ba']) == {'a': ['b', 'c'], 'b': ['a']} + +def test_count_by(): + assert count_by(_ % 2, range(5)) == {0: 3, 1: 2} + assert count_by(r'\d', ['a1', 'b2', 'c1']) == {'1': 2, '2': 1} + +def test_count_by_is_defaultdict(): + cnts = count_by(len, []) + assert cnts[1] == 0 + +def test_count_reps(): + assert count_reps([0, 1, 0]) == {0: 2, 1: 1} + +def test_partition(): + assert lpartition(2, [0, 1, 2, 3, 4]) == [[0, 1], [2, 3]] + assert lpartition(2, 1, [0, 1, 2, 3]) == [[0, 1], [1, 2], [2, 3]] + # test iters + assert lpartition(2, iter(range(5))) == [[0, 1], [2, 3]] + assert lmap(list, lpartition(2, range(5))) == [[0, 1], [2, 3]] + +def test_chunks(): + assert lchunks(2, [0, 1, 2, 3, 4]) == [[0, 1], [2, 3], [4]] + assert lchunks(2, 1, [0, 1, 2, 3]) == [[0, 1], [1, 2], [2, 3], [3]] + assert lchunks(3, 1, iter(range(3))) == [[0, 1, 2], [1, 2], [2]] + +def test_partition_by(): + assert lpartition_by(lambda x: x == 3, [1,2,3,4,5]) == [[1,2], [3], [4,5]] + assert lpartition_by('x', 'abxcd') == [['a', 'b'], ['x'], ['c', 'd']] + assert lpartition_by(r'\d', '1211') == [['1'], ['2'], ['1','1']] + + +def test_with_prev(): + assert list(with_prev(range(3))) == [(0, None), (1, 0), (2, 1)] + +def test_with_next(): + assert list(with_next(range(3))) == [(0, 1), (1, 2), (2, None)] + +def test_pairwise(): + assert list(pairwise(range(3))) == [(0, 1), (1, 2)] + +def test_lzip(): + assert lzip('12', 'xy') == [('1', 'x'), ('2', 'y')] + assert lzip('123', 'xy') == [('1', 'x'), ('2', 'y')] + assert lzip('12', 'xyz') == [('1', 'x'), ('2', 'y')] + assert lzip('12', iter('xyz')) == [('1', 'x'), ('2', 'y')] + +def test_lzip_strict(): + assert lzip('123', 'xy', strict=False) == [('1', 'x'), ('2', 'y')] + assert lzip('12', 'xy', strict=True) == [('1', 'x'), ('2', 'y')] + assert lzip('12', iter('xy'), strict=True) == [('1', 'x'), ('2', 'y')] + for wrap in (str, iter): + with pytest.raises(ValueError): lzip(wrap('123'), wrap('xy'), strict=True) + with pytest.raises(ValueError): lzip(wrap('12'), wrap('xyz'), wrap('abcd'), strict=True) + with pytest.raises(ValueError): lzip(wrap('123'), wrap('xy'), wrap('abcd'), strict=True) + with pytest.raises(ValueError): lzip(wrap('123'), wrap('xyz'), wrap('ab'), strict=True) + + +def test_reductions(): + assert lreductions(add, []) == [] + assert lreductions(add, [None]) == [None] + assert lreductions(add, [1, 2, 3, 4]) == [1, 3, 6, 10] + assert lreductions(lambda x, y: x + [y], [1,2,3], []) == [[1], [1, 2], [1, 2, 3]] + +def test_sums(): + assert lsums([]) == [] + assert lsums([1, 2, 3, 4]) == [1, 3, 6, 10] + assert lsums([[1],[2],[3]]) == [[1], [1, 2], [1, 2, 3]] + +def test_without(): + assert lwithout([]) == [] + assert lwithout([1, 2, 3, 4]) == [1, 2, 3, 4] + assert lwithout([1, 2, 1, 0, 3, 1, 4], 0, 1) == [2, 3, 4]