[Python-checkins] cpython: inspect.signature: Handle bound methods with '(*args)' signature correctly

yury.selivanov python-checkins at python.org
Tue Jan 28 18:26:42 CET 2014


http://hg.python.org/cpython/rev/3544827d42e6
changeset:   88796:3544827d42e6
user:        Yury Selivanov <yselivanov at sprymix.com>
date:        Tue Jan 28 12:26:24 2014 -0500
summary:
  inspect.signature: Handle bound methods with '(*args)' signature correctly #20401

files:
  Lib/inspect.py           |  39 ++++++++++++++++++++++++---
  Lib/test/test_inspect.py |  25 ++++++++++++++---
  2 files changed, 54 insertions(+), 10 deletions(-)


diff --git a/Lib/inspect.py b/Lib/inspect.py
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -1440,7 +1440,11 @@
             return meth
 
 
-def _get_partial_signature(wrapped_sig, partial, extra_args=()):
+def _signature_get_partial(wrapped_sig, partial, extra_args=()):
+    # Internal helper to calculate how 'wrapped_sig' signature will
+    # look like after applying a 'functools.partial' object (or alike)
+    # on it.
+
     new_params = OrderedDict(wrapped_sig.parameters.items())
 
     partial_args = partial.args or ()
@@ -1485,6 +1489,31 @@
     return wrapped_sig.replace(parameters=new_params.values())
 
 
+def _signature_bound_method(sig):
+    # Internal helper to transform signatures for unbound
+    # functions to bound methods
+
+    params = tuple(sig.parameters.values())
+
+    if not params or params[0].kind in (_VAR_KEYWORD, _KEYWORD_ONLY):
+        raise ValueError('invalid method signature')
+
+    kind = params[0].kind
+    if kind in (_POSITIONAL_OR_KEYWORD, _POSITIONAL_ONLY):
+        # Drop first parameter:
+        # '(p1, p2[, ...])' -> '(p2[, ...])'
+        params = params[1:]
+    else:
+        if kind is not _VAR_POSITIONAL:
+            # Unless we add a new parameter type we never
+            # get here
+            raise ValueError('invalid argument type')
+        # It's a var-positional parameter.
+        # Do nothing. '(*args[, ...])' -> '(*args[, ...])'
+
+    return sig.replace(parameters=params)
+
+
 def signature(obj):
     '''Get a signature object for the passed callable.'''
 
@@ -1502,7 +1531,7 @@
         # In this case we skip the first parameter of the underlying
         # function (usually `self` or `cls`).
         sig = signature(obj.__func__)
-        return sig.replace(parameters=tuple(sig.parameters.values())[1:])
+        return _signature_bound_method(sig)
 
     # Was this function wrapped by a decorator?
     obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__")))
@@ -1528,7 +1557,7 @@
         # automatically (as for boundmethods)
 
         wrapped_sig = signature(partialmethod.func)
-        sig = _get_partial_signature(wrapped_sig, partialmethod, (None,))
+        sig = _signature_get_partial(wrapped_sig, partialmethod, (None,))
 
         first_wrapped_param = tuple(wrapped_sig.parameters.values())[0]
         new_params = (first_wrapped_param,) + tuple(sig.parameters.values())
@@ -1540,7 +1569,7 @@
 
     if isinstance(obj, functools.partial):
         wrapped_sig = signature(obj.func)
-        return _get_partial_signature(wrapped_sig, obj)
+        return _signature_get_partial(wrapped_sig, obj)
 
     sig = None
     if isinstance(obj, type):
@@ -1584,7 +1613,7 @@
     if sig is not None:
         # For classes and objects we skip the first parameter of their
         # __call__, __new__, or __init__ methods
-        return sig.replace(parameters=tuple(sig.parameters.values())[1:])
+        return _signature_bound_method(sig)
 
     if isinstance(obj, types.BuiltinFunctionType):
         # Raise a nicer error message for builtins
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -1669,16 +1669,31 @@
 
     def test_signature_on_method(self):
         class Test:
-            def foo(self, arg1, arg2=1) -> int:
+            def __init__(*args):
                 pass
-
-        meth = Test().foo
-
-        self.assertEqual(self.signature(meth),
+            def m1(self, arg1, arg2=1) -> int:
+                pass
+            def m2(*args):
+                pass
+            def __call__(*, a):
+                pass
+
+        self.assertEqual(self.signature(Test().m1),
                          ((('arg1', ..., ..., "positional_or_keyword"),
                            ('arg2', 1, ..., "positional_or_keyword")),
                           int))
 
+        self.assertEqual(self.signature(Test().m2),
+                         ((('args', ..., ..., "var_positional"),),
+                          ...))
+
+        self.assertEqual(self.signature(Test),
+                         ((('args', ..., ..., "var_positional"),),
+                          ...))
+
+        with self.assertRaisesRegex(ValueError, 'invalid method signature'):
+            self.signature(Test())
+
     def test_signature_on_classmethod(self):
         class Test:
             @classmethod

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list