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()


Reply via email to