Hello community, here is the log from the commit of package python-boltons for openSUSE:Factory checked in at 2020-01-24 13:06:56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-boltons (Old) and /work/SRC/openSUSE:Factory/.python-boltons.new.26092 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-boltons" Fri Jan 24 13:06:56 2020 rev:5 rq:766377 version:20.0.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-boltons/python-boltons.changes 2019-03-11 11:15:24.841339310 +0100 +++ /work/SRC/openSUSE:Factory/.python-boltons.new.26092/python-boltons.changes 2020-01-24 13:09:56.693403722 +0100 @@ -1,0 +2,11 @@ +Wed Jan 22 15:50:52 UTC 2020 - Marketa Calabkova <[email protected]> + +- update to 20.0.0 + * New module pathutils + * add strutils.unwrap_text which does what you think to wrapped text + * iterutils.chunked to work with the bytes type + * funcutils.format_invocation for formatting simple function calls func(pos1, pos2, kw_k=kw_v) + * A bunch of small fixes and enhancements. + * many more in upstream CHANGELOG.md + +------------------------------------------------------------------- Old: ---- boltons-19.1.0.tar.gz New: ---- boltons-20.0.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-boltons.spec ++++++ --- /var/tmp/diff_new_pack.58TpSU/_old 2020-01-24 13:09:57.617404093 +0100 +++ /var/tmp/diff_new_pack.58TpSU/_new 2020-01-24 13:09:57.617404093 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-boltons # -# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2020 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,13 +18,13 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-boltons -Version: 19.1.0 +Version: 20.0.0 Release: 0 Summary: The "Boltons" utility package for Python License: BSD-3-Clause Group: Development/Languages/Python -Url: https://github.com/mahmoud/boltons -Source: https://github.com/mahmoud/boltons/archive/19.1.0.tar.gz#/boltons-%{version}.tar.gz +URL: https://github.com/mahmoud/boltons +Source: https://github.com/mahmoud/boltons/archive/%{version}.tar.gz#/boltons-%{version}.tar.gz BuildRequires: %{python_module pytest} BuildRequires: %{python_module setuptools} BuildRequires: fdupes @@ -48,7 +48,7 @@ %python_expand %fdupes %{buildroot}%{$python_sitelib} %check -%python_exec -m pytest +%pytest %files %{python_files} %license LICENSE ++++++ boltons-19.1.0.tar.gz -> boltons-20.0.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/.travis.yml new/boltons-20.0.0/.travis.yml --- old/boltons-19.1.0/.travis.yml 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/.travis.yml 2020-01-09 00:32:03.000000000 +0100 @@ -7,24 +7,23 @@ python: # Standard release https://docs.travis-ci.com/user/languages # /python#choosing-python-versions-to-test-against - - "2.6" # EOLed 5 years ago. https://www.python.org/dev/peps/pep-0361/#release-lifespan - "2.7" - "3.4" - "3.5" - "3.6" - + # PyPy2.7: https://doc.pypy.org/en/latest # /index-of-release-notes.html#cpython-2-7-compatible-versions - - pypy # Python 2.7.13 on PyPy 5.8.0 - #- pypy-6.0.0 # Python 2.7.13 on PyPy 6.0.0 # Travis needs to upgrade! + - pypy # PyPy3.5: https://doc.pypy.org/en/latest # /index-of-release-notes.html#cpython-3-3-compatible-versions - - pypy3 # Python 3.5.3 on PyPy PyPy 5.8.0-beta0 - #- pypy3-6.0.0 # Python 3.5.3 on PyPy 6.0.0 # Travis needs to upgrade! + - pypy3 matrix: include: + - python: 2.6 + dist: trusty - python: 3.7 dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069) - python: nightly # Python 3.8.0a0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/CHANGELOG.md new/boltons-20.0.0/CHANGELOG.md --- old/boltons-19.1.0/CHANGELOG.md 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/CHANGELOG.md 2020-01-09 00:32:03.000000000 +0100 @@ -1,8 +1,77 @@ boltons Changelog ================= -Since February 20, 2013 there have been 35 releases and 1333 commits for -an average of one 38-commit release every 9 weeks. +Since February 20, 2013 there have been 41 releases and 1405 commits for +an average of one 34-commit release about every 8 weeks. + + +20.0.0 +------ +*January 8, 2020* + +* New module [pathutils][pathutils]: + * [pathutils.augpath][pathutils.augpath] augments a path by modifying its components + * [pathutils.shrinkuser][pathutils.shrinkuser] inverts :func:`os.path.expanduser`. + * [pathutils.expandpath][pathutils.expandpath] shell-like environ and tilde expansion +* add `include_dirs` param to [fileutils.iter_find_files][fileutils.iter_find_files] +* Make [funcutils.format_invocation][funcutils.format_invocation] more deterministic +* add [strutils.unwrap_text][strutils.unwrap_text] which does what you think to wrapped text +* Py3 fixes + * [strutils.chunked][strutils.chunked] to work with the `bytes` type ([#231][i231]) + * [cacheutils.ThresholdCounter][cacheutils.ThresholdCounter]'s `get_common_count()` + +[i231]: https://github.com/mahmoud/boltons/issues/231 +[pathutils]: https://boltons.readthedocs.io/en/latest/pathutils.html +[pathutils.augpath]: https://boltons.readthedocs.io/en/latest/pathutils.html#boltons.pathutils.augpath +[pathutils.augpath]: https://boltons.readthedocs.io/en/latest/pathutils.html#boltons.pathutils.augpath +[pathutils.shrinkuser]: https://boltons.readthedocs.io/en/latest/pathutils.html#boltons.pathutils.shrinkuser +[pathutils.expandpath]: https://boltons.readthedocs.io/en/latest/pathutils.html#boltons.pathutils.expandpath +[strutils.unwrap_text]: https://boltons.readthedocs.io/en/latest/strutils.html#boltons.strutils.unwrap_text + +19.3.0 +------ +*(October 28, 2019)* + +Three funcutils: + +* [funcutils.format_invocation][funcutils.format_invocation] for formatting simple function calls `func(pos1, pos2, kw_k=kw_v)` +* [funcutils.format_exp_repr][funcutils.format_exp_repr] for formatting a repr like `Type(pos, kw_k=kw_v)` +* [funcutils.format_nonexp_repr][funcutils.format_nonexp_repr] for formatting a repr like `<Type k=v>` + +[funcutils.format_invocation]: https://boltons.readthedocs.io/en/latest/funcutils.html#boltons.funcutils.format_invocation +[funcutils.format_exp_repr]: https://boltons.readthedocs.io/en/latest/funcutils.html#boltons.funcutils.format_exp_repr +[funcutils.format_nonexp_repr]: https://boltons.readthedocs.io/en/latest/funcutils.html#boltons.funcutils.format_nonexp_repr + +19.2.0 +------ +*(October 19, 2019)* + +A bunch of small fixes and enhancements. + +* [tbutils.TracebackInfo][tbutils.TracebackInfo]'s from_frame now respects `level` arg +* [OrderedMultiDict.sorted()][OrderedMultiDict.sorted] now maintains all items, not just the most recent +* [setutils.complement()][setutils.complement] now supports `__rsub__` for better interop with the builtin `set` +* [FunctionBuilder][FunctionBuilder] fixed a few py3 warnings related to inspect module usage (`formatargspec`) +* [iterutils.bucketize][iterutils.bucketize] now takes a string key which works like an attribute getter, similar to other iterutils functions +* Docstring fixes across the board +* CI fixes for Travis default dist change + +[OrderedMultiDict.sorted]: http://boltons.readthedocs.org/en/latest/dictutils.html#boltons.dictutils.OrderedMultiDict.sorted + +19.1.0 +------ +*(February 28, 2019)* + +Couple of enhancements, couple of cleanups. + +* [queueutils][queueutils] now supports float-based priorities ([#204][i204]) +* [FunctionBuilder][funcutils.FunctionBuilder] has a new + `get_arg_names()` method, and its `get_defaults_dict()` method + finally includes kwonly argument defaults. +* [strutils.gzip_bytes][strutils.gzip_bytes] arrives to match + [strutils.gunzip_bytes][strutils.gunzip_bytes] + +[i204]: https://github.com/mahmoud/boltons/issues/204 19.0.1 ------ @@ -10,7 +79,7 @@ Quick release to enhance [FunctionBuilder][funcutils.FunctionBuilder] and [funcutils.wraps][funcutils.wraps] to maintain function -annotations on Python 3+. ([#133][i333], [#134][i134], [#203][i203]) +annotations on Python 3+. ([#133][i133], [#134][i134], [#203][i203]) [i133]: https://github.com/mahmoud/boltons/issues/133 [i134]: https://github.com/mahmoud/boltons/issues/134 @@ -941,6 +1010,7 @@ [mathutils.ceil]: http://boltons.readthedocs.org/en/latest/mathutils.html#boltons.mathutils.ceil [mathutils.floor]: http://boltons.readthedocs.org/en/latest/mathutils.html#boltons.mathutils.floor [mathutils.clamp]: http://boltons.readthedocs.org/en/latest/mathutils.html#boltons.mathutils.clamp +[queueutils]: http://boltons.readthedocs.org/en/latest/queueutils.html [setutils.complement]: http://boltons.readthedocs.org/en/latest/setutils.html#boltons.setutils.complement [socketutils]: http://boltons.readthedocs.org/en/latest/socketutils.html [socketutils.BufferedSocket]: http://boltons.readthedocs.org/en/latest/socketutils.html#boltons.socketutils.BufferedSocket @@ -963,6 +1033,7 @@ [strutils.args2sh]: http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.args2sh [strutils.escape_shell_args]: http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.escape_shell_args [strutils.find_hashtags]: http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.find_hashtags +[strutils.gzip_bytes]: http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.gzip_bytes [strutils.gunzip_bytes]: http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.gunzip_bytes [strutils.html2text]: http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.html2text [strutils.indent]: http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.indent diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/TODO.rst new/boltons-20.0.0/TODO.rst --- old/boltons-19.1.0/TODO.rst 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/TODO.rst 2020-01-09 00:32:03.000000000 +0100 @@ -1,6 +1,35 @@ TODO ==== [email protected]('critical', 'update campaign', verbose=True, inject_as='_act') +def update(self, _act, force=False): + +Resulted in: + +Traceback (most recent call last): +File "/home/mahmoud/virtualenvs/pacetrack/bin/pt", line 11, in <module> +load_entry_point('pacetrack', 'console_scripts', 'pt')() +File "/home/mahmoud/hatnote/pacetrack/pacetrack/cli.py", line 131, in main +cmd.run() +File "/home/mahmoud/projects/face/face/command.py", line 403, in run +ret = inject(wrapped, kwargs) +File "/home/mahmoud/projects/face/face/sinter.py", line 59, in inject +return f(**kwargs) +File "<sinter generated next_ d43eb353c6855dfc>", line 6, in next_ +File "/home/mahmoud/hatnote/pacetrack/pacetrack/cli.py", line 138, in mw_cli_log +return next_() +File "<sinter generated next_ d43eb353c6855dfc>", line 4, in next_ +File "/home/mahmoud/hatnote/pacetrack/pacetrack/cli.py", line 89, in update +return update_all(campaign_ids=posargs_, force=force, jsub=jsub, args_=args_) +File "/home/mahmoud/hatnote/pacetrack/pacetrack/cli.py", line 73, in update_all +cur_pt = load_and_update_campaign(campaign_dir, force=force) +File "/home/mahmoud/hatnote/pacetrack/pacetrack/update.py", line 622, in load_and_update_campaign +ptc.update(force=force) +File "<boltons.funcutils.FunctionBuilder-4>", line 2, in update +File "/home/mahmoud/virtualenvs/pacetrack/local/lib/python2.7/site-packages/lithoxyl/logger.py", line 298, in logged_func +return func_to_log(*a, **kw) +TypeError: update() got multiple values for keyword argument '_act' + dictutils --------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/appveyor.yml new/boltons-20.0.0/appveyor.yml --- old/boltons-19.1.0/appveyor.yml 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/appveyor.yml 2020-01-09 00:32:03.000000000 +0100 @@ -25,7 +25,7 @@ install: - "%PYTHON%/Scripts/easy_install -U pip" - - "%PYTHON%/Scripts/pip install tox wheel" + - "%PYTHON%/Scripts/pip install -U --force-reinstall tox wheel" build: false # Not a C# project, build stuff at the test step instead. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/cacheutils.py new/boltons-20.0.0/boltons/cacheutils.py --- old/boltons-19.1.0/boltons/cacheutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/boltons/cacheutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -705,7 +705,7 @@ """ if n <= 0: return [] - ret = sorted(self.iteritems(), key=lambda x: x[1][0], reverse=True) + ret = sorted(self.iteritems(), key=lambda x: x[1], reverse=True) if n is None or n >= len(ret): return ret return ret[:n] @@ -714,7 +714,7 @@ """Get the sum of counts for keys exceeding the configured data threshold. """ - return sum([count for count, _ in self._count_map.itervalues()]) + return sum([count for count, _ in self._count_map.values()]) def get_uncommon_count(self): """Get the sum of counts for keys that were culled because the @@ -796,6 +796,8 @@ integer IDs, such that no two objects have the same ID at the same time. + Maps arbitrary hashable objects to IDs. + Based on https://gist.github.com/kurtbrose/25b48114de216a5e55df """ def __init__(self): @@ -833,13 +835,13 @@ return a in self.mapping def __iter__(self): - return self.mapping.itervalues() + return iter(self.mapping) def __len__(self): return self.mapping.__len__() def iteritems(self): - return self.mapping.iteritems() + return iter((k, self.mapping[k][0]) for k in iter(self.mapping)) # end cacheutils.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/dictutils.py new/boltons-20.0.0/boltons/dictutils.py --- old/boltons-19.1.0/boltons/dictutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/boltons/dictutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -480,10 +480,10 @@ >>> omd = OrderedMultiDict(zip('hello', 'world')) >>> omd.sorted(key=lambda i: i[1]) # i[0] is the key, i[1] is the val - OrderedMultiDict([('o', 'd'), ('l', 'l'), ('e', 'o'), ('h', 'w')]) + OrderedMultiDict([('o', 'd'), ('l', 'l'), ('e', 'o'), ('l', 'r'), ('h', 'w')]) """ cls = self.__class__ - return cls(sorted(self.iteritems(), key=key, reverse=reverse)) + return cls(sorted(self.iteritems(multi=True), key=key, reverse=reverse)) def sortedvalues(self, key=None, reverse=False): """Returns a copy of the :class:`OrderedMultiDict` with the same keys @@ -710,7 +710,8 @@ curr = curr[PREV] -_SELF_INIT_MARKER = object() +_OTO_INV_MARKER = object() +_OTO_UNIQUE_MARKER = object() class OneToOne(dict): @@ -741,15 +742,61 @@ For a very similar project, with even more one-to-one functionality, check out `bidict <https://github.com/jab/bidict>`_. """ - __slots__ = ('inv') + __slots__ = ('inv',) def __init__(self, *a, **kw): - if a and a[0] is _SELF_INIT_MARKER: - self.inv = a[1] - dict.__init__(self, [(v, k) for k, v in self.inv.items()]) - else: - dict.__init__(self, *a, **kw) - self.inv = self.__class__(_SELF_INIT_MARKER, self) + raise_on_dupe = False + if a: + if a[0] is _OTO_INV_MARKER: + self.inv = a[1] + dict.__init__(self, [(v, k) for k, v in self.inv.items()]) + return + elif a[0] is _OTO_UNIQUE_MARKER: + a, raise_on_dupe = a[1:], True + + dict.__init__(self, *a, **kw) + self.inv = self.__class__(_OTO_INV_MARKER, self) + + if len(self) == len(self.inv): + # if lengths match, that means everything's unique + return + + if not raise_on_dupe: + dict.clear(self) + dict.update(self, [(v, k) for k, v in self.inv.items()]) + return + + # generate an error message if the values aren't 1:1 + + val_multidict = {} + for k, v in self.items(): + val_multidict.setdefault(v, []).append(k) + + dupes = dict([(v, k_list) for v, k_list in + val_multidict.items() if len(k_list) > 1]) + + raise ValueError('expected unique values, got multiple keys for' + ' the following values: %r' % dupes) + + @classmethod + def unique(cls, *a, **kw): + """This alternate constructor for OneToOne will raise an exception + when input values overlap. For instance: + + >>> OneToOne.unique({'a': 1, 'b': 1}) + Traceback (most recent call last): + ... + ValueError: expected unique values, got multiple keys for the following values: ... + + This even works across inputs: + + >>> a_dict = {'a': 2} + >>> OneToOne.unique(a_dict, b=2) + Traceback (most recent call last): + ... + ValueError: expected unique values, got multiple keys for the following values: ... + """ + return cls(_OTO_UNIQUE_MARKER, *a, **kw) def __setitem__(self, key, val): hash(val) # ensure val is a valid key diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/fileutils.py new/boltons-20.0.0/boltons/fileutils.py --- old/boltons-19.1.0/boltons/fileutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/boltons/fileutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -163,7 +163,7 @@ Here's an example that holds true on most systems: >>> import tempfile - >>> 'r' in FilePerms.from_path(tempfile.tempdir).user + >>> 'r' in FilePerms.from_path(tempfile.gettempdir()).user True """ stat_res = os.stat(path) @@ -451,13 +451,10 @@ return -_CUR_DIR = os.path.dirname(os.path.abspath(__file__)) - - -def iter_find_files(directory, patterns, ignored=None): - """Returns a generator that yields file paths under a *directory* - (recursively), matching *patterns* using `glob`_ syntax (e.g., ``*.txt``). - Also supports *ignored* patterns. +def iter_find_files(directory, patterns, ignored=None, include_dirs=False): + """Returns a generator that yields file paths under a *directory*, + matching *patterns* using `glob`_ syntax (e.g., ``*.txt``). Also + supports *ignored* patterns. Args: directory (str): Path that serves as the root of the @@ -466,9 +463,12 @@ glob-formatted patterns to find under *directory*. ignored (str or list): A single pattern or list of glob-formatted patterns to ignore. + include_dirs (bool): Whether to include directories that match + patterns, as well. Defaults to ``False``. For example, finding Python files in the current directory: + >>> _CUR_DIR = os.path.dirname(os.path.abspath(__file__)) >>> filenames = sorted(iter_find_files(_CUR_DIR, '*.py')) >>> os.path.basename(filenames[-1]) 'urlutils.py' @@ -490,6 +490,14 @@ ignored = [ignored] ign_re = re.compile('|'.join([fnmatch.translate(p) for p in ignored])) for root, dirs, files in os.walk(directory): + if include_dirs: + for basename in dirs: + if pats_re.match(basename): + if ignored and ign_re.match(basename): + continue + filename = os.path.join(root, basename) + yield filename + for basename in files: if pats_re.match(basename): if ignored and ign_re.match(basename): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/funcutils.py new/boltons-20.0.0/boltons/funcutils.py --- old/boltons-19.1.0/boltons/funcutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/boltons/funcutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -39,6 +39,65 @@ NO_DEFAULT = object() +_IS_PY35 = sys.version_info >= (3, 5) +if not _IS_PY35: + # py35+ wants you to use signature instead, but + # inspect_formatargspec is way simpler for what it is. Copied the + # vendoring approach from alembic: + # https://github.com/sqlalchemy/alembic/blob/4cdad6aec32b4b5573a2009cc356cb4b144bd359/alembic/util/compat.py#L92 + from inspect import formatargspec as inspect_formatargspec +else: + from inspect import formatannotation + + def inspect_formatargspec( + args, varargs=None, varkw=None, defaults=None, + kwonlyargs=(), kwonlydefaults={}, annotations={}, + formatarg=str, + formatvarargs=lambda name: '*' + name, + formatvarkw=lambda name: '**' + name, + formatvalue=lambda value: '=' + repr(value), + formatreturns=lambda text: ' -> ' + text, + formatannotation=formatannotation): + """Copy formatargspec from python 3.7 standard library. + Python 3 has deprecated formatargspec and requested that Signature + be used instead, however this requires a full reimplementation + of formatargspec() in terms of creating Parameter objects and such. + Instead of introducing all the object-creation overhead and having + to reinvent from scratch, just copy their compatibility routine. + """ + + def formatargandannotation(arg): + result = formatarg(arg) + if arg in annotations: + result += ': ' + formatannotation(annotations[arg]) + return result + specs = [] + if defaults: + firstdefault = len(args) - len(defaults) + for i, arg in enumerate(args): + spec = formatargandannotation(arg) + if defaults and i >= firstdefault: + spec = spec + formatvalue(defaults[i - firstdefault]) + specs.append(spec) + if varargs is not None: + specs.append(formatvarargs(formatargandannotation(varargs))) + else: + if kwonlyargs: + specs.append('*') + if kwonlyargs: + for kwonlyarg in kwonlyargs: + spec = formatargandannotation(kwonlyarg) + if kwonlydefaults and kwonlyarg in kwonlydefaults: + spec += formatvalue(kwonlydefaults[kwonlyarg]) + specs.append(spec) + if varkw is not None: + specs.append(formatvarkw(formatargandannotation(varkw))) + result = '(' + ', '.join(specs) + ')' + if 'return' in annotations: + result += formatreturns(formatannotation(annotations['return'])) + return result + + def get_module_callables(mod, ignore=None): """Returns two maps of (*types*, *funcs*) from *mod*, optionally ignoring based on the :class:`bool` return value of the *ignore* @@ -222,6 +281,147 @@ partial = CachedInstancePartial +def format_invocation(name='', args=(), kwargs=None): + """Given a name, positional arguments, and keyword arguments, format + a basic Python-style function call. + + >>> print(format_invocation('func', args=(1, 2), kwargs={'c': 3})) + func(1, 2, c=3) + >>> print(format_invocation('a_func', args=(1,))) + a_func(1) + >>> print(format_invocation('kw_func', kwargs=[('a', 1), ('b', 2)])) + kw_func(a=1, b=2) + + """ + kwargs = kwargs or {} + a_text = ', '.join([repr(a) for a in args]) + if isinstance(kwargs, dict): + kwarg_items = [(k, kwargs[k]) for k in sorted(kwargs)] + else: + kwarg_items = kwargs + kw_text = ', '.join(['%s=%r' % (k, v) for k, v in kwarg_items]) + + all_args_text = a_text + if all_args_text and kw_text: + all_args_text += ', ' + all_args_text += kw_text + + return '%s(%s)' % (name, all_args_text) + + +def format_exp_repr(obj, pos_names, req_names=None, opt_names=None, opt_key=None): + """Render an expression-style repr of an object, based on attribute + names, which are assumed to line up with arguments to an initializer. + + >>> class Flag(object): + ... def __init__(self, length, width, depth=None): + ... self.length = length + ... self.width = width + ... self.depth = depth + ... + + That's our Flag object, here are some example reprs for it: + + >>> flag = Flag(5, 10) + >>> print(format_exp_repr(flag, ['length', 'width'], [], ['depth'])) + Flag(5, 10) + >>> flag2 = Flag(5, 15, 2) + >>> print(format_exp_repr(flag2, ['length'], ['width', 'depth'])) + Flag(5, width=15, depth=2) + + By picking the pos_names, req_names, opt_names, and opt_key, you + can fine-tune how you want the repr to look. + + Args: + obj (object): The object whose type name will be used and + attributes will be checked + pos_names (list): Required list of attribute names which will be + rendered as positional arguments in the output repr. + req_names (list): List of attribute names which will always + appear in the keyword arguments in the output repr. Defaults to None. + opt_names (list): List of attribute names which may appear in + the keyword arguments in the output repr, provided they pass + the *opt_key* check. Defaults to None. + opt_key (callable): A function or callable which checks whether + an opt_name should be in the repr. Defaults to a + ``None``-check. + + """ + cn = obj.__class__.__name__ + req_names = req_names or [] + opt_names = opt_names or [] + uniq_names, all_names = set(), [] + for name in req_names + opt_names: + if name in uniq_names: + continue + uniq_names.add(name) + all_names.append(name) + + if opt_key is None: + opt_key = lambda v: v is None + assert callable(opt_key) + + args = [getattr(obj, name, None) for name in pos_names] + + kw_items = [(name, getattr(obj, name, None)) for name in all_names] + kw_items = [(name, val) for name, val in kw_items + if not (name in opt_names and opt_key(val))] + + return format_invocation(cn, args, kw_items) + + +def format_nonexp_repr(obj, req_names=None, opt_names=None, opt_key=None): + """Format a non-expression-style repr + + Some object reprs look like object instantiation, e.g., App(r=[], mw=[]). + + This makes sense for smaller, lower-level objects whose state + roundtrips. But a lot of objects contain values that don't + roundtrip, like types and functions. + + For those objects, there is the non-expression style repr, which + mimic's Python's default style to make a repr like so: + + >>> class Flag(object): + ... def __init__(self, length, width, depth=None): + ... self.length = length + ... self.width = width + ... self.depth = depth + ... + >>> flag = Flag(5, 10) + >>> print(format_nonexp_repr(flag, ['length', 'width'], ['depth'])) + <Flag length=5 width=10> + + If no attributes are specified or set, utilizes the id, not unlike Python's + built-in behavior. + + >>> print(format_nonexp_repr(flag)) + <Flag id=...> + """ + cn = obj.__class__.__name__ + req_names = req_names or [] + opt_names = opt_names or [] + uniq_names, all_names = set(), [] + for name in req_names + opt_names: + if name in uniq_names: + continue + uniq_names.add(name) + all_names.append(name) + + if opt_key is None: + opt_key = lambda v: v is None + assert callable(opt_key) + + items = [(name, getattr(obj, name, None)) for name in all_names] + labels = ['%s=%r' % (name, val) for name, val in items + if not (name in opt_names and opt_key(val))] + if not labels: + labels = ['id=%s' % id(obj)] + ret = '<%s %s>' % (cn, ' '.join(labels)) + return ret + + + # # # # # # Function builder # # # @@ -418,7 +618,8 @@ varkw (str): Name of the catch-all variable for keyword arguments. E.g., "kwargs" if the resultant function is to have ``**kwargs`` in the signature. Defaults to None. - defaults (dict): A mapping of argument names to default values. + defaults (tuple): A tuple containing default argument values for + those arguments that have defaults. kwonlyargs (list): Argument names which are only valid as keyword arguments. **Python 3 only.** kwonlydefaults (dict): A mapping, same as normal *defaults*, @@ -502,11 +703,11 @@ with_annotations is ignored on Python 2. On Python 3 signature will omit annotations if it is set to False. """ - return inspect.formatargspec(self.args, self.varargs, + return inspect_formatargspec(self.args, self.varargs, self.varkw, []) def get_invocation_str(self): - return inspect.formatargspec(self.args, self.varargs, + return inspect_formatargspec(self.args, self.varargs, self.varkw, [])[1:-1] else: def get_sig_str(self, with_annotations=True): @@ -519,7 +720,8 @@ annotations = self.annotations else: annotations = {} - return inspect.formatargspec(self.args, + + return inspect_formatargspec(self.args, self.varargs, self.varkw, [], @@ -542,7 +744,7 @@ for arg in self.kwonlyargs) formatters['formatvalue'] = lambda value: '=' + value - sig = inspect.formatargspec(self.args, + sig = inspect_formatargspec(self.args, self.varargs, self.varkw, [], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/iterutils.py new/boltons-20.0.0/boltons/iterutils.py --- old/boltons-19.1.0/boltons/iterutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/boltons/iterutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -41,6 +41,7 @@ # Python 3 compat _IS_PY3 = True basestring = (str, bytes) + unicode = str izip, xrange = zip, range @@ -201,7 +202,7 @@ def chunked_iter(src, size, **kw): """Generates *size*-sized chunks from *src* iterable. Unless the - optional *fill* keyword argument is provided, iterables not even + optional *fill* keyword argument is provided, iterables not evenly divisible by *size* will have a final chunk that is smaller than *size*. @@ -231,6 +232,8 @@ postprocess = lambda chk: chk if isinstance(src, basestring): postprocess = lambda chk, _sep=type(src)(): _sep.join(chk) + if _IS_PY3 and isinstance(src, bytes): + postprocess = lambda chk: bytes(chk) src_iter = iter(src) while True: cur_chunk = list(itertools.islice(src_iter, size)) @@ -471,9 +474,8 @@ return -def bucketize(src, key=None, value_transform=None, key_filter=None): - """Group values in the *src* iterable by the value returned by *key*, - which defaults to :class:`bool`, grouping values by truthiness. +def bucketize(src, key=bool, value_transform=None, key_filter=None): + """Group values in the *src* iterable by the value returned by *key*. >>> bucketize(range(5)) {False: [0], True: [1, 2, 3, 4]} @@ -481,6 +483,12 @@ >>> bucketize(range(5), is_odd) {False: [0, 2, 4], True: [1, 3]} + *key* is :class:`bool` by default, but can either be a callable or a string + name of the attribute on which to bucketize objects. + + >>> bucketize([1+1j, 2+2j, 1, 2], key='real') + {1.0: [(1+1j), 1], 2.0: [(2+2j), 2]} + Value lists are not deduplicated: >>> bucketize([None, None, None, 'hello']) @@ -510,10 +518,14 @@ """ if not is_iterable(src): raise TypeError('expected an iterable') - if key is None: - key = bool - if not callable(key): - raise TypeError('expected callable key function') + + if isinstance(key, basestring): + key_func = lambda x: getattr(x, key, x) + elif callable(key): + key_func = key + else: + raise TypeError('expected key to be callable or a string') + if value_transform is None: value_transform = lambda x: x if not callable(value_transform): @@ -521,13 +533,13 @@ ret = {} for val in src: - key_of_val = key(val) + key_of_val = key_func(val) if key_filter is None or key_filter(key_of_val): ret.setdefault(key_of_val, []).append(value_transform(val)) return ret -def partition(src, key=None): +def partition(src, key=bool): """No relation to :meth:`str.partition`, ``partition`` is like :func:`bucketize`, but for added convenience returns a tuple of ``(truthy_values, falsy_values)``. @@ -537,7 +549,8 @@ ['hi', 'bye'] *key* defaults to :class:`bool`, but can be carefully overridden to - use any function that returns either ``True`` or ``False``. + use either a function that returns either ``True`` or ``False`` or + a string name of the attribute on which to partition objects. >>> import string >>> is_digit = lambda x: x in string.digits @@ -742,7 +755,7 @@ yield item def flatten(iterable): - """``flatten_iter()`` returns a collapsed list of all the elements from + """``flatten()`` returns a collapsed list of all the elements from *iterable* while collapsing any nested iterables. >>> nested = [[1, 2], [[3], [4, 5]]] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/pathutils.py new/boltons-20.0.0/boltons/pathutils.py --- old/boltons-19.1.0/boltons/pathutils.py 1970-01-01 01:00:00.000000000 +0100 +++ new/boltons-20.0.0/boltons/pathutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -0,0 +1,154 @@ +""" +Functions for working with filesystem paths. + +The :func:`expandpath` function expands the tilde to $HOME and environment +variables to their values. + +The :func:`augpath` function creates variants of an existing path without +having to spend multiple lines of code splitting it up and stitching it back +together. + +The :func:`shrinkuser` function replaces your home directory with a tilde. +""" +from __future__ import print_function + +from os.path import (expanduser, expandvars, join, normpath, split, splitext) +import os + + +__all__ = [ + 'augpath', 'shrinkuser', 'expandpath', +] + + +def augpath(path, suffix='', prefix='', ext=None, base=None, dpath=None, + multidot=False): + """ + Augment a path by modifying its components. + + Creates a new path with a different extension, basename, directory, prefix, + and/or suffix. + + A prefix is inserted before the basename. A suffix is inserted + between the basename and the extension. The basename and extension can be + replaced with a new one. Essentially a path is broken down into components + (dpath, base, ext), and then recombined as (dpath, prefix, base, suffix, + ext) after replacing any specified component. + + Args: + path (str | PathLike): a path to augment + suffix (str, default=''): placed between the basename and extension + prefix (str, default=''): placed in front of the basename + ext (str, default=None): if specified, replaces the extension + base (str, default=None): if specified, replaces the basename without + extension + dpath (str | PathLike, default=None): if specified, replaces the + directory + multidot (bool, default=False): Allows extensions to contain multiple + dots. Specifically, if False, everything after the last dot in the + basename is the extension. If True, everything after the first dot + in the basename is the extension. + + Returns: + str: augmented path + + Example: + >>> path = 'foo.bar' + >>> suffix = '_suff' + >>> prefix = 'pref_' + >>> ext = '.baz' + >>> newpath = augpath(path, suffix, prefix, ext=ext, base='bar') + >>> print('newpath = %s' % (newpath,)) + newpath = pref_bar_suff.baz + + Example: + >>> augpath('foo.bar') + 'foo.bar' + >>> augpath('foo.bar', ext='.BAZ') + 'foo.BAZ' + >>> augpath('foo.bar', suffix='_') + 'foo_.bar' + >>> augpath('foo.bar', prefix='_') + '_foo.bar' + >>> augpath('foo.bar', base='baz') + 'baz.bar' + >>> augpath('foo.tar.gz', ext='.zip', multidot=True) + 'foo.zip' + >>> augpath('foo.tar.gz', ext='.zip', multidot=False) + 'foo.tar.zip' + >>> augpath('foo.tar.gz', suffix='_new', multidot=True) + 'foo_new.tar.gz' + """ + # Breakup path + orig_dpath, fname = split(path) + if multidot: + # The first dot defines the extension + parts = fname.split('.', 1) + orig_base = parts[0] + orig_ext = '' if len(parts) == 1 else '.' + parts[1] + else: + # The last dot defines the extension + orig_base, orig_ext = splitext(fname) + # Replace parts with specified augmentations + if dpath is None: + dpath = orig_dpath + if ext is None: + ext = orig_ext + if base is None: + base = orig_base + # Recombine into new path + new_fname = ''.join((prefix, base, suffix, ext)) + newpath = join(dpath, new_fname) + return newpath + + +def shrinkuser(path, home='~'): + """ + Inverse of :func:`os.path.expanduser`. + + Args: + path (str | PathLike): path in system file structure + home (str, default='~'): symbol used to replace the home path. + Defaults to '~', but you might want to use '$HOME' or + '%USERPROFILE%' instead. + + Returns: + str: path: shortened path replacing the home directory with a tilde + + Example: + >>> path = expanduser('~') + >>> assert path != '~' + >>> assert shrinkuser(path) == '~' + >>> assert shrinkuser(path + '1') == path + '1' + >>> assert shrinkuser(path + '/1') == join('~', '1') + >>> assert shrinkuser(path + '/1', '$HOME') == join('$HOME', '1') + """ + path = normpath(path) + userhome_dpath = expanduser('~') + if path.startswith(userhome_dpath): + if len(path) == len(userhome_dpath): + path = home + elif path[len(userhome_dpath)] == os.path.sep: + path = home + path[len(userhome_dpath):] + return path + + +def expandpath(path): + """ + Shell-like expansion of environment variables and tilde home directory. + + Args: + path (str | PathLike): the path to expand + + Returns: + str : expanded path + + Example: + >>> import os + >>> os.environ['SPAM'] = 'eggs' + >>> assert expandpath('~/$SPAM') == expanduser('~/eggs') + >>> assert expandpath('foo') == 'foo' + """ + path = expanduser(path) + path = expandvars(path) + return path diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/setutils.py new/boltons-20.0.0/boltons/setutils.py --- old/boltons-19.1.0/boltons/setutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/boltons/setutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -473,15 +473,14 @@ way to invert an expression, you can just throw a complement on the set. Consider this example of a name filter:: - >>> class NamesFilter(object): - ... def __init__(self, allowed): - ... self._allowed = allowed - ... - ... def filter(self, names): - ... return [name for name in names if name in self._allowed] - - >>> NamesFilter(set(['alice', 'bob'])).filter(['alice', 'bob', 'carol']) - ['alice', 'bob'] + >>> class NamesFilter(object): + ... def __init__(self, allowed): + ... self._allowed = allowed + ... + ... def filter(self, names): + ... return [name for name in names if name in self._allowed] + >>> NamesFilter(set(['alice', 'bob'])).filter(['alice', 'bob', 'carol']) + ['alice', 'bob'] What if we want to just express "let all the names through"? @@ -862,6 +861,22 @@ else: # + + return _ComplementSet(included=self._included.difference(inc)) + def __rsub__(self, other): + inc, exc = _norm_args_notimplemented(other) + if inc is NotImplemented: + return NotImplemented + # rsub, so the expression being evaluated is "other - self" + if self._included is None: + if exc is None: # - + + return _ComplementSet(included=inc & self._excluded) + else: # - - + return _ComplementSet(included=self._excluded - exc) + else: + if inc is None: # + - + return _ComplementSet(excluded=exc | self._included) + else: # + + + return _ComplementSet(included=inc.difference(self._included)) + def difference_update(self, other): try: self -= other @@ -895,11 +910,18 @@ return hash(self._included) ^ hash(self._excluded) def __len__(self): - if self._included: + if self._included is not None: return len(self._included) raise NotImplementedError('complemented sets have undefined length') def __iter__(self): - if self._included: + if self._included is not None: return iter(self._included) raise NotImplementedError('complemented sets have undefined contents') + + def __bool__(self): + if self._included is not None: + return bool(self._included) + return True + + __nonzero__ = __bool__ # py2 compat diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/statsutils.py new/boltons-20.0.0/boltons/statsutils.py --- old/boltons-19.1.0/boltons/statsutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/boltons/statsutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -583,12 +583,12 @@ allowing for simple visualization, even in console environments. >>> data = list(range(20)) + list(range(5, 15)) + [10] - >>> print(Stats(data).format_histogram()) - 0.0: 5 ################################ - 4.4: 8 ################################################### - 8.9: 11 ###################################################################### - 13.3: 5 ################################ - 17.8: 2 ############# + >>> print(Stats(data).format_histogram(width=30)) + 0.0: 5 ######### + 4.4: 8 ############### + 8.9: 11 #################### + 13.3: 5 ######### + 17.8: 2 #### In this histogram, five values are between 0.0 and 4.4, eight are between 4.4 and 8.9, and two values lie between 17.8 and diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/strutils.py new/boltons-20.0.0/boltons/strutils.py --- old/boltons-19.1.0/boltons/strutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/boltons/strutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -42,7 +42,7 @@ 'asciify', 'is_ascii', 'is_uuid', 'html2text', 'strip_ansi', 'bytes2human', 'find_hashtags', 'a10n', 'gzip_bytes', 'gunzip_bytes', 'iter_splitlines', 'indent', 'escape_shell_args', - 'args2cmd', 'args2sh', 'parse_int_list', 'format_int_list'] + 'args2cmd', 'args2sh', 'parse_int_list', 'format_int_list', 'unwrap_text'] _punct_ws_str = string.punctuation + string.whitespace @@ -1092,3 +1092,34 @@ """Shortcut function to invoke multi-replace in a single command.""" m = MultiReplace(sub_map, **kwargs) return m.sub(text) + + +def unwrap_text(text, ending='\n\n'): + r""" + Unwrap text, the natural complement to :func:`textwrap.wrap`. + + >>> text = "Short \n lines \nwrapped\nsmall.\n\nAnother\nparagraph." + >>> unwrap_text(text) + 'Short lines wrapped small.\n\nAnother paragraph.' + + Args: + text: A string to unwrap. + ending (str): The string to join all unwrapped paragraphs + by. Pass ``None`` to get the list. Defaults to '\n\n' for + compatibility with Markdown and RST. + + """ + all_grafs = [] + cur_graf = [] + for line in text.splitlines(): + line = line.strip() + if line: + cur_graf.append(line) + else: + all_grafs.append(' '.join(cur_graf)) + cur_graf = [] + if cur_graf: + all_grafs.append(' '.join(cur_graf)) + if ending is None: + return all_grafs + return ending.join(all_grafs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/tbutils.py new/boltons-20.0.0/boltons/tbutils.py --- old/boltons-19.1.0/boltons/tbutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/boltons/tbutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -245,7 +245,7 @@ """ ret = [] if frame is None: - frame = sys._getframe(1) + frame = sys._getframe(level) if limit is None: limit = getattr(sys, 'tracebacklimit', 1000) n = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/boltons/urlutils.py new/boltons-20.0.0/boltons/urlutils.py --- old/boltons-19.1.0/boltons/urlutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/boltons/urlutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -1021,7 +1021,7 @@ >>> from pprint import pprint as pp # ensuring proper key ordering >>> omd = OrderedMultiDict([('a', 1), ('b', 2), ('a', 3)]) >>> pp(dict(omd)) - {'a': [1, 3], 'b': [2]} + {'a': 3, 'b': 2} Note that modifying those lists will modify the OMD. If you want a safe-to-modify or flat dictionary, use :meth:`OrderedMultiDict.todict()`. @@ -1377,7 +1377,7 @@ >>> omd = OrderedMultiDict(zip('hello', 'world')) >>> omd.sorted(key=lambda i: i[1]) # i[0] is the key, i[1] is the val - OrderedMultiDict([('o', 'd'), ('l', 'l'), ('e', 'o'), ('h', 'w')]) + OrderedMultiDict([('o', 'd'), ('l', 'l'), ('e', 'o'), ('l', 'r'), ('h', 'w')]) """ cls = self.__class__ return cls(sorted(self.iteritems(), key=key, reverse=reverse)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/docs/conf.py new/boltons-20.0.0/docs/conf.py --- old/boltons-19.1.0/docs/conf.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/docs/conf.py 2020-01-09 00:32:03.000000000 +0100 @@ -97,11 +97,11 @@ # General information about the project. project = u'boltons' -copyright = u'2019, Mahmoud Hashemi' +copyright = u'2020, Mahmoud Hashemi' author = u'Mahmoud Hashemi' -version = '19.1' -release = '19.1.0' +version = '20.0' +release = '20.0.0' if os.name != 'nt': today_fmt = '%B %d, %Y' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/docs/fileutils.rst new/boltons-20.0.0/docs/fileutils.rst --- old/boltons-19.1.0/docs/fileutils.rst 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/docs/fileutils.rst 2020-01-09 00:32:03.000000000 +0100 @@ -7,7 +7,7 @@ ------------------------------ Python's :mod:`os`, :mod:`os.path`, and :mod:`shutil` modules provide -good coverage of file wrangling fundaments, and these functions help +good coverage of file wrangling fundamentals, and these functions help close a few remaining gaps. .. autofunction:: boltons.fileutils.mkdir_p diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/docs/funcutils.rst new/boltons-20.0.0/docs/funcutils.rst --- old/boltons-19.1.0/docs/funcutils.rst 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/docs/funcutils.rst 2020-01-09 00:32:03.000000000 +0100 @@ -42,3 +42,6 @@ .. autofunction:: copy_function .. autofunction:: dir_dict .. autofunction:: mro_items +.. autofunction:: format_invocation +.. autofunction:: format_exp_repr +.. autofunction:: format_nonexp_repr diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/docs/index.rst new/boltons-20.0.0/docs/index.rst --- old/boltons-19.1.0/docs/index.rst 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/docs/index.rst 2020-01-09 00:32:03.000000000 +0100 @@ -117,6 +117,7 @@ mathutils mboxutils namedutils + pathutils queueutils setutils socketutils diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/docs/pathutils.rst new/boltons-20.0.0/docs/pathutils.rst --- old/boltons-19.1.0/docs/pathutils.rst 1970-01-01 01:00:00.000000000 +0100 +++ new/boltons-20.0.0/docs/pathutils.rst 2020-01-09 00:32:03.000000000 +0100 @@ -0,0 +1,6 @@ +``pathutils`` - Filesystem fun +============================== + +.. automodule:: boltons.pathutils + :members: + :undoc-members: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/pytest.ini new/boltons-20.0.0/pytest.ini --- old/boltons-19.1.0/pytest.ini 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/pytest.ini 2020-01-09 00:32:03.000000000 +0100 @@ -1,2 +1,2 @@ [pytest] -doctest_optionflags = ALLOW_UNICODE +doctest_optionflags=NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL ELLIPSIS ALLOW_UNICODE diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/setup.py new/boltons-20.0.0/setup.py --- old/boltons-19.1.0/setup.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/setup.py 2020-01-09 00:32:03.000000000 +0100 @@ -13,7 +13,7 @@ __author__ = 'Mahmoud Hashemi' -__version__ = '19.1.0' +__version__ = '20.0.0' __contact__ = '[email protected]' __url__ = 'https://github.com/mahmoud/boltons' __license__ = 'BSD' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/tests/test_cacheutils.py new/boltons-20.0.0/tests/test_cacheutils.py --- old/boltons-19.1.0/tests/test_cacheutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/tests/test_cacheutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -5,7 +5,7 @@ import pytest -from boltons.cacheutils import LRU, LRI, cached, cachedmethod, cachedproperty, MinIDMap +from boltons.cacheutils import LRU, LRI, cached, cachedmethod, cachedproperty, MinIDMap, ThresholdCounter class CountingCallable(object): @@ -405,14 +405,51 @@ midm = MinIDMap() class Foo(object): - pass + def __init__(self, val): + self.val = val # use this circular array to have them periodically collected ref_wheel = [None, None, None] for i in range(1000): - nxt = Foo() + nxt = Foo(i) ref_wheel[i % len(ref_wheel)] = nxt assert midm.get(nxt) <= len(ref_wheel) if i % 10 == 0: midm.drop(nxt) + + # test __iter__ + assert sorted([f.val for f in list(midm)[:10]]) == list(range(1000 - len(ref_wheel), 1000)) + + items = list(midm.iteritems()) + assert isinstance(items[0][0], Foo) + assert sorted(item[1] for item in items) == list(range(0, len(ref_wheel))) + + +def test_threshold_counter(): + tc = ThresholdCounter(threshold=0.1) + tc.add(1) + + assert tc.items() == [(1, 1)] + + tc.update([2] * 10) + + assert tc.get(1) == 0 + + tc.add(5) + assert 5 in tc + + assert len(list(tc.elements())) == 11 + + assert tc.threshold == 0.1 + assert tc.get_common_count() == 11 + assert tc.get_uncommon_count() == 1 # bc the initial 1 was dropped + assert round(tc.get_commonality(), 2) == 0.92 + assert tc.most_common(2) == [(2, 10), (5, 1)] + assert list(tc.elements()) == ([2] * 10) + [5] + + assert tc[2] == 10 + assert len(tc) == 2 + assert sorted(tc.keys()) == [2, 5] + assert sorted(tc.values()) == [1, 10] + assert sorted(tc.items()) == [(2, 10), (5, 1)] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/tests/test_dictutils.py new/boltons-20.0.0/tests/test_dictutils.py --- old/boltons-19.1.0/tests/test_dictutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/tests/test_dictutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -319,6 +319,23 @@ e.update({1:2}, cat="dog") ck({1:2, "cat":"dog"}, {2:1, "dog":"cat"}) + # try various overlapping values + oto = OneToOne({'a': 0, 'b': 0}) + assert len(oto) == len(oto.inv) == 1 + + oto['c'] = 0 + assert len(oto) == len(oto.inv) == 1 + assert oto.inv[0] == 'c' + + oto.update({'z': 0, 'y': 0}) + assert len(oto) == len(oto.inv) == 1 + + # test out unique classmethod + with pytest.raises(ValueError): + OneToOne.unique({'a': 0, 'b': 0}) + + return + def test_many_to_many(): m2m = ManyToMany() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/tests/test_funcutils.py new/boltons-20.0.0/tests/test_funcutils.py --- old/boltons-19.1.0/tests/test_funcutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/tests/test_funcutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -2,6 +2,7 @@ from boltons.funcutils import (copy_function, total_ordering, + format_invocation, InstancePartial, CachedInstancePartial) @@ -61,3 +62,10 @@ assert num < 5 assert num >= 2 assert num != 1 + + +def test_format_invocation(): + assert format_invocation('d') == "d()" + assert format_invocation('f', ('a', 'b')) == "f('a', 'b')" + assert format_invocation('g', (), {'x': 'y'}) == "g(x='y')" + assert format_invocation('h', ('a', 'b'), {'x': 'y', 'z': 'zz'}) == "h('a', 'b', x='y', z='zz')" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/tests/test_funcutils_fb.py new/boltons-20.0.0/tests/test_funcutils_fb.py --- old/boltons-19.1.0/tests/test_funcutils_fb.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/tests/test_funcutils_fb.py 2020-01-09 00:32:03.000000000 +0100 @@ -271,3 +271,27 @@ assert 'test' in fb_example.args assert fb_example.get_arg_names() == ('req', 'test') assert fb_example.get_arg_names(only_required=True) == ('req',) + + [email protected]( + "args, varargs, varkw, defaults, invocation_str, sig_str", + [ + (["a", "b"], None, None, None, "a, b", "(a, b)"), + (None, "args", "kwargs", None, "*args, **kwargs", "(*args, **kwargs)"), + ("a", None, None, dict(a="a"), "a", "(a)"), + ], +) +def test_get_invocation_sig_str( + args, varargs, varkw, defaults, invocation_str, sig_str +): + fb = FunctionBuilder( + name='return_five', + body='return 5', + args=args, + varargs=varargs, + varkw=varkw, + defaults=defaults + ) + + assert fb.get_invocation_str() == invocation_str + assert fb.get_sig_str() == sig_str diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/tests/test_funcutils_fb_py3.py new/boltons-20.0.0/tests/test_funcutils_fb_py3.py --- old/boltons-19.1.0/tests/test_funcutils_fb_py3.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/tests/test_funcutils_fb_py3.py 2020-01-09 00:32:03.000000000 +0100 @@ -147,3 +147,43 @@ with pytest.raises(TypeError): assert better_func('positional') return + + [email protected]( + "args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, invocation_str, sig_str", + [ + ( + None, + "args", + "kwargs", + None, + "a", + dict(a="a"), + "*args, a=a, **kwargs", + "(*args, a, **kwargs)", + ) + ], +) +def test_get_invocation_sig_str( + args, + varargs, + varkw, + defaults, + kwonlyargs, + kwonlydefaults, + invocation_str, + sig_str, +): + fb = FunctionBuilder( + name="return_five", + body="return 5", + args=args, + varargs=varargs, + varkw=varkw, + defaults=defaults, + kwonlyargs=kwonlyargs, + kwonlydefaults=kwonlydefaults, + ) + + assert fb.get_invocation_str() == invocation_str + assert fb.get_sig_str() == sig_str diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/tests/test_iterutils.py new/boltons-20.0.0/tests/test_iterutils.py --- old/boltons-19.1.0/tests/test_iterutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/tests/test_iterutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -502,3 +502,10 @@ for x in range(10000): guid_iter = GUIDerator(size=26) assert len(next(guid_iter)) == 26 + + +def test_chunked_bytes(): + # see #231 + from boltons.iterutils import chunked + + assert chunked(b'123', 2) in (['12', '3'], [b'12', b'3']) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/tests/test_setutils.py new/boltons-20.0.0/tests/test_setutils.py --- old/boltons-19.1.0/tests/test_setutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/tests/test_setutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -113,9 +113,12 @@ assert (sab ^ cab) == (cbc ^ sbc) assert cab - cc == sc assert cab - sab == cab + assert sab - cab == sab assert (cab ^ cbc | set('b')) == (sab | sbc) everything = complement(frozenset()) assert everything in everything # https://en.wikipedia.org/wiki/Russell%27s_paradox + assert bool(cab) + assert not complement(u) # destructive testing cab ^= sab cab ^= sab diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/boltons-19.1.0/tests/test_socketutils.py new/boltons-20.0.0/tests/test_socketutils.py --- old/boltons-19.1.0/tests/test_socketutils.py 2019-02-28 09:11:11.000000000 +0100 +++ new/boltons-20.0.0/tests/test_socketutils.py 2020-01-09 00:32:03.000000000 +0100 @@ -105,7 +105,9 @@ return - +IS_PYPY_2 = ('__pypy__' in sys.builtin_module_names + and sys.version_info[0] == 2) [email protected](IS_PYPY_2, reason="pypy2 bug, fixed in 7.2. unmark when this test stops failing on travis (when they upgrade from 7.1)") def test_client_disconnecting(): def get_bs_pair(): x, y = socket.socketpair()
