Repository: mahmoud/boltons

Issue:
funcutils.wraps() not working with partials
Hi!

With boltons 20.0.0 (on Linux), I get errors when trying to wrap partials. My goal is to fix the docstring and signature of the wrapped partial, so it acts as a normal function, with an option to "hide" the given arguments with "injected".

Examples:
```
from functools import partial
from boltons.funcutils import wraps

def func(a, b=1, c=1):
    """Docstring"""
    return (a, b, c)

newf = wraps(func)(partial(func, b=2))
newf(1)
# Expected (1, 2, 1)
# Got : TypeError : func() got multiple values for argument 'b'
```

After inspection, I believe this is due to `partial` modifying the signature of `func` by changing `b` and  all subsequent  args to "keyword-only". Which `wraps` doesn't see as it builds the new function from `func` directly.

```
from inspect import signature

signature(partial(func, b=2)) # <Signature (a, *, b=2, c=1)>
signature(newf, follow_wrapped) # <Signature (a, b=1, c=1)>
``` 

So. First of all, is this a bug of `boltons.funcutils.wraps` or is this just not in the scope of what it is supposed to do?
I could use `functools.update_wrapper` but I really wanted to "inject" the new keywords, make them disappear from the signature. Also, using `help()` on a "updated" partial doesn't work, while it does on  `wraps(func)(partial(func))`.

I quickly modified `wraps` and was able to obtain the wanted behaviour by passing the partial func directly and using that one to create the underlying `FunctionBuilder` instance. It works since the arguments are the same, just with modified properties and defaults, but it kinda doesn't make sense since `wraps` is supposed to be a decorator. Thus a solution I see is to move the code from `wraps` to a new  `update_wrapper`. Example:

```
def update_wrapper(wrapper, wrapped, build_from=None, injected=None, expected=None, **kw):
     ... same code as `wraps` but:
     fb = FunctionBuilder.from_func(build_from or wrapped)
     ... and it returns "fully_wrapped"

def wraps(func, injected=None, expected=None, **kw):
    def wrapper_wrapper(wrapper_func):
        return update_wrapper(wrapper_func, func, injected=injected, expected=expected, **kw)
    return wrapper_wrapper
```

What do you think?
(If needed or wanted, I have time to suggest a PR on this)

Comments:

Patch:
diff --git a/boltons/funcutils.py b/boltons/funcutils.py
index f52a047..9e67787 100644
--- a/boltons/funcutils.py
+++ b/boltons/funcutils.py
@@ -38,6 +38,11 @@ try:
 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 @@ class InstancePartial(functools.partial):
     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 @@ class CachedInstancePartial(functools.partial):
 
     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,6 +294,7 @@ class CachedInstancePartial(functools.partial):
             obj.__dict__[name] = ret = make_method(self, obj, obj_type)
             return ret
 
+
 partial = CachedInstancePartial
 
 
@@ -428,8 +445,45 @@ def format_nonexp_repr(obj, req_names=None, opt_names=None, opt_key=None):
 
 
 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 +491,20 @@ def wraps(func, injected=None, expected=None, **kw):
       * 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 +518,16 @@ def wraps(func, injected=None, expected=None, **kw):
         >>> 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 +535,22 @@ def wraps(func, injected=None, expected=None, **kw):
             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 +569,15 @@ def wraps(func, injected=None, expected=None, **kw):
 
     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 +594,15 @@ def wraps(func, injected=None, expected=None, **kw):
     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 +835,20 @@ class FunctionBuilder(object):
         if not callable(func):
             raise TypeError('expected callable object, not %r' % (func,))
 
-        kwargs = {'name': func.__name__,
-                  'doc': func.__doc__,
-                  'module': getattr(func, '__module__', None),  # e.g., method_descriptor
-                  '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 --git a/boltons/socketutils.py b/boltons/socketutils.py
index 86e7187..d72e2c1 100644
--- a/boltons/socketutils.py
+++ b/boltons/socketutils.py
@@ -635,23 +635,32 @@ class NetstringSocket(object):
 
     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 --git a/boltons/strutils.py b/boltons/strutils.py
index a2ad303..094c3b5 100644
--- a/boltons/strutils.py
+++ b/boltons/strutils.py
@@ -281,14 +281,14 @@ _IRR_S2P = {'addendum': 'addenda', 'alga': 'algae', 'alumna': 'alumnae',
             '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'}
 
 


Test patch:
diff --git a/tests/test_funcutils.py b/tests/test_funcutils.py
index a4b6793..783399d 100644
--- a/tests/test_funcutils.py
+++ b/tests/test_funcutils.py
@@ -34,6 +34,15 @@ def test_partials():
     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 --git a/tests/test_funcutils_fb.py b/tests/test_funcutils_fb.py
index 77e5a6d..05d1582 100644
--- a/tests/test_funcutils_fb.py
+++ b/tests/test_funcutils_fb.py
@@ -1,5 +1,4 @@
 import pytest
-
 from boltons.funcutils import wraps, FunctionBuilder
 
 
diff --git a/tests/test_funcutils_fb_py3.py b/tests/test_funcutils_fb_py3.py
index e6af8e0..1315473 100644
--- a/tests/test_funcutils_fb_py3.py
+++ b/tests/test_funcutils_fb_py3.py
@@ -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 @@ def test_wraps_py3():
         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
 
diff --git a/tests/test_funcutils_fb_py37.py b/tests/test_funcutils_fb_py37.py
index fc2f74b..7b10f72 100644
--- a/tests/test_funcutils_fb_py37.py
+++ b/tests/test_funcutils_fb_py37.py
@@ -5,6 +5,10 @@ import inspect
 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 @@ def test_wraps_async():
     # 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 --git a/tests/test_socketutils.py b/tests/test_socketutils.py
index 90d988d..dd095eb 100644
--- a/tests/test_socketutils.py
+++ b/tests/test_socketutils.py
@@ -248,9 +248,8 @@ def test_timeout_setters_getters():
 
 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 @@ def netstring_server(server_socket):
                     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 @@ def netstring_server(server_socket):
     except Exception as e:
         print(u'netstring_server exiting with error: %r' % e)
         raise
-    return
 
 
 def test_socketutils_netstring():
@@ -376,3 +373,57 @@ def test_socketutils_netstring():
 
     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