Hello community, here is the log from the commit of package python-more-itertools for openSUSE:Factory checked in at 2020-10-25 18:08:22 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-more-itertools (Old) and /work/SRC/openSUSE:Factory/.python-more-itertools.new.3463 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-more-itertools" Sun Oct 25 18:08:22 2020 rev:12 rq:834878 version:8.5.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-more-itertools/python-more-itertools.changes 2020-08-13 10:15:47.766698836 +0200 +++ /work/SRC/openSUSE:Factory/.python-more-itertools.new.3463/python-more-itertools.changes 2020-10-25 18:08:29.907451950 +0100 @@ -1,0 +2,12 @@ +Wed Sep 16 11:13:30 UTC 2020 - Dirk Mueller <dmuel...@suse.com> + +- update to 8.5.0: + * windowed_complete() (thanks to MarcinKonowalczyk) + Changes to existing itertools: + * The is_sorted() implementation was improved (thanks to cool-RR) + * The groupby_transform() now accepts a reducefunc parameter. + * The last() implementation was improved (thanks to brianmaissy) + * Various documentation fixes (thanks to craigrosie, samuelstjean, PiCT0) + * The tests for distinct_combinations() were improved (thanks to Minabsapi) + +------------------------------------------------------------------- Old: ---- more-itertools-8.4.0.tar.gz New: ---- more-itertools-8.5.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-more-itertools.spec ++++++ --- /var/tmp/diff_new_pack.GgeXDS/_old 2020-10-25 18:08:31.571453524 +0100 +++ /var/tmp/diff_new_pack.GgeXDS/_new 2020-10-25 18:08:31.575453528 +0100 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-more-itertools -Version: 8.4.0 +Version: 8.5.0 Release: 0 Summary: More routines for operating on iterables, beyond itertools License: MIT ++++++ more-itertools-8.4.0.tar.gz -> more-itertools-8.5.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.4.0/PKG-INFO new/more-itertools-8.5.0/PKG-INFO --- old/more-itertools-8.4.0/PKG-INFO 2020-06-14 13:31:51.462923300 +0200 +++ new/more-itertools-8.5.0/PKG-INFO 2020-08-28 20:32:44.358470200 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: more-itertools -Version: 8.4.0 +Version: 8.5.0 Summary: More routines for operating on iterables, beyond itertools Home-page: https://github.com/more-itertools/more-itertools Author: Erik Rose @@ -13,9 +13,6 @@ .. image:: https://readthedocs.org/projects/more-itertools/badge/?version=latest :target: https://more-itertools.readthedocs.io/en/stable/ - .. image:: https://coveralls.io/repos/github/more-itertools/more-itertools/badge.svg?branch=master - :target: https://coveralls.io/github/more-itertools/more-itertools?branch=master - Python's ``itertools`` library is a gem - you can compose elegant solutions for a variety of problems with the functions it provides. In ``more-itertools`` we collect additional building blocks, recipes, and routines for working with @@ -177,7 +174,7 @@ Blog posts about ``more-itertools``: - * `Yo, I heard you like decorators <https://bbayles.com/index/decorator_factory>`__ + * `Yo, I heard you like decorators <https://www.bbayles.com/index/decorator_factory>`__ * `Tour of Python Itertools <https://martinheinz.dev/blog/16>`__ @@ -196,6 +193,31 @@ :noindex: + 8.5.0 + ----- + + + * New itertools + * windowed_complete (thanks to MarcinKonowalczyk) + + * Changes to existing itertools: + * The is_sorted implementation was improved (thanks to cool-RR) + * The groupby_transform now accepts a ``reducefunc`` parameter. + * The last implementation was improved (thanks to brianmaissy) + + * Other changes + * Various documentation fixes (thanks to craigrosie, samuelstjean, PiCT0) + * The tests for distinct_combinations were improved (thanks to Minabsapi) + * Automated tests now run on GitHub Actions. All commits now check: + * That unit tests pass + * That the examples in docstrings work + * That test coverage remains high (using `coverage`) + * For linting errors (using `flake8`) + * For consistent style (using `black`) + * That the type stubs work (using `mypy`) + * That the docs build correctly (using `sphinx`) + * That packages build correctly (using `twine`) + 8.4.0 ----- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.4.0/README.rst new/more-itertools-8.5.0/README.rst --- old/more-itertools-8.4.0/README.rst 2020-06-14 13:31:22.000000000 +0200 +++ new/more-itertools-8.5.0/README.rst 2020-08-28 20:08:58.000000000 +0200 @@ -5,9 +5,6 @@ .. image:: https://readthedocs.org/projects/more-itertools/badge/?version=latest :target: https://more-itertools.readthedocs.io/en/stable/ -.. image:: https://coveralls.io/repos/github/more-itertools/more-itertools/badge.svg?branch=master - :target: https://coveralls.io/github/more-itertools/more-itertools?branch=master - Python's ``itertools`` library is a gem - you can compose elegant solutions for a variety of problems with the functions it provides. In ``more-itertools`` we collect additional building blocks, recipes, and routines for working with @@ -169,7 +166,7 @@ Blog posts about ``more-itertools``: -* `Yo, I heard you like decorators <https://bbayles.com/index/decorator_factory>`__ +* `Yo, I heard you like decorators <https://www.bbayles.com/index/decorator_factory>`__ * `Tour of Python Itertools <https://martinheinz.dev/blog/16>`__ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.4.0/docs/api.rst new/more-itertools-8.5.0/docs/api.rst --- old/more-itertools-8.4.0/docs/api.rst 2020-06-14 13:31:22.000000000 +0200 +++ new/more-itertools-8.5.0/docs/api.rst 2020-08-28 20:08:58.000000000 +0200 @@ -62,6 +62,7 @@ .. autofunction:: substrings .. autofunction:: substrings_indexes .. autofunction:: stagger +.. autofunction:: windowed_complete ---- @@ -224,6 +225,7 @@ .. autofunction:: always_reversible .. autofunction:: consumer .. autofunction:: with_iter +.. autoclass:: callback_iter ---- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.4.0/docs/versions.rst new/more-itertools-8.5.0/docs/versions.rst --- old/more-itertools-8.4.0/docs/versions.rst 2020-06-14 13:31:22.000000000 +0200 +++ new/more-itertools-8.5.0/docs/versions.rst 2020-08-28 20:22:12.000000000 +0200 @@ -5,6 +5,31 @@ .. automodule:: more_itertools :noindex: +8.5.0 +----- + + +* New itertools + * :func:`windowed_complete` (thanks to MarcinKonowalczyk) + +* Changes to existing itertools: + * The :func:`is_sorted` implementation was improved (thanks to cool-RR) + * The :func:`groupby_transform` now accepts a ``reducefunc`` parameter. + * The :func:`last` implementation was improved (thanks to brianmaissy) + +* Other changes + * Various documentation fixes (thanks to craigrosie, samuelstjean, PiCT0) + * The tests for :func:`distinct_combinations` were improved (thanks to Minabsapi) + * Automated tests now run on GitHub Actions. All commits now check: + * That unit tests pass + * That the examples in docstrings work + * That test coverage remains high (using `coverage`) + * For linting errors (using `flake8`) + * For consistent style (using `black`) + * That the type stubs work (using `mypy`) + * That the docs build correctly (using `sphinx`) + * That packages build correctly (using `twine`) + 8.4.0 ----- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.4.0/more_itertools/__init__.py new/more-itertools-8.5.0/more_itertools/__init__.py --- old/more-itertools-8.4.0/more_itertools/__init__.py 2020-06-14 13:31:22.000000000 +0200 +++ new/more-itertools-8.5.0/more_itertools/__init__.py 2020-08-28 20:22:42.000000000 +0200 @@ -1,4 +1,4 @@ from .more import * # noqa from .recipes import * # noqa -__version__ = '8.4.0' +__version__ = '8.5.0' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.4.0/more_itertools/more.py new/more-itertools-8.5.0/more_itertools/more.py --- old/more-itertools-8.4.0/more_itertools/more.py 2020-06-14 13:31:22.000000000 +0200 +++ new/more-itertools-8.5.0/more_itertools/more.py 2020-08-28 20:29:13.000000000 +0200 @@ -1,6 +1,8 @@ import warnings + from collections import Counter, defaultdict, deque, abc from collections.abc import Sequence +from concurrent.futures import ThreadPoolExecutor from functools import partial, wraps from heapq import merge, heapify, heapreplace, heappop from itertools import ( @@ -18,9 +20,10 @@ zip_longest, ) from math import exp, floor, log +from queue import Empty, Queue from random import random, randrange, uniform from operator import itemgetter, sub, gt, lt -from sys import maxsize +from sys import hexversion, maxsize from time import monotonic from .recipes import ( @@ -33,10 +36,12 @@ ) __all__ = [ + 'AbortThread', 'adjacent', 'always_iterable', 'always_reversible', 'bucket', + 'callback_iter', 'chunked', 'circular_shifts', 'collapse', @@ -105,6 +110,7 @@ 'UnequalIterablesError', 'zip_equal', 'zip_offset', + 'windowed_complete', ] _marker = object() @@ -175,18 +181,19 @@ raise ``ValueError``. """ try: - try: - # Try to access the last item directly + if isinstance(iterable, Sequence): return iterable[-1] - except (TypeError, AttributeError, KeyError): - # If not slice-able, iterate entirely using length-1 deque - return deque(iterable, maxlen=1)[0] - except IndexError as e: # If the iterable was empty + # Work around https://bugs.python.org/issue38525 + elif hasattr(iterable, '__reversed__') and (hexversion != 0x030800F0): + return next(reversed(iterable)) + else: + return deque(iterable, maxlen=1)[-1] + except (IndexError, TypeError, StopIteration): if default is _marker: raise ValueError( - 'last() was called on an empty iterable, and no ' - 'default value was provided.' - ) from e + 'last() was called on an empty iterable, and no default was ' + 'provided.' + ) return default @@ -260,7 +267,7 @@ >>> if p: # peekable has items ... list(p) ['a', 'b'] - >>> if not p: # peekable is exhaused + >>> if not p: # peekable is exhausted ... list(p) [] @@ -462,9 +469,9 @@ def iterate(func, start): """Return ``start``, ``func(start)``, ``func(func(start))``, ... - >>> from itertools import islice - >>> list(islice(iterate(lambda x: 2*x, 1), 10)) - [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] + >>> from itertools import islice + >>> list(islice(iterate(lambda x: 2*x, 1), 10)) + [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] """ while True: @@ -572,8 +579,10 @@ If *r* is given, only the *r*-length permutations are yielded. - >>> sorted(distinct_permutations([1, 0, 1], r=3)) - [(0, 1, 1), (1, 0, 1), (1, 1, 0)] + >>> sorted(distinct_permutations([1, 0, 1], r=2)) + [(0, 1), (1, 0), (1, 1)] + >>> sorted(distinct_permutations(range(3), r=2)) + [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] """ # Algorithm: https://w.wiki/Qai @@ -1761,21 +1770,23 @@ return zip(adjacent_to_selected, i2) -def groupby_transform(iterable, keyfunc=None, valuefunc=None): - """An extension of :func:`itertools.groupby` that transforms the values of - *iterable* after grouping them. - *keyfunc* is a function used to compute a grouping key for each item. - *valuefunc* is a function for transforming the items after grouping. +def groupby_transform(iterable, keyfunc=None, valuefunc=None, reducefunc=None): + """An extension of :func:`itertools.groupby` that can apply transformations + to the grouped data. + + * *keyfunc* is a function computing a key value for each item in *iterable* + * *valuefunc* is a function that transforms the individual items from + *iterable* after grouping + * *reducefunc* is a function that transforms each group of items + + >>> iterable = 'aAAbBBcCC' + >>> keyfunc = lambda k: k.upper() + >>> valuefunc = lambda v: v.lower() + >>> reducefunc = lambda g: ''.join(g) + >>> list(groupby_transform(iterable, keyfunc, valuefunc, reducefunc)) + [('A', 'aaa'), ('B', 'bbb'), ('C', 'ccc')] - >>> iterable = 'AaaABbBCcA' - >>> keyfunc = lambda x: x.upper() - >>> valuefunc = lambda x: x.lower() - >>> grouper = groupby_transform(iterable, keyfunc, valuefunc) - >>> [(k, ''.join(g)) for k, g in grouper] - [('A', 'aaaa'), ('B', 'bbb'), ('C', 'cc'), ('A', 'a')] - - *keyfunc* and *valuefunc* default to identity functions if they are not - specified. + Each optional argument defaults to an identity function if not specified. :func:`groupby_transform` is useful when grouping elements of an iterable using a separate iterable as the key. To do this, :func:`zip` the iterables @@ -1795,8 +1806,13 @@ duplicate groups, you should sort the iterable by the key function. """ - res = groupby(iterable, keyfunc) - return ((k, map(valuefunc, g)) for k, g in res) if valuefunc else res + ret = groupby(iterable, keyfunc) + if valuefunc: + ret = ((k, map(valuefunc, g)) for k, g in ret) + if reducefunc: + ret = ((k, reducefunc(g)) for k, g in ret) + + return ret class numeric_range(abc.Sequence, abc.Hashable): @@ -2632,8 +2648,8 @@ def circular_shifts(iterable): """Return a list of circular shifts of *iterable*. - >>> circular_shifts(range(4)) - [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] + >>> circular_shifts(range(4)) + [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] """ lst = list(iterable) return take(len(lst), windowed(cycle(lst), len(lst))) @@ -2876,7 +2892,7 @@ def partitions(iterable): - """Yield all possible order-perserving partitions of *iterable*. + """Yield all possible order-preserving partitions of *iterable*. >>> iterable = 'abc' >>> for part in partitions(iterable): @@ -3239,8 +3255,171 @@ compare = lt if reverse else gt it = iterable if (key is None) else map(key, iterable) - for a, b in pairwise(it): - if compare(a, b): + return not any(starmap(compare, pairwise(it))) + + +class AbortThread(BaseException): + pass + + +class callback_iter: + """Convert a function that uses callbacks to an iterator. + + Let *func* be a function that takes a `callback` keyword argument. + For example: + + >>> def func(callback=None): + ... for i, c in [(1, 'a'), (2, 'b'), (3, 'c')]: + ... if callback: + ... callback(i, c) + ... return 4 + + + Use ``with callback_iter(func)`` to get an iterator over the parameters + that are delivered to the callback. + + >>> with callback_iter(func) as it: + ... for args, kwargs in it: + ... print(args) + (1, 'a') + (2, 'b') + (3, 'c') + + The function will be called in a background thread. The ``done`` property + indicates whether it has completed execution. + + >>> it.done + True + + If it completes successfully, its return value will be available + in the ``result`` property. + + >>> it.result + 4 + + Notes: + + * If the function uses some keyword argument besides ``callback``, supply + *callback_kwd*. + * If it finished executing, but raised an exception, accessing the + ``result`` property will raise the same exception. + * If it hasn't finished executing, accessing the ``result`` + property from within the ``with`` block will raise ``RuntimeError``. + * If it hasn't finished executing, accessing the ``result`` property from + outside the ``with`` block will raise a + ``more_itertools.AbortThread`` exception. + * Provide *wait_seconds* to adjust how frequently the it is polled for + output. + + """ + + def __init__(self, func, callback_kwd='callback', wait_seconds=0.1): + self._func = func + self._callback_kwd = callback_kwd + self._aborted = False + self._future = None + self._wait_seconds = wait_seconds + self._executor = ThreadPoolExecutor(max_workers=1) + self._iterator = self._reader() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._aborted = True + self._executor.shutdown() + + def __iter__(self): + return self + + def __next__(self): + return next(self._iterator) + + @property + def done(self): + if self._future is None: return False + return self._future.done() + + @property + def result(self): + if not self.done: + raise RuntimeError('Function has not yet completed') + + return self._future.result() + + def _reader(self): + q = Queue() + + def callback(*args, **kwargs): + if self._aborted: + raise AbortThread('canceled by user') + + q.put((args, kwargs)) + + self._future = self._executor.submit( + self._func, **{self._callback_kwd: callback} + ) + + while True: + try: + item = q.get(timeout=self._wait_seconds) + except Empty: + pass + else: + q.task_done() + yield item + + if self._future.done(): + break + + remaining = [] + while True: + try: + item = q.get_nowait() + except Empty: + break + else: + q.task_done() + remaining.append(item) + q.join() + yield from remaining + + +def windowed_complete(iterable, n): + """ + Yield ``(beginning, middle, end)`` tuples, where: + + * Each ``middle`` has *n* items from *iterable* + * Each ``beginning`` has the items before the ones in ``middle`` + * Each ``end`` has the items after the ones in ``middle`` + + >>> iterable = range(7) + >>> n = 3 + >>> for beginning, middle, end in windowed_complete(iterable, n): + ... print(beginning, middle, end) + () (0, 1, 2) (3, 4, 5, 6) + (0,) (1, 2, 3) (4, 5, 6) + (0, 1) (2, 3, 4) (5, 6) + (0, 1, 2) (3, 4, 5) (6,) + (0, 1, 2, 3) (4, 5, 6) () + + Notes: + * *n* must be at least 0 and most equal to the length of *iterable*. + * This function will exhaust *iterable* and store all its items. + + """ + if n < 0: + raise ValueError('n must be >= 0') + + seq = tuple(iterable) + size = len(seq) + + if n > size: + raise ValueError('n must be <= len(seq)') - return True + for i in range(size - n + 1): + beginning = seq[:i] + middle = seq[i : i + n] + end = seq[i + n :] + yield beginning, middle, end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.4.0/more_itertools/recipes.py new/more-itertools-8.5.0/more_itertools/recipes.py --- old/more-itertools-8.4.0/more_itertools/recipes.py 2020-05-18 04:01:59.000000000 +0200 +++ new/more-itertools-8.5.0/more_itertools/recipes.py 2020-08-28 20:29:12.000000000 +0200 @@ -92,9 +92,9 @@ def tail(n, iterable): """Return an iterator over the last *n* items of *iterable*. - >>> t = tail(3, 'ABCDEFG') - >>> list(t) - ['E', 'F', 'G'] + >>> t = tail(3, 'ABCDEFG') + >>> list(t) + ['E', 'F', 'G'] """ return iter(deque(iterable, maxlen=n)) @@ -143,11 +143,11 @@ def nth(iterable, n, default=None): """Returns the nth item or a default value. - >>> l = range(10) - >>> nth(l, 3) - 3 - >>> nth(l, 20, "zebra") - 'zebra' + >>> l = range(10) + >>> nth(l, 3) + 3 + >>> nth(l, 20, "zebra") + 'zebra' """ return next(islice(iterable, n, None), default) @@ -170,8 +170,8 @@ def quantify(iterable, pred=bool): """Return the how many times the predicate is true. - >>> quantify([True, False, True]) - 2 + >>> quantify([True, False, True]) + 2 """ return sum(map(pred, iterable)) @@ -194,8 +194,8 @@ def ncycles(iterable, n): """Returns the sequence elements *n* times - >>> list(ncycles(["a", "b"], 3)) - ['a', 'b', 'a', 'b', 'a', 'b'] + >>> list(ncycles(["a", "b"], 3)) + ['a', 'b', 'a', 'b', 'a', 'b'] """ return chain.from_iterable(repeat(tuple(iterable), n)) @@ -204,8 +204,8 @@ def dotproduct(vec1, vec2): """Returns the dot product of the two iterables. - >>> dotproduct([10, 10], [20, 20]) - 400 + >>> dotproduct([10, 10], [20, 20]) + 400 """ return sum(map(operator.mul, vec1, vec2)) @@ -253,8 +253,8 @@ 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)] + >>> take(4, pairwise(count())) + [(0, 1), (1, 2), (2, 3), (3, 4)] """ a, b = tee(iterable) @@ -265,8 +265,8 @@ def grouper(iterable, n, fillvalue=None): """Collect data into fixed-length chunks or blocks. - >>> list(grouper('ABCDEFG', 3, 'x')) - [('A', 'B', 'C'), ('D', 'E', 'F'), ('G', 'x', 'x')] + >>> list(grouper('ABCDEFG', 3, 'x')) + [('A', 'B', 'C'), ('D', 'E', 'F'), ('G', 'x', 'x')] """ if isinstance(iterable, int): @@ -401,10 +401,10 @@ def unique_justseen(iterable, key=None): """Yields elements in order, ignoring serial duplicates - >>> list(unique_justseen('AAAABBBCCDAABBB')) - ['A', 'B', 'C', 'D', 'A', 'B'] - >>> list(unique_justseen('ABBCcAD', str.lower)) - ['A', 'B', 'C', 'A', 'D'] + >>> list(unique_justseen('AAAABBBCCDAABBB')) + ['A', 'B', 'C', 'D', 'A', 'B'] + >>> list(unique_justseen('ABBCcAD', str.lower)) + ['A', 'B', 'C', 'A', 'D'] """ return map(next, map(operator.itemgetter(1), groupby(iterable, key))) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.4.0/more_itertools.egg-info/PKG-INFO new/more-itertools-8.5.0/more_itertools.egg-info/PKG-INFO --- old/more-itertools-8.4.0/more_itertools.egg-info/PKG-INFO 2020-06-14 13:31:51.000000000 +0200 +++ new/more-itertools-8.5.0/more_itertools.egg-info/PKG-INFO 2020-08-28 20:32:44.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: more-itertools -Version: 8.4.0 +Version: 8.5.0 Summary: More routines for operating on iterables, beyond itertools Home-page: https://github.com/more-itertools/more-itertools Author: Erik Rose @@ -13,9 +13,6 @@ .. image:: https://readthedocs.org/projects/more-itertools/badge/?version=latest :target: https://more-itertools.readthedocs.io/en/stable/ - .. image:: https://coveralls.io/repos/github/more-itertools/more-itertools/badge.svg?branch=master - :target: https://coveralls.io/github/more-itertools/more-itertools?branch=master - Python's ``itertools`` library is a gem - you can compose elegant solutions for a variety of problems with the functions it provides. In ``more-itertools`` we collect additional building blocks, recipes, and routines for working with @@ -177,7 +174,7 @@ Blog posts about ``more-itertools``: - * `Yo, I heard you like decorators <https://bbayles.com/index/decorator_factory>`__ + * `Yo, I heard you like decorators <https://www.bbayles.com/index/decorator_factory>`__ * `Tour of Python Itertools <https://martinheinz.dev/blog/16>`__ @@ -196,6 +193,31 @@ :noindex: + 8.5.0 + ----- + + + * New itertools + * windowed_complete (thanks to MarcinKonowalczyk) + + * Changes to existing itertools: + * The is_sorted implementation was improved (thanks to cool-RR) + * The groupby_transform now accepts a ``reducefunc`` parameter. + * The last implementation was improved (thanks to brianmaissy) + + * Other changes + * Various documentation fixes (thanks to craigrosie, samuelstjean, PiCT0) + * The tests for distinct_combinations were improved (thanks to Minabsapi) + * Automated tests now run on GitHub Actions. All commits now check: + * That unit tests pass + * That the examples in docstrings work + * That test coverage remains high (using `coverage`) + * For linting errors (using `flake8`) + * For consistent style (using `black`) + * That the type stubs work (using `mypy`) + * That the docs build correctly (using `sphinx`) + * That packages build correctly (using `twine`) + 8.4.0 ----- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.4.0/setup.cfg new/more-itertools-8.5.0/setup.cfg --- old/more-itertools-8.4.0/setup.cfg 2020-06-14 13:31:51.462923300 +0200 +++ new/more-itertools-8.5.0/setup.cfg 2020-08-28 20:32:44.358470200 +0200 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 8.4.0 +current_version = 8.5.0 commit = True tag = False files = more_itertools/__init__.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/more-itertools-8.4.0/tests/test_more.py new/more-itertools-8.5.0/tests/test_more.py --- old/more-itertools-8.4.0/tests/test_more.py 2020-06-14 13:31:22.000000000 +0200 +++ new/more-itertools-8.5.0/tests/test_more.py 2020-08-28 20:29:16.000000000 +0200 @@ -150,13 +150,19 @@ class LastTests(TestCase): def test_basic(self): - for iterable, expected in [ + cases = [ (range(4), 3), (iter(range(4)), 3), (range(1), 0), (iter(range(1)), 0), (IterOnlyRange(5), 4), - ]: + ({n: str(n) for n in range(5)}, 4), + ] + # Versions below 3.6.0 don't have ordered dicts + if version_info >= (3, 6, 0): + cases.append(({0: '0', -1: '-1', 2: '-2'}, 2)) + + for iterable, expected in cases: with self.subTest(iterable=iterable): self.assertEqual(mi.last(iterable), expected) @@ -164,6 +170,7 @@ for iterable, default, expected in [ (range(1), None, 0), ([], None, None), + ({}, None, None), (iter([]), None, None), ]: with self.subTest(args=(iterable, default)): @@ -172,17 +179,8 @@ def test_empty(self): for iterable in ([], iter(range(0))): with self.subTest(iterable=iterable): - try: + with self.assertRaises(ValueError): mi.last(iterable) - except ValueError: - formatted_exc = format_exc() - self.assertIn('IndexError', formatted_exc) - self.assertIn( - 'The above exception was the direct cause', - formatted_exc, - ) - else: - self.fail() class NthOrLastTests(TestCase): @@ -2150,7 +2148,6 @@ class GroupByTransformTests(TestCase): def assertAllGroupsEqual(self, groupby1, groupby2): - """Compare two groupby objects for equality, both keys and groups.""" for a, b in zip(groupby1, groupby2): key1, group1 = a key2, group2 = b @@ -2160,7 +2157,6 @@ self.assertRaises(StopIteration, lambda: next(groupby2)) def test_default_funcs(self): - """Test that groupby_transform() with default args mimics groupby()""" iterable = [(x // 5, x) for x in range(1000)] actual = mi.groupby_transform(iterable) expected = groupby(iterable) @@ -2207,6 +2203,22 @@ expected = groupby(iterable, key) self.assertAllGroupsEqual(actual, expected) + def test_reducefunc(self): + iterable = range(50) + keyfunc = lambda k: 10 * (k // 10) + valuefunc = lambda v: v + 1 + reducefunc = sum + actual = list( + mi.groupby_transform( + iterable, + keyfunc=keyfunc, + valuefunc=valuefunc, + reducefunc=reducefunc, + ) + ) + expected = [(0, 55), (10, 155), (20, 255), (30, 355), (40, 455)] + self.assertEqual(actual, expected) + class NumericRangeTests(TestCase): def test_basic(self): @@ -3709,20 +3721,20 @@ class DistinctCombinationsTests(TestCase): def test_basic(self): - iterable = (1, 2, 2, 3, 3, 3) - for r in range(len(iterable)): - with self.subTest(r=r): - actual = sorted(mi.distinct_combinations(iterable, r)) - expected = sorted(set(combinations(iterable, r))) - self.assertEqual(actual, expected) - - def test_distinct(self): - iterable = list(range(6)) - for r in range(len(iterable)): - with self.subTest(r=r): - actual = list(mi.distinct_combinations(iterable, r)) - expected = list(combinations(iterable, r)) - self.assertEqual(actual, expected) + for iterable in [ + (1, 2, 2, 3, 3, 3), # In order + range(6), # All distinct + 'abbccc', # Not numbers + 'cccbba', # Backward + 'mississippi', # No particular order + ]: + for r in range(len(iterable)): + with self.subTest(iterable=iterable, r=r): + actual = list(mi.distinct_combinations(iterable, r)) + expected = list( + mi.unique_everseen(combinations(iterable, r)) + ) + self.assertEqual(actual, expected) def test_negative(self): with self.assertRaises(ValueError): @@ -3901,3 +3913,136 @@ py_result = iterable == sorted(iterable, **kwargs) self.assertEqual(mi_result, expected) self.assertEqual(mi_result, py_result) + + +class CallbackIterTests(TestCase): + def _target(self, cb=None, exc=None, wait=0): + total = 0 + for i, c in enumerate('abc', 1): + total += i + if wait: + sleep(wait) + if cb: + cb(i, c, intermediate_total=total) + if exc: + raise exc('error in target') + + return total + + def test_basic(self): + func = lambda callback=None: self._target(cb=callback, wait=0.02) + with mi.callback_iter(func, wait_seconds=0.01) as it: + # Execution doesn't start until we begin iterating + self.assertFalse(it.done) + + # Consume everything + self.assertEqual( + list(it), + [ + ((1, 'a'), {'intermediate_total': 1}), + ((2, 'b'), {'intermediate_total': 3}), + ((3, 'c'), {'intermediate_total': 6}), + ], + ) + + # After consuming everything the future is done and the + # result is available. + self.assertTrue(it.done) + self.assertEqual(it.result, 6) + + # This examines the internal state of the ThreadPoolExecutor. This + # isn't documented, so may break in future Python versions. + self.assertTrue(it._executor._shutdown) + + def test_callback_kwd(self): + with mi.callback_iter(self._target, callback_kwd='cb') as it: + self.assertEqual( + list(it), + [ + ((1, 'a'), {'intermediate_total': 1}), + ((2, 'b'), {'intermediate_total': 3}), + ((3, 'c'), {'intermediate_total': 6}), + ], + ) + + def test_partial_consumption(self): + func = lambda callback=None: self._target(cb=callback) + with mi.callback_iter(func) as it: + self.assertEqual(next(it), ((1, 'a'), {'intermediate_total': 1})) + + self.assertTrue(it._executor._shutdown) + + def test_abort(self): + func = lambda callback=None: self._target(cb=callback, wait=0.1) + with mi.callback_iter(func) as it: + self.assertEqual(next(it), ((1, 'a'), {'intermediate_total': 1})) + + with self.assertRaises(mi.AbortThread): + it.result + + def test_no_result(self): + func = lambda callback=None: self._target(cb=callback) + with mi.callback_iter(func) as it: + with self.assertRaises(RuntimeError): + it.result + + def test_exception(self): + func = lambda callback=None: self._target(cb=callback, exc=ValueError) + with mi.callback_iter(func) as it: + self.assertEqual( + next(it), + ((1, 'a'), {'intermediate_total': 1}), + ) + + with self.assertRaises(ValueError): + it.result + + +class WindowedCompleteTests(TestCase): + """Tests for ``windowed_complete()``""" + + def test_basic(self): + actual = list(mi.windowed_complete([1, 2, 3, 4, 5], 3)) + expected = [ + ((), (1, 2, 3), (4, 5)), + ((1,), (2, 3, 4), (5,)), + ((1, 2), (3, 4, 5), ()), + ] + self.assertEqual(actual, expected) + + def test_zero_length(self): + actual = list(mi.windowed_complete([1, 2, 3], 0)) + expected = [ + ((), (), (1, 2, 3)), + ((1,), (), (2, 3)), + ((1, 2), (), (3,)), + ((1, 2, 3), (), ()), + ] + self.assertEqual(actual, expected) + + def test_wrong_length(self): + seq = [1, 2, 3, 4, 5] + for n in (-10, -1, len(seq) + 1, len(seq) + 10): + with self.subTest(n=n): + with self.assertRaises(ValueError): + list(mi.windowed_complete(seq, n)) + + def test_every_partition(self): + every_partition = lambda seq: chain( + *map(partial(mi.windowed_complete, seq), range(len(seq))) + ) + + seq = 'ABC' + actual = list(every_partition(seq)) + expected = [ + ((), (), ('A', 'B', 'C')), + (('A',), (), ('B', 'C')), + (('A', 'B'), (), ('C',)), + (('A', 'B', 'C'), (), ()), + ((), ('A',), ('B', 'C')), + (('A',), ('B',), ('C',)), + (('A', 'B'), ('C',), ()), + ((), ('A', 'B'), ('C',)), + (('A',), ('B', 'C'), ()), + ] + self.assertEqual(actual, expected)