Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-more-itertools for openSUSE:Factory checked in at 2021-03-10 08:49:38 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-more-itertools (Old) and /work/SRC/openSUSE:Factory/.python-more-itertools.new.2378 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-more-itertools" Wed Mar 10 08:49:38 2021 rev:14 rq:876826 version:8.7.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-more-itertools/python-more-itertools.changes 2020-12-24 19:41:28.467220043 +0100 +++ /work/SRC/openSUSE:Factory/.python-more-itertools.new.2378/python-more-itertools.changes 2021-03-10 08:49:50.350429692 +0100 @@ -1,0 +2,19 @@ +Thu Mar 4 21:03:34 UTC 2021 - Dirk M??ller <dmuel...@suse.com> + +- update to 8.7.0: + * New functions + * :func:`convolve` + * :func:`product_index`, :func:`combination_index`, and :func:`permutation_index` + * :func:`value_chain` + * Changes to existing functions + * :func:`distinct_combinations` now uses a non-recursive algorithm + * :func:`pad_none` is now the preferred name for :func:`padnone`, though the latter remains available. + * :func:`pairwise` will now use the Python standard library implementation on Python 3.10+ + * :func:`sort_together` now accepts a ``key`` argument + * :func:`seekable` now has a ``peek`` method, and can indicate whether the iterator it's wrapping is exhausted + * :func:`time_limited` can now indicate whether its iterator has expired + * The implementation of :func:`unique_everseen` was improved + * Other changes: + * Various documentation updates + +------------------------------------------------------------------- Old: ---- more-itertools-8.6.0.tar.gz New: ---- more-itertools-8.7.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-more-itertools.spec ++++++ --- /var/tmp/diff_new_pack.hwtvNN/_old 2021-03-10 08:49:50.902430262 +0100 +++ /var/tmp/diff_new_pack.hwtvNN/_new 2021-03-10 08:49:50.902430262 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-more-itertools # -# Copyright (c) 2020 SUSE LLC +# Copyright (c) 2021 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-more-itertools -Version: 8.6.0 +Version: 8.7.0 Release: 0 Summary: More routines for operating on iterables, beyond itertools License: MIT ++++++ more-itertools-8.6.0.tar.gz -> more-itertools-8.7.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/PKG-INFO new/more-itertools-8.7.0/PKG-INFO --- old/more-itertools-8.6.0/PKG-INFO 2020-10-30 14:50:39.324085500 +0100 +++ new/more-itertools-8.7.0/PKG-INFO 2021-02-08 02:52:59.821831700 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: more-itertools -Version: 8.6.0 +Version: 8.7.0 Summary: More routines for operating on iterables, beyond itertools Home-page: https://github.com/more-itertools/more-itertools Author: Erik Rose @@ -62,9 +62,11 @@ | | `zip_offset <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.zip_offset>`_, | | | `zip_equal <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.zip_equal>`_, | | | `dotproduct <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.dotproduct>`_, | + | | `convolve <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.convolve>`_, | | | `flatten <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.flatten>`_, | | | `roundrobin <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.roundrobin>`_, | - | | `prepend <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.prepend>`_ | + | | `prepend <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.prepend>`_, | + | | `value_chain <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.value_chain>`_ | +------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Summarizing | `ilen <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.ilen>`_, | | | `unique_to_each <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.unique_to_each>`_, | @@ -101,6 +103,9 @@ | | `circular_shifts <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.circular_shifts>`_, | | | `partitions <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.partitions>`_, | | | `set_partitions <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.set_partitions>`_, | + | | `product_index <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.product_index>`_, | + | | `combination_index <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.combination_index>`_, | + | | `permutation_index <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.permutation_index>`_, | | | `powerset <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.powerset>`_, | | | `random_product <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.random_product>`_, | | | `random_permutation <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.random_permutation>`_, | @@ -179,7 +184,7 @@ Blog posts about ``more-itertools``: * `Yo, I heard you like decorators <https://www.bbayles.com/index/decorator_factory>`__ - * `Tour of Python Itertools <https://martinheinz.dev/blog/16>`__ + * `Tour of Python Itertools <https://martinheinz.dev/blog/16>`__ (`Alternate <https://dev.to/martinheinz/tour-of-python-itertools-4122>`__) Development @@ -197,6 +202,26 @@ :noindex: + 8.7.0 + ----- + + * New functions + * convolve (from the Python itertools docs) + * product_index, combination_index, and permutation_index (thanks to N8Brooks) + * value_chain (thanks to jenstroeger) + + * Changes to existing functions + * distinct_combinations now uses a non-recursive algorithm (thanks to knutdrand) + * pad_none is now the preferred name for padnone, though the latter remains available. + * pairwise will now use the Python standard library implementation on Python 3.10+ + * sort_together now accepts a ``key`` argument (thanks to brianmaissy) + * seekable now has a ``peek`` method, and can indicate whether the iterator it's wrapping is exhausted (thanks to gsakkis) + * time_limited can now indicate whether its iterator has expired (thanks to roysmith) + * The implementation of unique_everseen was improved (thanks to plammens) + + * Other changes: + * Various documentation updates (thanks to cthoyt, Evantm, and cyphase) + 8.6.0 ----- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/README.rst new/more-itertools-8.7.0/README.rst --- old/more-itertools-8.6.0/README.rst 2020-10-30 14:50:21.000000000 +0100 +++ new/more-itertools-8.7.0/README.rst 2021-02-06 14:41:48.000000000 +0100 @@ -54,9 +54,11 @@ | | `zip_offset <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.zip_offset>`_, | | | `zip_equal <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.zip_equal>`_, | | | `dotproduct <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.dotproduct>`_, | +| | `convolve <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.convolve>`_, | | | `flatten <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.flatten>`_, | | | `roundrobin <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.roundrobin>`_, | -| | `prepend <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.prepend>`_ | +| | `prepend <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.prepend>`_, | +| | `value_chain <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.value_chain>`_ | +------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Summarizing | `ilen <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.ilen>`_, | | | `unique_to_each <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.unique_to_each>`_, | @@ -93,6 +95,9 @@ | | `circular_shifts <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.circular_shifts>`_, | | | `partitions <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.partitions>`_, | | | `set_partitions <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.set_partitions>`_, | +| | `product_index <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.product_index>`_, | +| | `combination_index <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.combination_index>`_, | +| | `permutation_index <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.permutation_index>`_, | | | `powerset <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.powerset>`_, | | | `random_product <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.random_product>`_, | | | `random_permutation <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.random_permutation>`_, | @@ -171,7 +176,7 @@ Blog posts about ``more-itertools``: * `Yo, I heard you like decorators <https://www.bbayles.com/index/decorator_factory>`__ -* `Tour of Python Itertools <https://martinheinz.dev/blog/16>`__ +* `Tour of Python Itertools <https://martinheinz.dev/blog/16>`__ (`Alternate <https://dev.to/martinheinz/tour-of-python-itertools-4122>`__) Development diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/docs/api.rst new/more-itertools-8.7.0/docs/api.rst --- old/more-itertools-8.6.0/docs/api.rst 2020-10-30 14:50:21.000000000 +0100 +++ new/more-itertools-8.7.0/docs/api.rst 2021-02-06 14:41:48.000000000 +0100 @@ -92,10 +92,11 @@ **Itertools recipes** -.. autofunction:: padnone +.. function:: padnone + :noindex: +.. autofunction:: pad_none .. autofunction:: ncycles - Combining ========= @@ -106,9 +107,10 @@ **New itertools** .. autofunction:: collapse -.. autofunction:: sort_together .. autofunction:: interleave .. autofunction:: interleave_longest +.. autofunction:: sort_together +.. autofunction:: value_chain .. autofunction:: zip_offset(*iterables, offsets, longest=False, fillvalue=None) .. autofunction:: zip_equal @@ -117,6 +119,7 @@ **Itertools recipes** .. autofunction:: dotproduct +.. autofunction:: convolve .. autofunction:: flatten .. autofunction:: roundrobin .. autofunction:: prepend @@ -199,6 +202,9 @@ .. autofunction:: circular_shifts .. autofunction:: partitions .. autofunction:: set_partitions +.. autofunction:: product_index +.. autofunction:: combination_index +.. autofunction:: permutation_index ---- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/docs/versions.rst new/more-itertools-8.7.0/docs/versions.rst --- old/more-itertools-8.6.0/docs/versions.rst 2020-10-30 14:50:21.000000000 +0100 +++ new/more-itertools-8.7.0/docs/versions.rst 2021-02-08 02:52:56.000000000 +0100 @@ -5,6 +5,26 @@ .. automodule:: more_itertools :noindex: +8.7.0 +----- + +* New functions + * :func:`convolve` (from the Python itertools docs) + * :func:`product_index`, :func:`combination_index`, and :func:`permutation_index` (thanks to N8Brooks) + * :func:`value_chain` (thanks to jenstroeger) + +* Changes to existing functions + * :func:`distinct_combinations` now uses a non-recursive algorithm (thanks to knutdrand) + * :func:`pad_none` is now the preferred name for :func:`padnone`, though the latter remains available. + * :func:`pairwise` will now use the Python standard library implementation on Python 3.10+ + * :func:`sort_together` now accepts a ``key`` argument (thanks to brianmaissy) + * :func:`seekable` now has a ``peek`` method, and can indicate whether the iterator it's wrapping is exhausted (thanks to gsakkis) + * :func:`time_limited` can now indicate whether its iterator has expired (thanks to roysmith) + * The implementation of :func:`unique_everseen` was improved (thanks to plammens) + +* Other changes: + * Various documentation updates (thanks to cthoyt, Evantm, and cyphase) + 8.6.0 ----- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/more_itertools/__init__.py new/more-itertools-8.7.0/more_itertools/__init__.py --- old/more-itertools-8.6.0/more_itertools/__init__.py 2020-10-30 14:50:21.000000000 +0100 +++ new/more-itertools-8.7.0/more_itertools/__init__.py 2021-02-08 02:52:56.000000000 +0100 @@ -1,4 +1,4 @@ from .more import * # noqa from .recipes import * # noqa -__version__ = '8.6.0' +__version__ = '8.7.0' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/more_itertools/more.py new/more-itertools-8.7.0/more_itertools/more.py --- old/more-itertools-8.6.0/more_itertools/more.py 2020-10-30 14:50:21.000000000 +0100 +++ new/more-itertools-8.7.0/more_itertools/more.py 2021-02-08 02:52:56.000000000 +0100 @@ -114,6 +114,10 @@ 'zip_offset', 'windowed_complete', 'all_unique', + 'value_chain', + 'product_index', + 'combination_index', + 'permutation_index', ] _marker = object() @@ -134,7 +138,7 @@ To use a fill-in value instead, see the :func:`grouper` recipe. If the length of *iterable* is not divisible by *n* and *strict* is - ``True``, then then ``ValueError`` will be raised before the last + ``True``, then ``ValueError`` will be raised before the last list is yielded. """ @@ -1134,7 +1138,7 @@ [(1, 2, 3), (4, 5, 6), (7, 8)] If the length of *seq* is not divisible by *n* and *strict* is - ``True``, then then ``ValueError`` will be raised before the last + ``True``, then ``ValueError`` will be raised before the last slice is yielded. This function will only work for iterables that support slicing. @@ -1568,7 +1572,7 @@ return zip(*staggered) -def sort_together(iterables, key_list=(0,), reverse=False): +def sort_together(iterables, key_list=(0,), key=None, reverse=False): """Return the input iterables sorted together, with *key_list* as the priority for sorting. All iterables are trimmed to the length of the shortest one. @@ -1590,18 +1594,47 @@ >>> sort_together(iterables, key_list=(1, 2)) [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')] + To sort by a function of the elements of the iterable, pass a *key* + function. Its arguments are the elements of the iterables corresponding to + the key list:: + + >>> names = ('a', 'b', 'c') + >>> lengths = (1, 2, 3) + >>> widths = (5, 2, 1) + >>> def area(length, width): + ... return length * width + >>> sort_together([names, lengths, widths], key_list=(1, 2), key=area) + [('c', 'b', 'a'), (3, 2, 1), (1, 2, 5)] + Set *reverse* to ``True`` to sort in descending order. >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True) [(3, 2, 1), ('a', 'b', 'c')] """ - return list( - zip( - *sorted( - zip(*iterables), key=itemgetter(*key_list), reverse=reverse + if key is None: + # if there is no key function, the key argument to sorted is an + # itemgetter + key_argument = itemgetter(*key_list) + else: + # if there is a key function, call it with the items at the offsets + # specified by the key function as arguments + key_list = list(key_list) + if len(key_list) == 1: + # if key_list contains a single item, pass the item at that offset + # as the only argument to the key function + key_offset = key_list[0] + key_argument = lambda zipped_items: key(zipped_items[key_offset]) + else: + # if key_list contains multiple items, use itemgetter to return a + # tuple of items, which we pass as *args to the key function + get_key_items = itemgetter(*key_list) + key_argument = lambda zipped_items: key( + *get_key_items(zipped_items) ) - ) + + return list( + zip(*sorted(zip(*iterables), key=key_argument, reverse=reverse)) ) @@ -2463,8 +2496,8 @@ set to something other than ``None``, it will skip the first element when computing successive differences. - >>> iterable = [100, 101, 103, 106] # accumate([1, 2, 3], initial=100) - >>> list(difference(iterable, initial=100)) + >>> iterable = [10, 11, 13, 16] # accumulate([1, 2, 3], initial=10) + >>> list(difference(iterable, initial=10)) [1, 2, 3] """ @@ -2558,6 +2591,27 @@ >>> next(it), next(it), next(it) ('0', '1', '2') + Call :meth:`peek` to look ahead one item without advancing the iterator: + + >>> it = seekable('1234') + >>> it.peek() + '1' + >>> list(it) + ['1', '2', '3', '4'] + >>> it.peek(default='empty') + 'empty' + + Before the iterator is at its end, calling :func:`bool` on it will return + ``True``. After it will return ``False``: + + >>> it = seekable('5678') + >>> bool(it) + True + >>> list(it) + ['5', '6', '7', '8'] + >>> bool(it) + False + You may view the contents of the cache with the :meth:`elements` method. That returns a :class:`SequenceView`, a view that updates automatically: @@ -2615,6 +2669,25 @@ self._cache.append(item) return item + def __bool__(self): + try: + self.peek() + except StopIteration: + return False + return True + + def peek(self, default=_marker): + try: + peeked = next(self) + except StopIteration: + if default is _marker: + raise + return default + if self._index is None: + self._index = len(self._cache) + self._index -= 1 + return peeked + def elements(self): return SequenceView(self._cache) @@ -2994,9 +3067,11 @@ yield from set_partitions_helper(L, k) -def time_limited(limit_seconds, iterable): +class time_limited: """ Yield items from *iterable* until *limit_seconds* have passed. + If the time limit expires before all items have been yielded, the + ``timed_out`` parameter will be set to ``True``. >>> from time import sleep >>> def generator(): @@ -3004,9 +3079,11 @@ ... yield 2 ... sleep(0.2) ... yield 3 - >>> iterable = generator() - >>> list(time_limited(0.1, iterable)) + >>> iterable = time_limited(0.1, generator()) + >>> list(iterable) [1, 2] + >>> iterable.timed_out + True Note that the time is checked before each item is yielded, and iteration stops if the time elapsed is greater than *limit_seconds*. If your time @@ -3014,14 +3091,25 @@ the iterable, the function will run for 2 seconds and not yield anything. """ - if limit_seconds < 0: - raise ValueError('limit_seconds must be positive') - start_time = monotonic() - for item in iterable: - if monotonic() - start_time > limit_seconds: - break - yield item + def __init__(self, limit_seconds, iterable): + if limit_seconds < 0: + raise ValueError('limit_seconds must be positive') + self.limit_seconds = limit_seconds + self._iterable = iter(iterable) + self._start_time = monotonic() + self.timed_out = False + + def __iter__(self): + return self + + def __next__(self): + item = next(self._iterable) + if monotonic() - self._start_time > self.limit_seconds: + self.timed_out = True + raise StopIteration + + return item def only(iterable, default=None, too_long=None): @@ -3117,11 +3205,29 @@ raise ValueError('r must be non-negative') elif r == 0: yield () - else: - pool = tuple(iterable) - for i, prefix in unique_everseen(enumerate(pool), key=itemgetter(1)): - for suffix in distinct_combinations(pool[i + 1 :], r - 1): - yield (prefix,) + suffix + return + pool = tuple(iterable) + generators = [unique_everseen(enumerate(pool), key=itemgetter(1))] + current_combo = [None] * r + level = 0 + while generators: + try: + cur_idx, p = next(generators[-1]) + except StopIteration: + generators.pop() + level -= 1 + continue + current_combo[level] = p + if level + 1 == r: + yield tuple(current_combo) + else: + generators.append( + unique_everseen( + enumerate(pool[cur_idx + 1 :], cur_idx + 1), + key=itemgetter(1), + ) + ) + level += 1 def filter_except(validator, iterable, *exceptions): @@ -3494,6 +3600,11 @@ The products of *args* can be ordered lexicographically. :func:`nth_product` computes the product at sort position *index* without computing the previous products. + + >>> nth_product(8, range(2), range(2), range(2), range(2)) + (1, 0, 0, 0) + + ``IndexError`` will be raised if the given *index* is invalid. """ pools = list(map(tuple, reversed(args))) ns = list(map(len, pools)) @@ -3522,6 +3633,12 @@ computes the subsequence at sort position *index* directly, without computing the previous subsequences. + >>> nth_permutation('ghijk', 2, 5) + ('h', 'i') + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. """ pool = list(iterable) n = len(pool) @@ -3552,3 +3669,123 @@ break return tuple(map(pool.pop, result)) + + +def value_chain(*args): + """Yield all arguments passed to the function in the same order in which + they were passed. If an argument itself is iterable then iterate over its + values. + + >>> list(value_chain(1, 2, 3, [4, 5, 6])) + [1, 2, 3, 4, 5, 6] + + Binary and text strings are not considered iterable and are emitted + as-is: + + >>> list(value_chain('12', '34', ['56', '78'])) + ['12', '34', '56', '78'] + + + Multiple levels of nesting are not flattened. + + """ + for value in args: + if isinstance(value, (str, bytes)): + yield value + continue + try: + yield from value + except TypeError: + yield value + + +def product_index(element, *args): + """Equivalent to ``list(product(*args)).index(element)`` + + The products of *args* can be ordered lexicographically. + :func:`product_index` computes the first index of *element* without + computing the previous products. + + >>> product_index([8, 2], range(10), range(5)) + 42 + + ``ValueError`` will be raised if the given *element* isn't in the product + of *args*. + """ + index = 0 + + for x, pool in zip_longest(element, args, fillvalue=_marker): + if x is _marker or pool is _marker: + raise ValueError('element is not a product of args') + + pool = tuple(pool) + index = index * len(pool) + pool.index(x) + + return index + + +def combination_index(element, iterable): + """Equivalent to ``list(combinations(iterable, r)).index(element)`` + + The subsequences of *iterable* that are of length *r* can be ordered + lexicographically. :func:`combination_index` computes the index of the + first *element*, without computing the previous combinations. + + >>> combination_index('adf', 'abcdefg') + 10 + + ``ValueError`` will be raised if the given *element* isn't one of the + combinations of *iterable*. + """ + element = enumerate(element) + k, y = next(element, (None, None)) + if k is None: + return 0 + + indexes = [] + pool = enumerate(iterable) + for n, x in pool: + if x == y: + indexes.append(n) + tmp, y = next(element, (None, None)) + if tmp is None: + break + else: + k = tmp + else: + raise ValueError('element is not a combination of iterable') + + n, _ = last(pool, default=(n, None)) + + # Python versiosn below 3.8 don't have math.comb + index = 1 + for i, j in enumerate(reversed(indexes), start=1): + j = n - j + if i <= j: + index += factorial(j) // (factorial(i) * factorial(j - i)) + + return factorial(n + 1) // (factorial(k + 1) * factorial(n - k)) - index + + +def permutation_index(element, iterable): + """Equivalent to ``list(permutations(iterable, r)).index(element)``` + + The subsequences of *iterable* that are of length *r* where order is + important can be ordered lexicographically. :func:`permutation_index` + computes the index of the first *element* directly, without computing + the previous permutations. + + >>> permutation_index([1, 3, 2], range(5)) + 19 + + ``ValueError`` will be raised if the given *element* isn't one of the + permutations of *iterable*. + """ + index = 0 + pool = list(iterable) + for i, x in zip(range(len(pool), -1, -1), element): + r = pool.index(x) + index = index * i + r + del pool[r] + + return index diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/more_itertools/recipes.py new/more-itertools-8.7.0/more_itertools/recipes.py --- old/more-itertools-8.6.0/more_itertools/recipes.py 2020-10-27 21:53:24.000000000 +0100 +++ new/more-itertools-8.7.0/more_itertools/recipes.py 2021-02-06 14:41:54.000000000 +0100 @@ -27,6 +27,7 @@ __all__ = [ 'all_equal', 'consume', + 'convolve', 'dotproduct', 'first_true', 'flatten', @@ -36,6 +37,7 @@ 'nth', 'nth_combination', 'padnone', + 'pad_none', 'pairwise', 'partition', 'powerset', @@ -177,10 +179,10 @@ return sum(map(pred, iterable)) -def padnone(iterable): +def pad_none(iterable): """Returns the sequence of elements and then returns ``None`` indefinitely. - >>> take(5, padnone(range(3))) + >>> take(5, pad_none(range(3))) [0, 1, 2, None, None] Useful for emulating the behavior of the built-in :func:`map` function. @@ -191,6 +193,9 @@ return chain(iterable, repeat(None)) +padnone = pad_none + + def ncycles(iterable, n): """Returns the sequence elements *n* times @@ -250,16 +255,30 @@ return starmap(func, repeat(args, times)) -def pairwise(iterable): +def _pairwise(iterable): """Returns an iterator of paired items, overlapping, from the original >>> take(4, pairwise(count())) [(0, 1), (1, 2), (2, 3), (3, 4)] + On Python 3.10 and above, this is an alias for :func:`itertools.pairwise`. + """ a, b = tee(iterable) next(b, None) - return zip(a, b) + yield from zip(a, b) + + +try: + from itertools import pairwise as itertools_pairwise +except ImportError: + pairwise = _pairwise +else: + + def pairwise(iterable): + yield from itertools_pairwise(iterable) + + pairwise.__doc__ = _pairwise.__doc__ def grouper(iterable, n, fillvalue=None): @@ -382,12 +401,13 @@ ``key=lambda x: frozenset(x.items())`` can be used. """ + key = key if key is not None else lambda x: x seenset = set() seenset_add = seenset.add seenlist = [] seenlist_add = seenlist.append - iterable, keys = tee(iterable) - for element, k in zip(iterable, map(key, keys) if key else keys): + for element in iterable: + k = key(element) try: if k not in seenset: seenset_add(k) @@ -530,6 +550,12 @@ sort position *index* directly, without computing the previous subsequences. + >>> nth_combination(range(5), 3, 5) + (0, 3, 4) + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. """ pool = tuple(iterable) n = len(pool) @@ -566,7 +592,28 @@ >>> list(prepend(value, iterator)) ['0', '1', '2', '3'] - To prepend multiple values, see :func:`itertools.chain`. + To prepend multiple values, see :func:`itertools.chain` + or :func:`value_chain`. """ return chain([value], iterator) + + +def convolve(signal, kernel): + """Convolve the iterable *signal* with the iterable *kernel*. + + >>> signal = (1, 2, 3, 4, 5) + >>> kernel = [3, 2, 1] + >>> list(convolve(signal, kernel)) + [3, 8, 14, 20, 26, 14, 5] + + Note: the input arguments are not interchangeable, as the *kernel* + is immediately consumed and stored. + + """ + kernel = tuple(kernel)[::-1] + n = len(kernel) + window = deque([0], maxlen=n) * n + for x in chain(signal, repeat(0, n - 1)): + window.append(x) + yield sum(map(operator.mul, kernel, window)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/more_itertools.egg-info/PKG-INFO new/more-itertools-8.7.0/more_itertools.egg-info/PKG-INFO --- old/more-itertools-8.6.0/more_itertools.egg-info/PKG-INFO 2020-10-30 14:50:39.000000000 +0100 +++ new/more-itertools-8.7.0/more_itertools.egg-info/PKG-INFO 2021-02-08 02:52:59.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: more-itertools -Version: 8.6.0 +Version: 8.7.0 Summary: More routines for operating on iterables, beyond itertools Home-page: https://github.com/more-itertools/more-itertools Author: Erik Rose @@ -62,9 +62,11 @@ | | `zip_offset <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.zip_offset>`_, | | | `zip_equal <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.zip_equal>`_, | | | `dotproduct <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.dotproduct>`_, | + | | `convolve <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.convolve>`_, | | | `flatten <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.flatten>`_, | | | `roundrobin <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.roundrobin>`_, | - | | `prepend <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.prepend>`_ | + | | `prepend <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.prepend>`_, | + | | `value_chain <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.value_chain>`_ | +------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Summarizing | `ilen <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.ilen>`_, | | | `unique_to_each <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.unique_to_each>`_, | @@ -101,6 +103,9 @@ | | `circular_shifts <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.circular_shifts>`_, | | | `partitions <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.partitions>`_, | | | `set_partitions <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.set_partitions>`_, | + | | `product_index <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.product_index>`_, | + | | `combination_index <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.combination_index>`_, | + | | `permutation_index <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.permutation_index>`_, | | | `powerset <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.powerset>`_, | | | `random_product <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.random_product>`_, | | | `random_permutation <https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.random_permutation>`_, | @@ -179,7 +184,7 @@ Blog posts about ``more-itertools``: * `Yo, I heard you like decorators <https://www.bbayles.com/index/decorator_factory>`__ - * `Tour of Python Itertools <https://martinheinz.dev/blog/16>`__ + * `Tour of Python Itertools <https://martinheinz.dev/blog/16>`__ (`Alternate <https://dev.to/martinheinz/tour-of-python-itertools-4122>`__) Development @@ -197,6 +202,26 @@ :noindex: + 8.7.0 + ----- + + * New functions + * convolve (from the Python itertools docs) + * product_index, combination_index, and permutation_index (thanks to N8Brooks) + * value_chain (thanks to jenstroeger) + + * Changes to existing functions + * distinct_combinations now uses a non-recursive algorithm (thanks to knutdrand) + * pad_none is now the preferred name for padnone, though the latter remains available. + * pairwise will now use the Python standard library implementation on Python 3.10+ + * sort_together now accepts a ``key`` argument (thanks to brianmaissy) + * seekable now has a ``peek`` method, and can indicate whether the iterator it's wrapping is exhausted (thanks to gsakkis) + * time_limited can now indicate whether its iterator has expired (thanks to roysmith) + * The implementation of unique_everseen was improved (thanks to plammens) + + * Other changes: + * Various documentation updates (thanks to cthoyt, Evantm, and cyphase) + 8.6.0 ----- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/setup.cfg new/more-itertools-8.7.0/setup.cfg --- old/more-itertools-8.6.0/setup.cfg 2020-10-30 14:50:39.324085500 +0100 +++ new/more-itertools-8.7.0/setup.cfg 2021-02-08 02:52:59.821831700 +0100 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 8.6.0 +current_version = 8.7.0 commit = True tag = False files = more_itertools/__init__.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/tests/test_more.py new/more-itertools-8.7.0/tests/test_more.py --- old/more-itertools-8.6.0/tests/test_more.py 2020-10-16 23:23:26.000000000 +0200 +++ new/more-itertools-8.7.0/tests/test_more.py 2021-02-06 14:41:48.000000000 +0100 @@ -223,15 +223,22 @@ self.assertRaises(ValueError, lambda: mi.nth_or_last(range(0), 0)) -class PeekableTests(TestCase): - """Tests for ``peekable()`` behavor not incidentally covered by testing - ``collate()`` +class PeekableMixinTests: + """Common tests for ``peekable()`` and ``seekable()`` behavior""" - """ + cls = None + + def test_passthrough(self): + """Iterating a peekable without using ``peek()`` or ``prepend()`` + should just give the underlying iterable's elements (a trivial test but + useful to set a baseline in case something goes wrong)""" + expected = [1, 2, 3, 4, 5] + actual = list(self.cls(expected)) + self.assertEqual(actual, expected) def test_peek_default(self): """Make sure passing a default into ``peek()`` works.""" - p = mi.peekable([]) + p = self.cls([]) self.assertEqual(p.peek(7), 7) def test_truthiness(self): @@ -239,10 +246,10 @@ the iterable. """ - p = mi.peekable([]) + p = self.cls([]) self.assertFalse(p) - p = mi.peekable(range(3)) + p = self.cls(range(3)) self.assertTrue(p) def test_simple_peeking(self): @@ -250,11 +257,21 @@ iterator, respectively. """ - p = mi.peekable(range(10)) + p = self.cls(range(10)) self.assertEqual(next(p), 0) self.assertEqual(p.peek(), 1) + self.assertEqual(p.peek(), 1) self.assertEqual(next(p), 1) + +class PeekableTests(PeekableMixinTests, TestCase): + """Tests for ``peekable()`` behavior not incidentally covered by testing + ``collate()`` + + """ + + cls = mi.peekable + def test_indexing(self): """ Indexing into the peekable shouldn't advance the iterator. @@ -334,14 +351,6 @@ self.assertEqual(old_cache, list(p._cache)) self.assertEqual(list(p), list(iterable)) - def test_passthrough(self): - """Iterating a peekable without using ``peek()`` or ``prepend()`` - should just give the underlying iterable's elements (a trivial test but - useful to set a baseline in case something goes wrong)""" - expected = [1, 2, 3, 4, 5] - actual = list(mi.peekable(expected)) - self.assertEqual(actual, expected) - # prepend() behavior tests def test_prepend(self): @@ -1920,6 +1929,44 @@ IndexError, lambda: mi.sort_together(iterables, key_list=(5,)) ) + def test_key_function(self): + """tests `key` function, including interaction with `key_list`""" + iterables = [ + ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'], + ['May', 'Aug.', 'May', 'June', 'July', 'July'], + [97, 20, 100, 70, 100, 20], + ] + self.assertEqual( + mi.sort_together(iterables, key=lambda x: x), + [ + ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'), + ('June', 'July', 'July', 'May', 'Aug.', 'May'), + (70, 100, 20, 97, 20, 100), + ], + ) + self.assertEqual( + mi.sort_together(iterables, key=lambda x: x[::-1]), + [ + ('GA', 'GA', 'GA', 'CT', 'CT', 'CT'), + ('May', 'Aug.', 'May', 'June', 'July', 'July'), + (97, 20, 100, 70, 100, 20), + ], + ) + self.assertEqual( + mi.sort_together( + iterables, + key_list=(0, 2), + key=lambda state, number: number + if state == 'CT' + else 2 * number, + ), + [ + ('CT', 'GA', 'CT', 'CT', 'GA', 'GA'), + ('July', 'Aug.', 'June', 'July', 'May', 'May'), + (20, 20, 70, 100, 97, 100), + ], + ) + def test_reverse(self): """tests `reverse` to ensure a reverse sort for `key_list` iterables""" iterables = [ @@ -3045,7 +3092,9 @@ self.assertEqual(actual, original) -class SeekableTest(TestCase): +class SeekableTest(PeekableMixinTests, TestCase): + cls = mi.seekable + def test_exhaustion_reset(self): iterable = [str(n) for n in range(10)] @@ -3663,16 +3712,25 @@ sleep(0.2) yield 3 - iterable = generator() - actual = list(mi.time_limited(0.1, iterable)) + iterable = mi.time_limited(0.1, generator()) + actual = list(iterable) expected = [1, 2] self.assertEqual(actual, expected) + self.assertTrue(iterable.timed_out) + + def test_complete(self): + iterable = mi.time_limited(2, iter(range(10))) + actual = list(iterable) + expected = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + self.assertEqual(actual, expected) + self.assertFalse(iterable.timed_out) def test_zero_limit(self): - iterable = count() - actual = list(mi.time_limited(0, iterable)) + iterable = mi.time_limited(0, count()) + actual = list(iterable) expected = [] self.assertEqual(actual, expected) + self.assertTrue(iterable.timed_out) def test_invalid_limit(self): with self.assertRaises(ValueError): @@ -4134,3 +4192,174 @@ def test_invalid_index(self): with self.assertRaises(IndexError): mi.nth_product(24, 'ab', 'cde', 'fghi') + + +class ValueChainTests(TestCase): + def test_empty(self): + actual = list(mi.value_chain()) + expected = [] + self.assertEqual(actual, expected) + + def test_simple(self): + actual = list(mi.value_chain(1, 2.71828, False, 'foo')) + expected = [1, 2.71828, False, 'foo'] + self.assertEqual(actual, expected) + + def test_more(self): + actual = list(mi.value_chain(b'bar', [1, 2, 3], 4, {'key': 1})) + expected = [b'bar', 1, 2, 3, 4, 'key'] + self.assertEqual(actual, expected) + + def test_empty_lists(self): + actual = list(mi.value_chain(1, 2, [], [3, 4])) + expected = [1, 2, 3, 4] + self.assertEqual(actual, expected) + + def test_complex(self): + obj = object() + actual = list( + mi.value_chain( + (1, (2, (3,))), + ['foo', ['bar', ['baz']], 'tic'], + {'key': {'foo': 1}}, + obj, + ) + ) + expected = [1, (2, (3,)), 'foo', ['bar', ['baz']], 'tic', 'key', obj] + self.assertEqual(actual, expected) + + +class ProductIndexTests(TestCase): + def test_basic(self): + iterables = ['ab', 'cdef', 'ghi'] + first_index = {} + for index, element in enumerate(product(*iterables)): + actual = mi.product_index(element, *iterables) + expected = first_index.setdefault(element, index) + self.assertEqual(actual, expected) + + def test_multiplicity(self): + iterables = ['ab', 'bab', 'cab'] + first_index = {} + for index, element in enumerate(product(*iterables)): + actual = mi.product_index(element, *iterables) + expected = first_index.setdefault(element, index) + self.assertEqual(actual, expected) + + def test_long(self): + actual = mi.product_index((1, 3, 12), range(101), range(22), range(53)) + expected = 1337 + self.assertEqual(actual, expected) + + def test_invalid_empty(self): + with self.assertRaises(ValueError): + mi.product_index('', 'ab', 'cde', 'fghi') + + def test_invalid_small(self): + with self.assertRaises(ValueError): + mi.product_index('ac', 'ab', 'cde', 'fghi') + + def test_invalid_large(self): + with self.assertRaises(ValueError): + mi.product_index('achi', 'ab', 'cde', 'fghi') + + def test_invalid_match(self): + with self.assertRaises(ValueError): + mi.product_index('axf', 'ab', 'cde', 'fghi') + + +class CombinationIndexTests(TestCase): + def test_r_less_than_n(self): + iterable = 'abcdefg' + r = 4 + first_index = {} + for index, element in enumerate(combinations(iterable, r)): + actual = mi.combination_index(element, iterable) + expected = first_index.setdefault(element, index) + self.assertEqual(actual, expected) + + def test_r_equal_to_n(self): + iterable = 'abcd' + r = len(iterable) + first_index = {} + for index, element in enumerate(combinations(iterable, r=r)): + actual = mi.combination_index(element, iterable) + expected = first_index.setdefault(element, index) + self.assertEqual(actual, expected) + + def test_multiplicity(self): + iterable = 'abacba' + r = 3 + first_index = {} + for index, element in enumerate(combinations(iterable, r)): + actual = mi.combination_index(element, iterable) + expected = first_index.setdefault(element, index) + self.assertEqual(actual, expected) + + def test_null(self): + actual = mi.combination_index(tuple(), []) + expected = 0 + self.assertEqual(actual, expected) + + def test_long(self): + actual = mi.combination_index((2, 12, 35, 126), range(180)) + expected = 2000000 + self.assertEqual(actual, expected) + + def test_invalid_order(self): + with self.assertRaises(ValueError): + mi.combination_index(tuple('acb'), 'abcde') + + def test_invalid_large(self): + with self.assertRaises(ValueError): + mi.combination_index(tuple('abcdefg'), 'abcdef') + + def test_invalid_match(self): + with self.assertRaises(ValueError): + mi.combination_index(tuple('axe'), 'abcde') + + +class PermutationIndexTests(TestCase): + def test_r_less_than_n(self): + iterable = 'abcdefg' + r = 4 + first_index = {} + for index, element in enumerate(permutations(iterable, r)): + actual = mi.permutation_index(element, iterable) + expected = first_index.setdefault(element, index) + self.assertEqual(actual, expected) + + def test_r_equal_to_n(self): + iterable = 'abcd' + first_index = {} + for index, element in enumerate(permutations(iterable)): + actual = mi.permutation_index(element, iterable) + expected = first_index.setdefault(element, index) + self.assertEqual(actual, expected) + + def test_multiplicity(self): + iterable = 'abacba' + r = 3 + first_index = {} + for index, element in enumerate(permutations(iterable, r)): + actual = mi.permutation_index(element, iterable) + expected = first_index.setdefault(element, index) + self.assertEqual(actual, expected) + + def test_null(self): + actual = mi.permutation_index(tuple(), []) + expected = 0 + self.assertEqual(actual, expected) + + def test_long(self): + actual = mi.permutation_index((2, 12, 35, 126), range(180)) + expected = 11631678 + self.assertEqual(actual, expected) + + def test_invalid_large(self): + with self.assertRaises(ValueError): + mi.permutation_index(tuple('abcdefg'), 'abcdef') + + def test_invalid_match(self): + with self.assertRaises(ValueError): + mi.permutation_index(tuple('axe'), 'abcde') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.6.0/tests/test_recipes.py new/more-itertools-8.7.0/tests/test_recipes.py --- old/more-itertools-8.6.0/tests/test_recipes.py 2020-10-27 21:50:38.000000000 +0100 +++ new/more-itertools-8.7.0/tests/test_recipes.py 2021-01-10 03:27:24.000000000 +0100 @@ -1,7 +1,7 @@ import warnings from doctest import DocTestSuite -from itertools import combinations, permutations +from itertools import combinations, count, permutations from math import factorial from unittest import TestCase @@ -166,13 +166,14 @@ class PadnoneTests(TestCase): - """Tests for ``padnone()``""" - - def test_happy_path(self): - """wrapper iterator should return None indefinitely""" - r = range(2) - p = mi.padnone(r) - self.assertEqual([0, 1, None, None], [next(p) for _ in range(4)]) + def test_basic(self): + iterable = range(2) + for func in (mi.pad_none, mi.padnone): + with self.subTest(func=func): + p = func(iterable) + self.assertEqual( + [0, 1, None, None], [next(p) for _ in range(4)] + ) class NcyclesTests(TestCase): @@ -654,3 +655,33 @@ actual = tuple(mi.prepend(value, iterator)) expected = ('ab',) + tuple('cdefg') self.assertEqual(actual, expected) + + +class Convolvetests(TestCase): + def test_moving_average(self): + signal = iter([10, 20, 30, 40, 50]) + kernel = [0.5, 0.5] + actual = list(mi.convolve(signal, kernel)) + expected = [ + (10 + 0) / 2, + (20 + 10) / 2, + (30 + 20) / 2, + (40 + 30) / 2, + (50 + 40) / 2, + (0 + 50) / 2, + ] + self.assertEqual(actual, expected) + + def test_derivative(self): + signal = iter([10, 20, 30, 40, 50]) + kernel = [1, -1] + actual = list(mi.convolve(signal, kernel)) + expected = [10 - 0, 20 - 10, 30 - 20, 40 - 30, 50 - 40, 0 - 50] + self.assertEqual(actual, expected) + + def test_infinite_signal(self): + signal = count() + kernel = [1, -1] + actual = mi.take(5, mi.convolve(signal, kernel)) + expected = [0, 1, 1, 1, 1] + self.assertEqual(actual, expected)