]> granicus.if.org Git - python/commitdiff
inspect.signature: Add support for 'functools.partialmethod' #20223
authorYury Selivanov <yselivanov@sprymix.com>
Mon, 27 Jan 2014 22:28:37 +0000 (17:28 -0500)
committerYury Selivanov <yselivanov@sprymix.com>
Mon, 27 Jan 2014 22:28:37 +0000 (17:28 -0500)
Lib/functools.py
Lib/inspect.py
Lib/test/test_inspect.py

index 1e79b31140b91b64a38da118d1fdc8d06b974c08..2b77f7866a573fd566d987421e82d9cc2fd21145 100644 (file)
@@ -290,6 +290,7 @@ class partialmethod(object):
             call_args = (cls_or_self,) + self.args + tuple(rest)
             return self.func(*call_args, **call_keywords)
         _method.__isabstractmethod__ = self.__isabstractmethod__
+        _method._partialmethod = self
         return _method
 
     def __get__(self, obj, cls):
index 15584c1cb3241f0212c257e10de8b6482df660c0..9436e352763bea2a581534da0da49485087d9980 100644 (file)
@@ -1440,6 +1440,51 @@ def _get_user_defined_method(cls, method_name):
             return meth
 
 
+def _get_partial_signature(wrapped_sig, partial, extra_args=()):
+    new_params = OrderedDict(wrapped_sig.parameters.items())
+
+    partial_args = partial.args or ()
+    partial_keywords = partial.keywords or {}
+
+    if extra_args:
+        partial_args = extra_args + partial_args
+
+    try:
+        ba = wrapped_sig.bind_partial(*partial_args, **partial_keywords)
+    except TypeError as ex:
+        msg = 'partial object {!r} has incorrect arguments'.format(partial)
+        raise ValueError(msg) from ex
+
+    for arg_name, arg_value in ba.arguments.items():
+        param = new_params[arg_name]
+        if arg_name in partial_keywords:
+            # We set a new default value, because the following code
+            # is correct:
+            #
+            #   >>> def foo(a): print(a)
+            #   >>> print(partial(partial(foo, a=10), a=20)())
+            #   20
+            #   >>> print(partial(partial(foo, a=10), a=20)(a=30))
+            #   30
+            #
+            # So, with 'partial' objects, passing a keyword argument is
+            # like setting a new default value for the corresponding
+            # parameter
+            #
+            # We also mark this parameter with '_partial_kwarg'
+            # flag.  Later, in '_bind', the 'default' value of this
+            # parameter will be added to 'kwargs', to simulate
+            # the 'functools.partial' real call.
+            new_params[arg_name] = param.replace(default=arg_value,
+                                                 _partial_kwarg=True)
+
+        elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and
+                        not param._partial_kwarg):
+            new_params.pop(arg_name)
+
+    return wrapped_sig.replace(parameters=new_params.values())
+
+
 def signature(obj):
     '''Get a signature object for the passed callable.'''
 
@@ -1470,50 +1515,32 @@ def signature(obj):
         if sig is not None:
             return sig
 
+    try:
+        partialmethod = obj._partialmethod
+    except AttributeError:
+        pass
+    else:
+        # Unbound partialmethod (see functools.partialmethod)
+        # This means, that we need to calculate the signature
+        # as if it's a regular partial object, but taking into
+        # account that the first positional argument
+        # (usually `self`, or `cls`) will not be passed
+        # automatically (as for boundmethods)
+
+        wrapped_sig = signature(partialmethod.func)
+        sig = _get_partial_signature(wrapped_sig, partialmethod, (None,))
+
+        first_wrapped_param = tuple(wrapped_sig.parameters.values())[0]
+        new_params = (first_wrapped_param,) + tuple(sig.parameters.values())
+
+        return sig.replace(parameters=new_params)
+
     if isinstance(obj, types.FunctionType):
         return Signature.from_function(obj)
 
     if isinstance(obj, functools.partial):
-        sig = signature(obj.func)
-
-        new_params = OrderedDict(sig.parameters.items())
-
-        partial_args = obj.args or ()
-        partial_keywords = obj.keywords or {}
-        try:
-            ba = sig.bind_partial(*partial_args, **partial_keywords)
-        except TypeError as ex:
-            msg = 'partial object {!r} has incorrect arguments'.format(obj)
-            raise ValueError(msg) from ex
-
-        for arg_name, arg_value in ba.arguments.items():
-            param = new_params[arg_name]
-            if arg_name in partial_keywords:
-                # We set a new default value, because the following code
-                # is correct:
-                #
-                #   >>> def foo(a): print(a)
-                #   >>> print(partial(partial(foo, a=10), a=20)())
-                #   20
-                #   >>> print(partial(partial(foo, a=10), a=20)(a=30))
-                #   30
-                #
-                # So, with 'partial' objects, passing a keyword argument is
-                # like setting a new default value for the corresponding
-                # parameter
-                #
-                # We also mark this parameter with '_partial_kwarg'
-                # flag.  Later, in '_bind', the 'default' value of this
-                # parameter will be added to 'kwargs', to simulate
-                # the 'functools.partial' real call.
-                new_params[arg_name] = param.replace(default=arg_value,
-                                                     _partial_kwarg=True)
-
-            elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and
-                            not param._partial_kwarg):
-                new_params.pop(arg_name)
-
-        return sig.replace(parameters=new_params.values())
+        wrapped_sig = signature(obj.func)
+        return _get_partial_signature(wrapped_sig, obj)
 
     sig = None
     if isinstance(obj, type):
index 484e0dc0a5f13595ea632ac1266417b7d517ffa9..f5f18f0f218d89ce5ae9ff582763beb87d09f405 100644 (file)
@@ -1877,6 +1877,33 @@ class TestSignatureObject(unittest.TestCase):
         ba = inspect.signature(_foo).bind(12, 14)
         self.assertEqual(_foo(*ba.args, **ba.kwargs), (12, 14, 13))
 
+    def test_signature_on_partialmethod(self):
+        from functools import partialmethod
+
+        class Spam:
+            def test():
+                pass
+            ham = partialmethod(test)
+
+        with self.assertRaisesRegex(ValueError, "has incorrect arguments"):
+            inspect.signature(Spam.ham)
+
+        class Spam:
+            def test(it, a, *, c) -> 'spam':
+                pass
+            ham = partialmethod(test, c=1)
+
+        self.assertEqual(self.signature(Spam.ham),
+                         ((('it', ..., ..., 'positional_or_keyword'),
+                           ('a', ..., ..., 'positional_or_keyword'),
+                           ('c', 1, ..., 'keyword_only')),
+                          'spam'))
+
+        self.assertEqual(self.signature(Spam().ham),
+                         ((('a', ..., ..., 'positional_or_keyword'),
+                           ('c', 1, ..., 'keyword_only')),
+                          'spam'))
+
     def test_signature_on_decorated(self):
         import functools