Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-boltons for openSUSE:Factory 
checked in at 2021-02-04 20:21:51
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-boltons (Old)
 and      /work/SRC/openSUSE:Factory/.python-boltons.new.28504 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-boltons"

Thu Feb  4 20:21:51 2021 rev:6 rq:867974 version:20.2.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-boltons/python-boltons.changes    
2020-01-24 13:09:56.693403722 +0100
+++ /work/SRC/openSUSE:Factory/.python-boltons.new.28504/python-boltons.changes 
2021-02-04 20:21:52.694625660 +0100
@@ -1,0 +2,27 @@
+Sat Jan 30 07:06:02 UTC 2021 - John Vandenberg <jay...@gmail.com>
+
+- Include CHANGELOG.md & docs/*.rst
+- Update to v20.2.1
+  * Improve import time of iterutils
+  * Add custom repr parameter to funcutils.format_invocation
+- from v20.2.0
+  * Added iterutils.lstrip, iterutils.rstrip, iterutils.strip
+  * More robust and complete strutils.strip_ansi
+  * Add iterutils.untyped_sorted
+  * Fixes to IndexedSet rsub and index methods
+  * Expose text mode flag in fileutils.AtomicSaver
+  * Add strutils.int_list_complement and
+    strutils.int_list_to_int_tuples to the int_list suite
+  * Docs: intersphinx links finally point to Python 3 docs
+- from v20.1.0
+  * Add funcutils.update_wrapper, used to make a wrapper function
+    reflect various aspects of the wrapped function's API
+  * Fix FunctionBuilder handling of functions without __module__
+  * Add partial support to FunctionBuilder
+  * Fix NetstringSocket's handling of arguments in read_ns
+  * Fix IndexedSet's index() method to account for removals
+  * Add seekable, readable, and writable to SpooledIOBase
+  * Add a special case to singularize
+  * Fix various warnings for Py3.9
+
+-------------------------------------------------------------------

Old:
----
  boltons-20.0.0.tar.gz

New:
----
  boltons-20.2.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-boltons.spec ++++++
--- /var/tmp/diff_new_pack.uh6Y3f/_old  2021-02-04 20:21:53.350626659 +0100
+++ /var/tmp/diff_new_pack.uh6Y3f/_new  2021-02-04 20:21:53.354626665 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-boltons
 #
-# Copyright (c) 2020 SUSE LLC
+# Copyright (c) 2021 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-boltons
-Version:        20.0.0
+Version:        20.2.1
 Release:        0
 Summary:        The "Boltons" utility package for Python
 License:        BSD-3-Clause
@@ -52,7 +52,7 @@
 
 %files %{python_files}
 %license LICENSE
-%doc README.md
+%doc README.md CHANGELOG.md docs/*.rst
 %{python_sitelib}/*
 
 %changelog

++++++ boltons-20.0.0.tar.gz -> boltons-20.2.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/CHANGELOG.md 
new/boltons-20.2.1/CHANGELOG.md
--- old/boltons-20.0.0/CHANGELOG.md     2020-01-09 00:32:03.000000000 +0100
+++ new/boltons-20.2.1/CHANGELOG.md     2020-08-12 03:30:44.000000000 +0200
@@ -1,13 +1,46 @@
 boltons Changelog
 =================
 
-Since February 20, 2013 there have been 41 releases and 1405 commits for
+Since February 20, 2013 there have been 44 releases and 1468 commits for
 an average of one 34-commit release about every 8 weeks.
 
+20.2.1
+------
+*(August 11, 2020)*
+
+* Improve import time of [iterutils][iterutils] by deferring hashlib/socket 
imports
+* Add custom `repr` parameter to 
[funcutils.format_invocation][funcutils.format_invocation]
+
+20.2.0
+------
+*(June 21, 2020)*
+
+* Added [iterutils.lstrip][iterutils.lstrip], 
[iterutils.rstrip][iterutils.rstrip], [iterutils.strip][iterutils.strip]
+* More robust and complete [strutils.strip_ansi][strutils.strip_ansi]
+* Add [iterutils.untyped_sorted][iterutils.untyped_sorted]
+* Fixes to [IndexedSet][IndexedSet] rsub and index methods
+* Expose text mode flag in [fileutils.AtomicSaver][fileutils.AtomicSaver]
+* Add [strutils.int_list_complement][strutils.int_list_complement] and 
[strutils.int_list_to_int_tuples][strutils.int_list_to_int_tuples] to the 
*int_list* suite.
+* Docs: intersphinx links finally point to Python 3 docs
+
+20.1.0
+------
+*(March 29, 2020)*
+
+* Add [funcutils.update_wrapper][funcutils.update_wrapper], used to
+  make a wrapper function reflect various aspects of the wrapped
+  function's API.
+* Fix [FunctionBuilder][FunctionBuilder] handling of functions without 
`__module__`
+* Add `partial` support to [FunctionBuilder][FunctionBuilder]
+* Fix [NetstringSocket][socketutils.NetstringSocket]'s handling of arguments 
in `read_ns`
+* Fix [IndexedSet][IndexedSet]'s `index()` method to account for removals
+* Add `seekable`, `readable`, and `writable` to SpooledIOBase
+* Add a special case to `singularize`
+* Fix various warnings for Py3.9
 
 20.0.0
 ------
-*January 8, 2020*
+*(January 8, 2020)*
 
 * New module [pathutils][pathutils]:
     * [pathutils.augpath][pathutils.augpath] augments a path by modifying its 
components
@@ -17,7 +50,7 @@
 * 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])
+    * [iterutils.chunked][iterutils.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
@@ -830,7 +863,7 @@
   * add a cheesy little splay list construct that can be used for splay-
     like manual reordering for eventual optimization
   * traceback utils, first draft
-  * add strip_ansi() (need to make a cliutils or something)
+  * add [strutils.strip_ansi][strutils.strip_ansi] (need to make a cliutils or 
something)
   * add ansi strip task
   * mess with list tuning
   * add ordinalize()
@@ -955,6 +988,7 @@
 [funcutils.FunctionBuilder.add_arg]: 
https://boltons.readthedocs.io/en/latest/funcutils.html#boltons.funcutils.FunctionBuilder.add_arg
 [funcutils.partial_ordering]: 
http://boltons.readthedocs.org/en/latest/funcutils.html#boltons.funcutils.partial_ordering
 [funcutils.total_ordering]: 
http://boltons.readthedocs.org/en/latest/funcutils.html#boltons.funcutils.total_ordering
+[funcutils.update_wrapper]: 
http://boltons.readthedocs.org/en/latest/funcutils.html#boltons.funcutils.update_wrapper
 [funcutils.wraps]: 
http://boltons.readthedocs.org/en/latest/funcutils.html#boltons.funcutils.wraps
 [gcutils.GCToggler]: 
http://boltons.readthedocs.org/en/latest/gcutils.html#boltons.gcutils.GCToggler
 [gcutils.get_all]: 
http://boltons.readthedocs.org/en/latest/gcutils.html#boltons.gcutils.get_all
@@ -1000,8 +1034,12 @@
 [iterutils.remap]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.remap
 [iterutils.research]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.research
 [iterutils.soft_sorted]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.soft_sorted
+[iterutils.untyped_sorted]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.untyped_sorted
 [iterutils.split]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.split
 [iterutils.split_iter]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.split_iter
+[iterutils.strip]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.strip
+[iterutils.rstrip]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.rstrip
+[iterutils.lstrip]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.lstrip
 [iterutils.unique]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.unique
 [iterutils.windowed_iter]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.windowed_iter
 [iterutils.xfrange]: 
http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.xfrange
@@ -1012,6 +1050,7 @@
 [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
+[IndexedSet]: 
http://boltons.readthedocs.org/en/latest/setutils.html#boltons.setutils.IndexedSet
 [socketutils]: http://boltons.readthedocs.org/en/latest/socketutils.html
 [socketutils.BufferedSocket]: 
http://boltons.readthedocs.org/en/latest/socketutils.html#boltons.socketutils.BufferedSocket
 [socketutils.BufferedSocket.recv]: 
http://boltons.readthedocs.org/en/latest/socketutils.html#boltons.socketutils.BufferedSocket.recv
@@ -1044,7 +1083,10 @@
 [strutils.is_uuid]: 
http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.is_uuid
 [strutils.parse_int_list]: 
http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.parse_int_list
 [strutils.format_int_list]: 
http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.format_int_list
+[strutils.int_list_complement]: 
http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.int_list_complement
+[strutils.int_list_to_int_tuples]: 
http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.int_list_to_int_tuples
 [strutils.slugify]: 
http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.slugify
+[strutils.strip_ansi]: 
http://boltons.readthedocs.org/en/latest/strutils.html#boltons.strutils.strip_ansi
 [tableutils]: http://boltons.readthedocs.org/en/latest/tableutils.html
 [tableutils.Table]: 
http://boltons.readthedocs.org/en/latest/tableutils.html#boltons.tableutils.Table
 [tbutils]: http://boltons.readthedocs.org/en/latest/tbutils.html
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/ecoutils.py 
new/boltons-20.2.1/boltons/ecoutils.py
--- old/boltons-20.0.0/boltons/ecoutils.py      2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/ecoutils.py      2020-08-12 03:30:44.000000000 
+0200
@@ -374,7 +374,7 @@
         def dumps(val, indent):
             ret = _fake_json_dumps(val, indent=indent)
             if not indent:
-                ret = re.sub('\n\s*', ' ', ret)
+                ret = re.sub(r'\n\s*', ' ', ret)
             return ret
 
     data_dict = get_profile()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/fileutils.py 
new/boltons-20.2.1/boltons/fileutils.py
--- old/boltons-20.0.0/boltons/fileutils.py     2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/fileutils.py     2020-08-12 03:30:44.000000000 
+0200
@@ -319,6 +319,8 @@
             from the previous file, or if the file did not exist, to
             respect the user's configured `umask`_, usually resulting
             in octal 0644 or 0664.
+        text_mode (bool): Whether to open the destination file in text
+            mode.
         part_file (str): Name of the temporary *part_file*. Defaults
             to *dest_path* + ``.part``. Note that this argument is
             just the filename, and not the full path of the part
@@ -358,7 +360,7 @@
         self.overwrite_part = kwargs.pop('overwrite_part', False)
         self.part_filename = kwargs.pop('part_file', None)
         self.rm_part_on_exc = kwargs.pop('rm_part_on_exc', True)
-        self.text_mode = kwargs.pop('text_mode', False)  # for windows
+        self.text_mode = kwargs.pop('text_mode', False)
         self.buffering = kwargs.pop('buffering', -1)
         if kwargs:
             raise TypeError('unexpected kwargs: %r' % (kwargs.keys(),))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/formatutils.py 
new/boltons-20.2.1/boltons/formatutils.py
--- old/boltons-20.0.0/boltons/formatutils.py   2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/formatutils.py   2020-08-12 03:30:44.000000000 
+0200
@@ -53,7 +53,7 @@
 
 _pos_farg_re = re.compile('({{)|'         # escaped open-brace
                           '(}})|'         # escaped close-brace
-                          '({[:!.\[}])')  # anon positional format arg
+                          r'({[:!.\[}])')  # anon positional format arg
 
 
 def construct_format_field_str(fname, fspec, conv):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/funcutils.py 
new/boltons-20.2.1/boltons/funcutils.py
--- old/boltons-20.0.0/boltons/funcutils.py     2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/funcutils.py     2020-08-12 03:30:44.000000000 
+0200
@@ -38,6 +38,11 @@
 except ImportError:
     NO_DEFAULT = object()
 
+try:
+    from functools import partialmethod
+except ImportError:
+    partialmethod = None
+
 
 _IS_PY35 = sys.version_info >= (3, 5)
 if not _IS_PY35:
@@ -242,10 +247,16 @@
     has the same ability, but is slightly more efficient.
 
     """
+    if partialmethod is not None:  # NB: See 
https://github.com/mahmoud/boltons/pull/244
+        @property
+        def _partialmethod(self):
+            return partialmethod(self.func, *self.args, **self.keywords)
+
     def __get__(self, obj, obj_type):
         return make_method(self, obj, obj_type)
 
 
+
 class CachedInstancePartial(functools.partial):
     """The ``CachedInstancePartial`` is virtually the same as
     :class:`InstancePartial`, adding support for method-usage to
@@ -256,6 +267,11 @@
 
     See the :class:`InstancePartial` docstring for more details.
     """
+    if partialmethod is not None:  # NB: See 
https://github.com/mahmoud/boltons/pull/244
+        @property
+        def _partialmethod(self):
+            return partialmethod(self.func, *self.args, **self.keywords)
+
     def __get__(self, obj, obj_type):
         # These assignments could've been in __init__, but there was
         # no simple way to do it without breaking one of PyPy or Py3.
@@ -278,10 +294,11 @@
             obj.__dict__[name] = ret = make_method(self, obj, obj_type)
             return ret
 
+
 partial = CachedInstancePartial
 
 
-def format_invocation(name='', args=(), kwargs=None):
+def format_invocation(name='', args=(), kwargs=None, **kw):
     """Given a name, positional arguments, and keyword arguments, format
     a basic Python-style function call.
 
@@ -293,13 +310,16 @@
     kw_func(a=1, b=2)
 
     """
+    _repr = kw.pop('repr', repr)
+    if kw:
+        raise TypeError('unexpected keyword args: %r' % ', '.join(kw.keys()))
     kwargs = kwargs or {}
-    a_text = ', '.join([repr(a) for a in args])
+    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])
+    kw_text = ', '.join(['%s=%s' % (k, _repr(v)) for k, v in kwarg_items])
 
     all_args_text = a_text
     if all_args_text and kw_text:
@@ -428,8 +448,45 @@
 
 
 def wraps(func, injected=None, expected=None, **kw):
-    """Modeled after the built-in :func:`functools.wraps`, this function is
-    used to make your decorator's wrapper functions reflect the
+    """Decorator factory to apply update_wrapper() to a wrapper function.
+
+    Modeled after built-in :func:`functools.wraps`. Returns a decorator
+    that invokes update_wrapper() with the decorated function as the wrapper
+    argument and the arguments to wraps() as the remaining arguments.
+    Default arguments are as for update_wrapper(). This is a convenience
+    function to simplify applying partial() to update_wrapper().
+
+    Same example as in update_wrapper's doc but with wraps:
+
+        >>> from boltons.funcutils import wraps
+        >>>
+        >>> def print_return(func):
+        ...     @wraps(func)
+        ...     def wrapper(*args, **kwargs):
+        ...         ret = func(*args, **kwargs)
+        ...         print(ret)
+        ...         return ret
+        ...     return wrapper
+        ...
+        >>> @print_return
+        ... def example():
+        ...     '''docstring'''
+        ...     return 'example return value'
+        >>>
+        >>> val = example()
+        example return value
+        >>> example.__name__
+        'example'
+        >>> example.__doc__
+        'docstring'
+    """
+    return partial(update_wrapper, func=func, build_from=None,
+                   injected=injected, expected=expected, **kw)
+
+
+def update_wrapper(wrapper, func, injected=None, expected=None, 
build_from=None, **kw):
+    """Modeled after the built-in :func:`functools.update_wrapper`,
+    this function is used to make your wrapper function reflect the
     wrapped function's:
 
       * Name
@@ -437,21 +494,20 @@
       * Module
       * Signature
 
-    The built-in :func:`functools.wraps` copies the first three, but
-    does not copy the signature. This version of ``wraps`` can copy
+    The built-in :func:`functools.update_wrapper` copies the first three, but
+    does not copy the signature. This version of ``update_wrapper`` can copy
     the inner function's signature exactly, allowing seamless usage
     and :mod:`introspection <inspect>`. Usage is identical to the
     built-in version::
 
-        >>> from boltons.funcutils import wraps
+        >>> from boltons.funcutils import update_wrapper
         >>>
         >>> def print_return(func):
-        ...     @wraps(func)
         ...     def wrapper(*args, **kwargs):
         ...         ret = func(*args, **kwargs)
         ...         print(ret)
         ...         return ret
-        ...     return wrapper
+        ...     return update_wrapper(wrapper, func)
         ...
         >>> @print_return
         ... def example():
@@ -465,14 +521,16 @@
         >>> example.__doc__
         'docstring'
 
-    In addition, the boltons version of wraps supports modifying the
-    outer signature based on the inner signature. By passing a list of
+    In addition, the boltons version of update_wrapper supports
+    modifying the outer signature. By passing a list of
     *injected* argument names, those arguments will be removed from
     the outer wrapper's signature, allowing your decorator to provide
     arguments that aren't passed in.
 
     Args:
 
+        wrapper (function) : The callable to which the attributes of
+            *func* are to be copied.
         func (function): The callable whose attributes are to be copied.
         injected (list): An optional list of argument names which
             should not appear in the new wrapper's signature.
@@ -480,14 +538,22 @@
             default) pairs) representing new arguments introduced by
             the wrapper (the opposite of *injected*). See
             :meth:`FunctionBuilder.add_arg()` for more details.
+        build_from (function): The callable from which the new wrapper
+            is built. Defaults to *func*, unless *wrapper* is partial object
+            built from *func*, in which case it defaults to *wrapper*.
+            Useful in some specific cases where *wrapper* and *func* have the
+            same arguments but differ on which are keyword-only and 
positional-only.
         update_dict (bool): Whether to copy other, non-standard
             attributes of *func* over to the wrapper. Defaults to True.
         inject_to_varkw (bool): Ignore missing arguments when a
             ``**kwargs``-type catch-all is present. Defaults to True.
+        hide_wrapped (bool): Remove reference to the wrapped function(s)
+            in the updated function.
 
+    In opposition to the built-in :func:`functools.update_wrapper` bolton's
+    version returns a copy of the function and does not modifiy anything in 
place.
     For more in-depth wrapping of functions, see the
-    :class:`FunctionBuilder` type, on which wraps was built.
-
+    :class:`FunctionBuilder` type, on which update_wrapper was built.
     """
     if injected is None:
         injected = []
@@ -506,10 +572,15 @@
 
     update_dict = kw.pop('update_dict', True)
     inject_to_varkw = kw.pop('inject_to_varkw', True)
+    hide_wrapped = kw.pop('hide_wrapped', False)
     if kw:
         raise TypeError('unexpected kwargs: %r' % kw.keys())
 
-    fb = FunctionBuilder.from_func(func)
+    if isinstance(wrapper, functools.partial) and func is wrapper.func:
+        build_from = build_from or wrapper
+
+    fb = FunctionBuilder.from_func(build_from or func)
+
     for arg in injected:
         try:
             fb.remove_arg(arg)
@@ -526,14 +597,15 @@
     else:
         fb.body = 'return _call(%s)' % fb.get_invocation_str()
 
-    def wrapper_wrapper(wrapper_func):
-        execdict = dict(_call=wrapper_func, _func=func)
-        fully_wrapped = fb.get_func(execdict, with_dict=update_dict)
-        fully_wrapped.__wrapped__ = func  # ref to the original function (#115)
+    execdict = dict(_call=wrapper, _func=func)
+    fully_wrapped = fb.get_func(execdict, with_dict=update_dict)
 
-        return fully_wrapped
+    if hide_wrapped and hasattr(fully_wrapped, '__wrapped__'):
+        del fully_wrapped.__dict__['__wrapped__']
+    elif not hide_wrapped:
+        fully_wrapped.__wrapped__ = func  # ref to the original function (#115)
 
-    return wrapper_wrapper
+    return fully_wrapped
 
 
 def _parse_wraps_expected(expected):
@@ -766,11 +838,20 @@
         if not callable(func):
             raise TypeError('expected callable object, not %r' % (func,))
 
-        kwargs = {'name': func.__name__,
-                  'doc': func.__doc__,
-                  'module': func.__module__,
-                  'annotations': getattr(func, "__annotations__", {}),
-                  'dict': getattr(func, '__dict__', {})}
+        if isinstance(func, functools.partial):
+            if _IS_PY2:
+                raise ValueError('Cannot build FunctionBuilder instances from 
partials in python 2.')
+            kwargs = {'name': func.func.__name__,
+                      'doc': func.func.__doc__,
+                      'module': getattr(func.func, '__module__', None),  # 
e.g., method_descriptor
+                      'annotations': getattr(func.func, "__annotations__", {}),
+                      'dict': getattr(func.func, '__dict__', {})}
+        else:
+            kwargs = {'name': func.__name__,
+                      'doc': func.__doc__,
+                      'module': getattr(func, '__module__', None),  # e.g., 
method_descriptor
+                      'annotations': getattr(func, "__annotations__", {}),
+                      'dict': getattr(func, '__dict__', {})}
 
         kwargs.update(cls._argspec_to_dict(func))
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/ioutils.py 
new/boltons-20.2.1/boltons/ioutils.py
--- old/boltons-20.0.0/boltons/ioutils.py       2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/ioutils.py       2020-08-12 03:30:44.000000000 
+0200
@@ -167,6 +167,15 @@
         self.seek(pos)
         return val
 
+    def seekable(self):
+        return True
+
+    def readable(self):
+        return True
+
+    def writable(self):
+        return True
+
     __next__ = next
 
     def __len__(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/iterutils.py 
new/boltons-20.2.1/boltons/iterutils.py
--- old/boltons-20.0.0/boltons/iterutils.py     2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/iterutils.py     2020-08-12 03:30:44.000000000 
+0200
@@ -15,8 +15,6 @@
 import time
 import codecs
 import random
-import socket
-import hashlib
 import itertools
 
 try:
@@ -177,6 +175,100 @@
     return
 
 
+def lstrip(iterable, strip_value=None):
+    """Strips values from the beginning of an iterable. Stripped items will
+    match the value of the argument strip_value. Functionality is analigous
+    to that of the method str.lstrip. Returns a list.
+
+    >>> lstrip(['Foo', 'Bar', 'Bam'], 'Foo')
+    ['Bar', 'Bam']
+
+    """
+    return list(lstrip_iter(iterable, strip_value))
+
+
+def lstrip_iter(iterable, strip_value=None):
+    """Strips values from the beginning of an iterable. Stripped items will
+    match the value of the argument strip_value. Functionality is analigous
+    to that of the method str.lstrip. Returns a generator.
+
+    >>> list(lstrip_iter(['Foo', 'Bar', 'Bam'], 'Foo'))
+    ['Bar', 'Bam']
+
+    """
+    iterator = iter(iterable)
+    for i in iterator:
+        if i != strip_value:
+            yield i
+            break
+    for i in iterator:
+        yield i
+
+
+def rstrip(iterable, strip_value=None):
+    """Strips values from the end of an iterable. Stripped items will
+    match the value of the argument strip_value. Functionality is analigous
+    to that of the method str.rstrip. Returns a list.
+
+    >>> rstrip(['Foo', 'Bar', 'Bam'], 'Bam')
+    ['Foo', 'Bar']
+
+    """
+    return list(rstrip_iter(iterable,strip_value))
+
+
+def rstrip_iter(iterable, strip_value=None):
+    """Strips values from the end of an iterable. Stripped items will
+    match the value of the argument strip_value. Functionality is analigous
+    to that of the method str.rstrip. Returns a generator.
+
+    >>> list(rstrip_iter(['Foo', 'Bar', 'Bam'], 'Bam'))
+    ['Foo', 'Bar']
+
+    """
+    iterator = iter(iterable)
+    for i in iterator:
+        if i == strip_value:
+            cache = list()
+            cache.append(i)
+            broken = False
+            for i in iterator:
+                if i == strip_value:
+                    cache.append(i)
+                else:
+                    broken = True
+                    break
+            if not broken: # Return to caller here because the end of the
+                return     # iterator has been reached
+            for t in cache:
+                yield t
+        yield i
+
+
+def strip(iterable, strip_value=None):
+    """Strips values from the beginning and end of an iterable. Stripped items
+    will match the value of the argument strip_value. Functionality is
+    analigous to that of the method str.strip. Returns a list.
+
+    >>> strip(['Fu', 'Foo', 'Bar', 'Bam', 'Fu'], 'Fu')
+    ['Foo', 'Bar', 'Bam']
+
+    """
+    return list(strip_iter(iterable,strip_value))
+
+
+def strip_iter(iterable,strip_value=None):
+    """Strips values from the beginning and end of an iterable. Stripped items
+    will match the value of the argument strip_value. Functionality is
+    analigous to that of the method str.strip. Returns a generator.
+
+    >>> list(strip_iter(['Fu', 'Foo', 'Bar', 'Bam', 'Fu'], 'Fu'))
+    ['Foo', 'Bar', 'Bam']
+
+    """
+    return rstrip_iter(lstrip_iter(iterable,strip_value),strip_value)
+
+
 def chunked(src, size, count=None, **kw):
     """Returns a list of *count* chunks, each with *size* elements,
     generated from iterable *src*. If *src* is not evenly divisible by
@@ -615,10 +707,10 @@
 def redundant(src, key=None, groups=False):
     """The complement of :func:`unique()`.
 
-    By default returns non-unique values as a list of the *first*
-    redundant value in *src*. Pass ``groups=True`` to get groups of
-    all values with redundancies, ordered by position of the first
-    redundant value. This is useful in conjunction with some
+    By default returns non-unique/duplicate values as a list of the
+    *first* redundant value in *src*. Pass ``groups=True`` to get
+    groups of all values with redundancies, ordered by position of the
+    first redundant value. This is useful in conjunction with some
     normalizing *key* function.
 
     >>> redundant([1, 2, 3, 4])
@@ -1176,10 +1268,13 @@
         self.size = size
         if size < 20 or size > 36:
             raise ValueError('expected 20 < size <= 36')
+        import hashlib
+        self._sha1 = hashlib.sha1
         self.count = itertools.count()
         self.reseed()
 
     def reseed(self):
+        import socket
         self.pid = os.getpid()
         self.salt = '-'.join([str(self.pid),
                               socket.gethostname() or b'<nohostname>',
@@ -1198,14 +1293,14 @@
             if os.getpid() != self.pid:
                 self.reseed()
             target_bytes = (self.salt + str(next(self.count))).encode('utf8')
-            hash_text = hashlib.sha1(target_bytes).hexdigest()[:self.size]
+            hash_text = self._sha1(target_bytes).hexdigest()[:self.size]
             return hash_text
     else:
         def __next__(self):
             if os.getpid() != self.pid:
                 self.reseed()
-            return hashlib.sha1(self.salt +
-                                str(next(self.count))).hexdigest()[:self.size]
+            return self._sha1(self.salt +
+                              str(next(self.count))).hexdigest()[:self.size]
 
     next = __next__
 
@@ -1240,13 +1335,13 @@
     if _IS_PY3:
         def reseed(self):
             super(SequentialGUIDerator, self).reseed()
-            start_str = hashlib.sha1(self.salt.encode('utf8')).hexdigest()
+            start_str = self._sha1(self.salt.encode('utf8')).hexdigest()
             self.start = int(start_str[:self.size], 16)
             self.start |= (1 << ((self.size * 4) - 2))
     else:
         def reseed(self):
             super(SequentialGUIDerator, self).reseed()
-            start_str = hashlib.sha1(self.salt).hexdigest()
+            start_str = self._sha1(self.salt).hexdigest()
             self.start = int(start_str[:self.size], 16)
             self.start |= (1 << ((self.size * 4) - 2))
 
@@ -1307,6 +1402,47 @@
         last = sorted([x for x in seq if key(x) in last], key=lambda x: 
last.index(key(x)))
     return first + other + last
 
+
+def untyped_sorted(iterable, key=None, reverse=False):
+    """A version of :func:`sorted` which will happily sort an iterable of
+    heterogenous types and return a new list, similar to legacy Python's
+    behavior.
+
+    >>> untyped_sorted(['abc', 2.0, 1, 2, 'def'])
+    [1, 2.0, 2, 'abc', 'def']
+
+    Note how mutually orderable types are sorted as expected, as in
+    the case of the integers and floats above.
+
+    .. note::
+
+       Results may vary across Python versions and builds, but the
+       function will produce a sorted list, except in the case of
+       explicitly unorderable objects.
+
+    """
+    class _Wrapper(object):
+        slots = ('obj',)
+
+        def __init__(self, obj):
+            self.obj = obj
+
+        def __lt__(self, other):
+            obj = key(self.obj) if key is not None else self.obj
+            other = key(other.obj) if key is not None else other.obj
+            try:
+                ret = obj < other
+            except TypeError:
+                ret = ((type(obj).__name__, id(type(obj)), obj)
+                        < (type(other).__name__, id(type(other)), other))
+            return ret
+
+    if key is not None and not callable(key):
+        raise TypeError('expected function or callable object for key, not: %r'
+                        % key)
+
+    return sorted(iterable, key=_Wrapper, reverse=reverse)
+
 """
 May actually be faster to do an isinstance check for a str path
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/listutils.py 
new/boltons-20.2.1/boltons/listutils.py
--- old/boltons-20.0.0/boltons/listutils.py     2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/listutils.py     2020-08-12 03:30:44.000000000 
+0200
@@ -199,7 +199,7 @@
         return cls(it)
 
     def __iter__(self):
-        return chain(*self.lists)
+        return chain.from_iterable(self.lists)
 
     def __reversed__(self):
         return chain.from_iterable(reversed(l) for l in reversed(self.lists))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/setutils.py 
new/boltons-20.2.1/boltons/setutils.py
--- old/boltons-20.0.0/boltons/setutils.py      2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/setutils.py      2020-08-12 03:30:44.000000000 
+0200
@@ -143,6 +143,18 @@
             real_index += d_stop - d_start
         return real_index
 
+    def _get_apparent_index(self, index):
+        if index < 0:
+            index += len(self)
+        if not self.dead_indices:
+            return index
+        apparent_index = index
+        for d_start, d_stop in self.dead_indices:
+            if index < d_start:
+                break
+            apparent_index -= d_stop - d_start
+        return apparent_index
+
     def _add_dead(self, start, stop=None):
         # TODO: does not handle when the new interval subsumes
         # multiple existing intervals
@@ -293,9 +305,13 @@
 
     __or__  = __ror__  = union
     __and__ = __rand__ = intersection
-    __sub__ = __rsub__ = difference
+    __sub__ = difference
     __xor__ = __rxor__ = symmetric_difference
 
+    def __rsub__(self, other):
+        vals = [x for x in other if x not in self]
+        return type(other)(vals)
+
     # in-place set operations
     def update(self, *others):
         "update(*others) -> add values from one or more iterables"
@@ -419,7 +435,7 @@
     def index(self, val):
         "index(val) -> get the index of a value, raises if not present"
         try:
-            return self.item_index_map[val]
+            return self._get_apparent_index(self.item_index_map[val])
         except KeyError:
             cn = self.__class__.__name__
             raise ValueError('%r is not in %s' % (val, cn))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/socketutils.py 
new/boltons-20.2.1/boltons/socketutils.py
--- old/boltons-20.0.0/boltons/socketutils.py   2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/socketutils.py   2020-08-12 03:30:44.000000000 
+0200
@@ -635,23 +635,32 @@
 
     def setmaxsize(self, maxsize):
         self.maxsize = maxsize
-        self._msgsize_maxsize = len(str(maxsize)) + 1  # len(str()) == log10
+        self._msgsize_maxsize = self._calc_msgsize_maxsize(maxsize)
+
+    def _calc_msgsize_maxsize(self, maxsize):
+        return len(str(maxsize)) + 1  # len(str()) == log10
 
     def read_ns(self, timeout=_UNSET, maxsize=_UNSET):
         if timeout is _UNSET:
             timeout = self.timeout
 
+        if maxsize is _UNSET:
+            maxsize = self.maxsize
+            msgsize_maxsize = self._msgsize_maxsize
+        else:
+            msgsize_maxsize = self._calc_msgsize_maxsize(maxsize)
+
         size_prefix = self.bsock.recv_until(b':',
-                                            timeout=self.timeout,
-                                            maxsize=self._msgsize_maxsize)
+                                            timeout=timeout,
+                                            maxsize=msgsize_maxsize)
         try:
             size = int(size_prefix)
         except ValueError:
             raise NetstringInvalidSize('netstring message size must be valid'
                                        ' integer, not %r' % size_prefix)
 
-        if size > self.maxsize:
-            raise NetstringMessageTooLong(size, self.maxsize)
+        if size > maxsize:
+            raise NetstringMessageTooLong(size, maxsize)
         payload = self.bsock.recv_size(size)
         if self.bsock.recv(1) != b',':
             raise NetstringProtocolError("expected trailing ',' after message")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/strutils.py 
new/boltons-20.2.1/boltons/strutils.py
--- old/boltons-20.0.0/boltons/strutils.py      2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/strutils.py      2020-08-12 03:30:44.000000000 
+0200
@@ -36,13 +36,18 @@
     from html.parser import HTMLParser
     from html import entities as htmlentitydefs
 
+try:
+    import __builtin__ as builtins
+except ImportError:
+    import builtins
 
 __all__ = ['camel2under', 'under2camel', 'slugify', 'split_punct_ws',
            'unit_len', 'ordinalize', 'cardinalize', 'pluralize', 'singularize',
            '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', 
'unwrap_text']
+           'args2cmd', 'args2sh', 'parse_int_list', 'format_int_list',
+           'int_list_complement', 'int_list_to_int_tuples', 'unwrap_text']
 
 
 _punct_ws_str = string.punctuation + string.whitespace
@@ -281,14 +286,14 @@
             'ox': 'oxen', 'paralysis': 'paralyses', 'parenthesis': 
'parentheses',
             'person': 'people', 'phenomenon': 'phenomena', 'potato': 
'potatoes',
             'radius': 'radii', 'scarf': 'scarves', 'scissors': 'scissors',
-            'self': 'selves', 'series': 'series', 'sheep': 'sheep',
-            'shelf': 'shelves', 'species': 'species', 'stimulus': 'stimuli',
-            'stratum': 'strata', 'syllabus': 'syllabi', 'symposium': 
'symposia',
-            'synopsis': 'synopses', 'synthesis': 'syntheses', 'tableau': 
'tableaux',
-            'that': 'those', 'thesis': 'theses', 'thief': 'thieves',
-            'this': 'these', 'tomato': 'tomatoes', 'tooth': 'teeth',
-            'torpedo': 'torpedoes', 'vertebra': 'vertebrae', 'veto': 'vetoes',
-            'vita': 'vitae', 'watch': 'watches', 'wife': 'wives',
+            'self': 'selves', 'sense': 'senses', 'series': 'series', 'sheep':
+            'sheep', 'shelf': 'shelves', 'species': 'species', 'stimulus':
+            'stimuli', 'stratum': 'strata', 'syllabus': 'syllabi', 'symposium':
+            'symposia', 'synopsis': 'synopses', 'synthesis': 'syntheses',
+            'tableau': 'tableaux', 'that': 'those', 'thesis': 'theses',
+            'thief': 'thieves', 'this': 'these', 'tomato': 'tomatoes', 'tooth':
+            'teeth', 'torpedo': 'torpedoes', 'vertebra': 'vertebrae', 'veto':
+            'vetoes', 'vita': 'vitae', 'watch': 'watches', 'wife': 'wives',
             'wolf': 'wolves', 'woman': 'women'}
 
 
@@ -334,9 +339,22 @@
     return '%s%s%s' % (string[0], len(string[1:-1]), string[-1])
 
 
-ANSI_ESCAPE_BEGIN = '\x1b['
-ANSI_TERMINATORS = ('H', 'f', 'A', 'B', 'C', 'D', 'R', 's', 'u', 'J',
-                    'K', 'h', 'l', 'p', 'm')
+# Based on https://en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences
+ANSI_SEQUENCES = re.compile(r'''
+    \x1B            # Sequence starts with ESC, i.e. hex 0x1B
+    (?:
+        [@-Z\\-_]   # Second byte:
+                    #   all 0x40???0x5F range but CSI char, i.e ASCII 
@A???Z\]^_
+    |               # Or
+        \[          # CSI sequences, starting with [
+        [0-?]*      # Parameter bytes:
+                    #   range 0x30???0x3F, ASCII 0???9:;<=>?
+        [ -/]*      # Intermediate bytes:
+                    #   range 0x20???0x2F, ASCII space and !"#$%&'()*+,-./
+        [@-~]       # Final byte
+                    #   range 0x40???0x7E, ASCII @A???Z[\]^_`a???z{|}~
+    )
+''', re.VERBOSE)
 
 
 def strip_ansi(text):
@@ -344,33 +362,38 @@
     time when a log or redirected output accidentally captures console
     color codes and the like.
 
-    >>> strip_ansi('\x1b[0m\x1b[1;36mart\x1b[46;34m\xdc')
+    >>> strip_ansi('\x1b[0m\x1b[1;36mart\x1b[46;34m')
     'art'
 
-    The test above is an excerpt from ANSI art on
-    `sixteencolors.net`_. This function does not interpret or render
-    ANSI art, but you can do so with `ansi2img`_ or `escapes.js`_.
+    Supports unicode, str, bytes and bytearray content as input. Returns the
+    same type as the input.
+
+    There's a lot of ANSI art available for testing on `sixteencolors.net`_.
+    This function does not interpret or render ANSI art, but you can do so with
+    `ansi2img`_ or `escapes.js`_.
 
     .. _sixteencolors.net: http://sixteencolors.net
     .. _ansi2img: http://www.bedroomlan.org/projects/ansi2img
     .. _escapes.js: https://github.com/atdt/escapes.js
     """
     # TODO: move to cliutils.py
-    nansi, keep, i, text_len = [], True, 0, len(text)
-    while i < text_len:
-        if not keep and text[i] in ANSI_TERMINATORS:
-            keep = True
-        elif keep:
-            keep_end_i = text.find(ANSI_ESCAPE_BEGIN, i)
-            if keep_end_i < 0:
-                break
-            else:
-                nansi.append(text[i:keep_end_i])
-                i, keep = keep_end_i, False
-        i += 1
-    if not nansi:
-        return text
-    return type(text)().join(nansi)  # attempted unicode + str support
+
+    # Transform any ASCII-like content to unicode to allow regex to match, and
+    # save input type for later.
+    target_type = None
+    # Unicode type aliased to str is code-smell for Boltons in Python 3 env.
+    is_py3 = (unicode == builtins.str)
+    if is_py3 and isinstance(text, (bytes, bytearray)):
+        target_type = type(text)
+        text = text.decode('utf-8')
+
+    cleaned = ANSI_SEQUENCES.sub('', text)
+
+    # Transform back the result to the same bytearray type provided by the 
user.
+    if target_type and target_type != type(cleaned):
+        cleaned = target_type(cleaned, 'utf-8')
+
+    return cleaned
 
 
 def asciify(text, ignore=False):
@@ -980,6 +1003,129 @@
     return output_str
 
 
+def complement_int_list(
+        range_string, range_start=0, range_end=None,
+        delim=',', range_delim='-'):
+    """ Returns range string that is the complement of the one provided as
+    *range_string* parameter.
+
+    These range strings are of the kind produce by :func:`format_int_list`, and
+    parseable by :func:`parse_int_list`.
+
+    Args:
+        range_string (str): String of comma separated positive integers or
+           ranges (e.g. '1,2,4-6,8'). Typical of a custom page range string
+           used in printer dialogs.
+        range_start (int): A positive integer from which to start the resulting
+           range. Value is inclusive. Defaults to ``0``.
+        range_end (int): A positive integer from which the produced range is
+           stopped. Value is exclusive. Defaults to the maximum value found in
+           the provided ``range_string``.
+        delim (char): Defaults to ','. Separates integers and contiguous ranges
+           of integers.
+        range_delim (char): Defaults to '-'. Indicates a contiguous range of
+           integers.
+
+    >>> complement_int_list('1,3,5-8,10-11,15')
+    '0,2,4,9,12-14'
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_start=0)
+    '0,2,4,9,12-14'
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_start=1)
+    '2,4,9,12-14'
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_start=2)
+    '2,4,9,12-14'
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_start=3)
+    '4,9,12-14'
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_end=15)
+    '0,2,4,9,12-14'
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_end=14)
+    '0,2,4,9,12-13'
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_end=13)
+    '0,2,4,9,12'
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_end=20)
+    '0,2,4,9,12-14,16-19'
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_end=0)
+    ''
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_start=-1)
+    '0,2,4,9,12-14'
+
+    >>> complement_int_list('1,3,5-8,10-11,15', range_end=-1)
+    ''
+
+    >>> complement_int_list('1,3,5-8', range_start=1, range_end=1)
+    ''
+
+    >>> complement_int_list('1,3,5-8', range_start=2, range_end=2)
+    ''
+
+    >>> complement_int_list('1,3,5-8', range_start=2, range_end=3)
+    '2'
+
+    >>> complement_int_list('1,3,5-8', range_start=-10, range_end=-5)
+    ''
+
+    >>> complement_int_list('1,3,5-8', range_start=20, range_end=10)
+    ''
+
+    >>> complement_int_list('')
+    ''
+    """
+    int_list = set(parse_int_list(range_string, delim, range_delim))
+    if range_end is None:
+        if int_list:
+            range_end = max(int_list) + 1
+        else:
+            range_end = range_start
+    complement_values = set(
+        range(range_end)) - int_list - set(range(range_start))
+    return format_int_list(complement_values, delim, range_delim)
+
+
+def int_ranges_from_int_list(range_string, delim=',', range_delim='-'):
+    """ Transform a string of ranges (*range_string*) into a tuple of tuples.
+
+    Args:
+        range_string (str): String of comma separated positive integers or
+           ranges (e.g. '1,2,4-6,8'). Typical of a custom page range string
+           used in printer dialogs.
+        delim (char): Defaults to ','. Separates integers and contiguous ranges
+           of integers.
+        range_delim (char): Defaults to '-'. Indicates a contiguous range of
+           integers.
+
+    >>> int_ranges_from_int_list('1,3,5-8,10-11,15')
+    ((1, 1), (3, 3), (5, 8), (10, 11), (15, 15))
+
+    >>> int_ranges_from_int_list('1')
+    ((1, 1),)
+
+    >>> int_ranges_from_int_list('')
+    ()
+    """
+    int_tuples = []
+    # Normalize the range string to our internal format for processing.
+    range_string = format_int_list(
+        parse_int_list(range_string, delim, range_delim))
+    if range_string:
+        for bounds in range_string.split(','):
+            if '-' in bounds:
+                start, end = bounds.split('-')
+            else:
+                start, end = bounds, bounds
+            int_tuples.append((int(start), int(end)))
+    return tuple(int_tuples)
+
+
 class MultiReplace(object):
     """
     MultiReplace is a tool for doing multiple find/replace actions in one pass.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/boltons/urlutils.py 
new/boltons-20.2.1/boltons/urlutils.py
--- old/boltons-20.0.0/boltons/urlutils.py      2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/boltons/urlutils.py      2020-08-12 03:30:44.000000000 
+0200
@@ -393,7 +393,7 @@
 
 
 class URL(object):
-    """The URL is one of the most ubiquitous data structures in the
+    r"""The URL is one of the most ubiquitous data structures in the
     virtual and physical landscape. From blogs to billboards, URLs are
     so common, that it's easy to overlook their complexity and
     power.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/docs/conf.py 
new/boltons-20.2.1/docs/conf.py
--- old/boltons-20.0.0/docs/conf.py     2020-01-09 00:32:03.000000000 +0100
+++ new/boltons-20.2.1/docs/conf.py     2020-08-12 03:30:44.000000000 +0200
@@ -100,8 +100,8 @@
 copyright = u'2020, Mahmoud Hashemi'
 author = u'Mahmoud Hashemi'
 
-version = '20.0'
-release = '20.0.0'
+version = '20.2'
+release = '20.2.1'
 
 if os.name != 'nt':
     today_fmt = '%B %d, %Y'
@@ -112,7 +112,7 @@
 pygments_style = 'sphinx'
 
 # Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'python': ('https://docs.python.org/2.7', None)}
+intersphinx_mapping = {'python': ('https://docs.python.org/', None)}
 
 
 # -- Options for HTML output ----------------------------------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/docs/iterutils.rst 
new/boltons-20.2.1/docs/iterutils.rst
--- old/boltons-20.0.0/docs/iterutils.rst       2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/docs/iterutils.rst       2020-08-12 03:30:44.000000000 
+0200
@@ -16,8 +16,6 @@
 counterparts comprising several common patterns of iteration not
 present in the standard library.
 
-.. autofunction:: split
-.. autofunction:: split_iter
 .. autofunction:: chunked
 .. autofunction:: chunked_iter
 .. autofunction:: pairwise
@@ -26,6 +24,22 @@
 .. autofunction:: windowed_iter
 .. autofunction:: unique
 .. autofunction:: unique_iter
+.. autofunction:: redundant
+
+Stripping and splitting
+-----------------------
+
+A couple of :class:`str`-inspired mechanics that have come in handy on
+iterables, too:
+
+.. autofunction:: split
+.. autofunction:: split_iter
+.. autofunction:: strip
+.. autofunction:: strip_iter
+.. autofunction:: lstrip
+.. autofunction:: lstrip_iter
+.. autofunction:: rstrip
+.. autofunction:: rstrip_iter
 
 Nested
 ------
@@ -75,6 +89,7 @@
 partially override the sort order?
 
 .. autofunction:: soft_sorted
+.. autofunction:: untyped_sorted
 
 Reduction
 ---------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/misc/linkify_changelog.py 
new/boltons-20.2.1/misc/linkify_changelog.py
--- old/boltons-20.0.0/misc/linkify_changelog.py        2020-01-09 
00:32:03.000000000 +0100
+++ new/boltons-20.2.1/misc/linkify_changelog.py        2020-08-12 
03:30:44.000000000 +0200
@@ -6,8 +6,8 @@
 BASE_RTD_URL = 'http://boltons.readthedocs.org/en/latest/'
 BASE_ISSUES_URL = 'https://github.com/mahmoud/boltons/issues/'
 
-_issues_re = re.compile('#(\d+)')
-_member_re = re.compile('((\w+utils)\.[a-zA-Z0-9_.]+)')
+_issues_re = re.compile(r'#(\d+)')
+_member_re = re.compile(r'((\w+utils)\.[a-zA-Z0-9_.]+)')
 
 URL_MAP = {}
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/misc/table_html_app.py 
new/boltons-20.2.1/misc/table_html_app.py
--- old/boltons-20.0.0/misc/table_html_app.py   2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/misc/table_html_app.py   2020-08-12 03:30:44.000000000 
+0200
@@ -88,7 +88,10 @@
         self.autotable_render = AutoTableRenderer()
 
     def render_response(self, request, context, _route):
-        from collections import Sized
+        try:
+            from collections.abc import Sized
+        except ImportError:
+            from collections import Sized
         if isinstance(context, basestring):  # already serialized
             if self._guess_json(context):
                 return Response(context, mimetype="application/json")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/setup.py new/boltons-20.2.1/setup.py
--- old/boltons-20.0.0/setup.py 2020-01-09 00:32:03.000000000 +0100
+++ new/boltons-20.2.1/setup.py 2020-08-12 03:30:44.000000000 +0200
@@ -13,7 +13,7 @@
 
 
 __author__ = 'Mahmoud Hashemi'
-__version__ = '20.0.0'
+__version__ = '20.2.1'
 __contact__ = 'mahm...@hatnote.com'
 __url__ = 'https://github.com/mahmoud/boltons'
 __license__ = 'BSD'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/tests/conftest.py 
new/boltons-20.2.1/tests/conftest.py
--- old/boltons-20.0.0/tests/conftest.py        2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/tests/conftest.py        2020-08-12 03:30:44.000000000 
+0200
@@ -2,7 +2,7 @@
 import re
 
 
-_VERSION_MARKER = re.compile('_py(?P<major_version>\d)(?P<minor_version>\d)?')
+_VERSION_MARKER = re.compile(r'_py(?P<major_version>\d)(?P<minor_version>\d)?')
 
 
 def pytest_ignore_collect(path, config):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/tests/test_dictutils.py 
new/boltons-20.2.1/tests/test_dictutils.py
--- old/boltons-20.0.0/tests/test_dictutils.py  2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/tests/test_dictutils.py  2020-08-12 03:30:44.000000000 
+0200
@@ -88,10 +88,14 @@
 
 
 def test_types():
-    import collections
+    try:
+        from collections.abc import MutableMapping
+    except ImportError:
+        from collections import MutableMapping
+
     omd = OMD()
     assert isinstance(omd, dict)
-    assert isinstance(omd, collections.MutableMapping)
+    assert isinstance(omd, MutableMapping)
 
 
 def test_multi_correctness():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/tests/test_funcutils.py 
new/boltons-20.2.1/tests/test_funcutils.py
--- old/boltons-20.0.0/tests/test_funcutils.py  2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/tests/test_funcutils.py  2020-08-12 03:30:44.000000000 
+0200
@@ -34,6 +34,15 @@
     assert g.cached_partial_greet() == 'Hello...'
     assert CachedInstancePartial(g.greet, excitement='s')() == 'Hellos'
 
+    g.native_greet = 'native reassigned'
+    assert g.native_greet == 'native reassigned'
+
+    g.partial_greet = 'partial reassigned'
+    assert g.partial_greet == 'partial reassigned'
+
+    g.cached_partial_greet = 'cached_partial reassigned'
+    assert g.cached_partial_greet == 'cached_partial reassigned'
+
 
 def test_copy_function():
     def callee():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/tests/test_funcutils_fb.py 
new/boltons-20.2.1/tests/test_funcutils_fb.py
--- old/boltons-20.0.0/tests/test_funcutils_fb.py       2020-01-09 
00:32:03.000000000 +0100
+++ new/boltons-20.2.1/tests/test_funcutils_fb.py       2020-08-12 
03:30:44.000000000 +0200
@@ -1,5 +1,4 @@
 import pytest
-
 from boltons.funcutils import wraps, FunctionBuilder
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/tests/test_funcutils_fb_py3.py 
new/boltons-20.2.1/tests/test_funcutils_fb_py3.py
--- old/boltons-20.0.0/tests/test_funcutils_fb_py3.py   2020-01-09 
00:32:03.000000000 +0100
+++ new/boltons-20.2.1/tests/test_funcutils_fb_py3.py   2020-08-12 
03:30:44.000000000 +0200
@@ -1,10 +1,16 @@
 
 import inspect
+import functools
 from collections import defaultdict
 
 import pytest
 
-from boltons.funcutils import wraps, FunctionBuilder
+from boltons.funcutils import wraps, FunctionBuilder, update_wrapper
+import boltons.funcutils as funcutils
+
+
+def wrappable_varkw_func(a, b, **kw):
+    return a, b
 
 
 def pita_wrap(flag=False):
@@ -47,6 +53,14 @@
         True, 'kwonly_non_roundtrippable_repr', 2)
 
 
+@pytest.mark.parametrize('partial_kind', (functools, funcutils))
+def test_update_wrapper_partial(partial_kind):
+    wrapper = partial_kind.partial(wrappable_varkw_func, b=1)
+
+    fully_wrapped = update_wrapper(wrapper, wrappable_varkw_func)
+    assert fully_wrapped(1) == (1, 1)
+
+
 def test_remove_kwonly_arg():
     # example adapted from https://github.com/mahmoud/boltons/issues/123
 
@@ -187,3 +201,43 @@
 
     assert fb.get_invocation_str() == invocation_str
     assert fb.get_sig_str() == sig_str
+
+
+def test_wraps_inner_kwarg_only():
+    """from https://github.com/mahmoud/boltons/issues/261
+
+    mh responds to the issue:
+
+    You'll notice that when kw-only args are involved the first time
+    (wraps(f)(g)) it works fine. The other way around, however,
+    wraps(g)(f) fails, because by the very nature of funcutils.wraps,
+    you're trying to give f the same signature as g. And f's signature
+    is not like g's. g supports positional b and f() does not.
+
+    If you want to make a wrapper which converts a keyword-only
+    argument to one that can be positional or keyword only, that'll
+    require a different approach for now.
+
+    A potential fix would be to pass all function arguments as
+    keywords. But doubt that's the right direction, because, while I
+    have yet to add positional argument only support, that'll
+    definitely throw a wrench into things.
+    """
+    from boltons.funcutils import wraps
+
+    def g(a: float, b=10):
+        return a * b
+
+    def f(a: int,  *, b=1):
+        return a * b
+
+    # all is well here...
+    assert f(3) == 3
+    assert g(3) == 30
+    assert wraps(f)(g)(3) == 3  # yay, g got the f default (not so with 
functools.wraps!)
+
+    # but this doesn't work
+    with pytest.raises(TypeError):
+        wraps(g)(f)(3)
+
+    return
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/tests/test_funcutils_fb_py37.py 
new/boltons-20.2.1/tests/test_funcutils_fb_py37.py
--- old/boltons-20.0.0/tests/test_funcutils_fb_py37.py  2020-01-09 
00:32:03.000000000 +0100
+++ new/boltons-20.2.1/tests/test_funcutils_fb_py37.py  2020-08-12 
03:30:44.000000000 +0200
@@ -5,6 +5,10 @@
 from boltons.funcutils import wraps, FunctionBuilder
 
 
+def wrappable_func(a, b):
+    return a, b
+
+
 def test_wraps_async():
     # from https://github.com/mahmoud/boltons/issues/194
     import asyncio
@@ -49,3 +53,21 @@
     # lol windows py37 somehow completes this in under 0.3
     # "assert 0.29700000000002547 > 0.3" 
https://ci.appveyor.com/project/mahmoud/boltons/builds/22261051/job/3jfq1tq2233csqp6
     assert duration > 0.25
+
+
+def test_wraps_hide_wrapped():
+    new_func = wraps(wrappable_func, injected='b')(lambda a: wrappable_func(a, 
b=1))
+    new_sig = inspect.signature(new_func, follow_wrapped=True)
+
+    assert list(new_sig.parameters.keys()) == ['a', 'b']
+
+    new_func = wraps(wrappable_func, injected='b', hide_wrapped=True)(lambda 
a: wrappable_func(a, b=1))
+    new_sig = inspect.signature(new_func, follow_wrapped=True)
+
+    assert list(new_sig.parameters.keys()) == ['a']
+
+    new_func = wraps(wrappable_func, injected='b')(lambda a: wrappable_func(a, 
b=1))
+    new_new_func = wraps(new_func, injected='a', hide_wrapped=True)(lambda: 
new_func(a=1))
+    new_new_sig = inspect.signature(new_new_func, follow_wrapped=True)
+
+    assert len(new_new_sig.parameters) == 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/tests/test_iterutils.py 
new/boltons-20.2.1/tests/test_iterutils.py
--- old/boltons-20.0.0/tests/test_iterutils.py  2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/tests/test_iterutils.py  2020-08-12 03:30:44.000000000 
+0200
@@ -509,3 +509,29 @@
     from boltons.iterutils import chunked
 
     assert chunked(b'123', 2) in (['12', '3'], [b'12', b'3'])
+
+
+def test_lstrip():
+    from boltons.iterutils import lstrip
+
+    assert lstrip([0,1,0,2,0,3,0],0) == [1,0,2,0,3,0]
+    assert lstrip([0,0,0,1,0,2,0,3,0],0) == [1,0,2,0,3,0]
+    assert lstrip([]) == []
+
+
+
+def test_rstrip():
+    from boltons.iterutils import rstrip
+
+    assert rstrip([0,1,0,2,0,3,0],0) == [0,1,0,2,0,3]
+    assert rstrip([0,1,0,2,0,3,0,0,0],0) == [0,1,0,2,0,3]
+    assert rstrip([]) == []
+
+
+def test_strip():
+    from boltons.iterutils import strip
+
+    assert strip([0,1,0,2,0,3,0],0) == [1,0,2,0,3]
+    assert strip([0,0,0,1,0,2,0,3,0,0,0],0) == [1,0,2,0,3]
+    assert strip([]) == []
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/tests/test_setutils.py 
new/boltons-20.2.1/tests/test_setutils.py
--- old/boltons-20.0.0/tests/test_setutils.py   2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/tests/test_setutils.py   2020-08-12 03:30:44.000000000 
+0200
@@ -28,6 +28,13 @@
     assert x[2:4:-1] == IndexedSet([8, 7])
 
 
+def test_indexed_set_rsub():
+    "From #252"
+    assert (set('abc') - IndexedSet('bcd')) == set(['a'])
+    assert (IndexedSet('abc') - IndexedSet('bcd')) == IndexedSet(['a'])
+    assert (frozenset('abc') - IndexedSet('bcd')) == frozenset(['a'])
+
+
 def test_indexed_set_mutate():
     thou = IndexedSet(range(1000))
     assert (thou.pop(), thou.pop()) == (999, 998)
@@ -157,3 +164,49 @@
     with raises(TypeError): opsmash(cab, object())
     assert opsmash(ops, cab) == ops
     assert opsmash(cab, ops) == ops
+
+
+def test_iset_index_method():
+    original_list = list(range(8, 20)) + list(range(8))
+
+    indexed_list = IndexedSet()
+
+    for i in original_list:
+        indexed_list.add(i)
+
+    for i in original_list:
+        index = indexed_list.index(i)
+        # if we're removing them in order, the target value should always be 
at index 0
+        assert index == 0
+        indexed_list.pop(index)
+
+    indexed_list = IndexedSet(range(10))
+
+    for i in reversed(range(10)):
+        if i % 2:
+            continue
+        index = indexed_list.index(i)
+        assert i == indexed_list.pop(index)
+
+
+    indexed_list = IndexedSet(range(32))
+
+    for i in list(indexed_list):
+        if i % 3:
+            index = indexed_list.index(i)
+            assert i == indexed_list.pop(index)
+
+    indexed_list = IndexedSet(range(10))
+
+    for i in range(10):
+        if i < 3:
+            continue
+        index = indexed_list.index(i)
+        assert i == indexed_list.pop(index)
+
+    indexed_list = IndexedSet(range(32))
+
+    for i in list(indexed_list):
+        if i % 3:
+            index = indexed_list.index(i)
+            assert i == indexed_list.pop(index)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/tests/test_socketutils.py 
new/boltons-20.2.1/tests/test_socketutils.py
--- old/boltons-20.0.0/tests/test_socketutils.py        2020-01-09 
00:32:03.000000000 +0100
+++ new/boltons-20.2.1/tests/test_socketutils.py        2020-08-12 
03:30:44.000000000 +0200
@@ -248,9 +248,8 @@
 
 def netstring_server(server_socket):
     "A basic netstring server loop, supporting a few operations"
-    running = True
     try:
-        while running:
+        while True:
             clientsock, addr = server_socket.accept()
             client = NetstringSocket(clientsock)
             while 1:
@@ -259,8 +258,7 @@
                     clientsock.close()
                     break
                 elif request == b'shutdown':
-                    running = False
-                    break
+                    return
                 elif request == b'reply4k':
                     client.write_ns(b'a' * 4096)
                 elif request == b'ping':
@@ -272,7 +270,6 @@
     except Exception as e:
         print(u'netstring_server exiting with error: %r' % e)
         raise
-    return
 
 
 def test_socketutils_netstring():
@@ -376,3 +373,57 @@
 
     client.write_ns(b'shutdown')
     print("all passed")
+
+
+def netstring_server_timeout_override(server_socket):
+    """Netstring socket has an unreasonably low timeout,
+    however it should be overriden by the `read_ns` argument."""
+
+    try:
+        while True:
+            clientsock, addr = server_socket.accept()
+            client = NetstringSocket(clientsock, timeout=0.01)
+            while 1:
+                request = client.read_ns(1)
+                if request == b'close':
+                    clientsock.close()
+                    break
+                elif request == b'shutdown':
+                    return
+                elif request == b'ping':
+                    client.write_ns(b'pong')
+    except Exception as e:
+        print(u'netstring_server exiting with error: %r' % e)
+        raise
+
+
+def test_socketutils_netstring_timeout():
+    """Tests that server socket timeout is overriden by the argument to read 
call.
+
+    Server has timeout of 10 ms, and we will sleep for 20 ms. If timeout is 
not overriden correctly,
+    a timeout exception will be raised."""
+
+    print("running timeout test")
+
+    # Set up server
+    server_socket = socket.socket()
+    server_socket.bind(('127.0.0.1', 0))  # localhost with ephemeral port
+    server_socket.listen(100)
+    ip, port = server_socket.getsockname()
+    start_server = lambda: netstring_server_timeout_override(server_socket)
+    threading.Thread(target=start_server).start()
+
+    # set up client
+    def client_connect():
+        clientsock = socket.create_connection((ip, port))
+        client = NetstringSocket(clientsock)
+        return client
+
+    # connect, ping-pong
+    client = client_connect()
+    time.sleep(0.02)
+    client.write_ns(b'ping')
+    assert client.read_ns() == b'pong'
+
+    client.write_ns(b'shutdown')
+    print("no timeout occured - all good.")
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/boltons-20.0.0/tests/test_strutils.py 
new/boltons-20.2.1/tests/test_strutils.py
--- old/boltons-20.0.0/tests/test_strutils.py   2020-01-09 00:32:03.000000000 
+0100
+++ new/boltons-20.2.1/tests/test_strutils.py   2020-08-12 03:30:44.000000000 
+0200
@@ -7,6 +7,38 @@
 from boltons import strutils
 
 
+def test_strip_ansi():
+    assert strutils.strip_ansi(
+        '\x1b[0m\x1b[1;36mart\x1b[46;34m\xdc') == 'art\xdc'
+    assert strutils.strip_ansi(
+        u'\x1b[0m\x1b[1;36mart\x1b[46;34m\xdc') == u'art??'
+    assert strutils.strip_ansi(
+        u'????????????????????????\n??? \x1b[1mCell\x1b[0m 
???\n????????????????????????') == (
+            u'????????????????????????\n'
+            u'??? Cell ???\n'
+            u'????????????????????????')
+    assert strutils.strip_ansi(
+        u'ls\r\n\x1B[00m\x1b[01;31mfile.zip\x1b[00m\r\n\x1b[01;31m') == \
+        u'ls\r\nfile.zip\r\n'
+    assert strutils.strip_ansi(
+        u'\t\u001b[0;35mIP\u001b[0m\t\u001b[0;36m192.1.0.2\u001b[0m') == \
+        u'\tIP\t192.1.0.2'
+    assert strutils.strip_ansi(u'(??????????)?????? \x1b[1m?????????\x1b[0m') 
== (
+        u'(??????????)?????? ?????????')
+    assert strutils.strip_ansi('(??????????)?????? \x1b[1m?????????\x1b[0m') 
== (
+        '(??????????)?????? ?????????')
+    assert strutils.strip_ansi(
+        b'(\xe2\x95\xaf\xc2\xb0\xe2\x96\xa1\xc2\xb0)\xe2\x95\xaf\xef\xb8'
+        b'\xb5 \x1b[1m\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x1b[0m') == (
+            b'(\xe2\x95\xaf\xc2\xb0\xe2\x96\xa1\xc2\xb0)\xe2\x95\xaf\xef\xb8'
+            b'\xb5 \xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb')
+    assert strutils.strip_ansi(
+        bytearray(u'(??????????)?????? \x1b[1m?????????\x1b[0m', 'utf-8')) == \
+        bytearray(
+            b'(\xe2\x95\xaf\xc2\xb0\xe2\x96\xa1\xc2\xb0)\xe2\x95\xaf\xef\xb8'
+            b'\xb5 \xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb')
+
+
 def test_asciify():
     ref = u'Beyonc??'
     b = strutils.asciify(ref)

Reply via email to