diff -r 5c0ee973a39a -r 6616f5041fc9 Include/methodobject.h --- a/Include/methodobject.h Sun Jun 03 14:40:32 2012 -0700 +++ b/Include/methodobject.h Tue Jun 05 14:48:00 2012 -0400 @@ -77,6 +77,7 @@ PyMethodDef *m_ml; /* Description of the C function to call */ PyObject *m_self; /* Passed as 'self' arg to the C func, can be NULL */ PyObject *m_module; /* The __module__ attribute, can be anything */ + PyObject *m_signature; /* The __signature__ attribute, cached Signature*/ } PyCFunctionObject; #endif diff -r 5c0ee973a39a -r 6616f5041fc9 Lib/inspect.py --- a/Lib/inspect.py Sun Jun 03 14:40:32 2012 -0700 +++ b/Lib/inspect.py Tue Jun 05 14:48:00 2012 -0400 @@ -26,8 +26,8 @@ # This module is in the public domain. No warranties. -__author__ = 'Ka-Ping Yee ' -__date__ = '1 Jan 2001' +__author__ = ('Ka-Ping Yee ', + 'Yury Selivanov ') import imp import importlib.machinery @@ -40,7 +40,7 @@ import types import warnings from operator import attrgetter -from collections import namedtuple +from collections import namedtuple, OrderedDict # Create constants for the compiler flags in Include/code.h # We try to get them from dis to avoid duplication, but fall @@ -1223,3 +1223,332 @@ if generator.gi_frame.f_lasti == -1: return GEN_CREATED return GEN_SUSPENDED + + +################################################################################ +### Function Signature Object (PEP 362) +################################################################################ + + +def signature(func): + '''Returns a signature object for the passed function.''' + if ismethod(func): + func = func.__func__ + try: + return func.__signature__ + except AttributeError: + sig = Signature(func) + func.__signature__ = sig + return sig + + +class BindError(TypeError): + '''Represents a failure of inspect.Signature.bind()''' + + +_void = object() + +class Parameter: + '''Represents a parameter in a function signature. + + Has the following public attributes: + + * name : str + The name of the parameter as a string. + * default : object + The default value for the parameter if specified. If the + parameter has no default value, this attribute is not set. + * annotation + The annotation for the parameter if specified. If the + parameter has no annotation, this attribute is not set. + * is_keyword_only : bool + True if the parameter is keyword-only, else False. + * is_args : bool + True if the parameter accepts variable number of arguments + (*args-like), else False. + * is_kwargs : bool + True if the parameter accepts variable number of keyword + arguments (**kwargs-like), else False. + * is_implemented : bool + True if the functionality behind the parameter is + implemented on the local platform. (Almost always True.) + ''' + + def __init__(self, name, *, + default=_void, annotation=_void, is_keyword_only=False, + is_args=False, is_kwargs=False, is_implemented=True): + + self.name = name + if default is not _void: + self.default = default + if annotation is not _void: + self.annotation = annotation + self.is_keyword_only = is_keyword_only + self.is_args = is_args + self.is_kwargs = is_kwargs + self.is_implemented = is_implemented + + def __repr__(self): + return '<{} at 0x{:x} {!r}>'.format(self.__class__.__name__, \ + id(self), self.name) + + +class BoundArguments: + '''Result of `Signature.bind` call. Holds the mapping of arguments + to the function's parameters. + + Has the following public attributes: + + * arguments : OrderedDict + An ordered mutable mapping of parameters' names to arguments' values. + Does not contain arguments' default values. + * args : tuple + Tuple of positional arguments values. + * kwargs : dict + Dict of keyword arguments values. + ''' + + def __init__(self, signature, arguments): + self.arguments = arguments + self.signature = signature + + @property + def args(self): + args = [] + for param_name, param in self.signature.parameters.items(): + if param.is_kwargs or param.is_keyword_only: + break + + try: + arg = self.arguments[param_name] + except KeyError: + continue + else: + if param.is_args: + args.extend(arg) + else: + args.append(arg) + + return tuple(args) + + @property + def kwargs(self): + kwargs = {} + for param_name, param in self.signature.parameters.items(): + if not param.is_kwargs and not param.is_keyword_only: + continue + + try: + arg = self.arguments[param_name] + except KeyError: + continue + else: + if param.is_kwargs: + kwargs.update(arg) + else: + kwargs[param_name] = arg + + return kwargs + + +class Signature: + '''A Signature object represents the overall signature of a function. + It stores a Parameter object for each parameter accepted by the + function, as well as information specific to the function itself. + + A Signature object has the following public attributes and methods: + + * name : str + Name of the function. + * qualname : str + Fully qualified name of the function. + * return_annotation : object + The annotation for the return type of the function if specified. + If the function has no annotation for its return type, this + attribute is not set. + * parameters : OrderedDict + An ordered mapping of parameters' names to the corresponding + Parameter objects (keyword-only arguments are in the same order + as listed in `code.co_varnames`). + * bind(*args, **kwargs) -> BoundArguments + Creates a mapping from positional and keyword arguments to + parameters. + ''' + + _parameter_cls = Parameter + _bound_arguments_cls = BoundArguments + + def __init__(self, func): + if ismethod(func): + func = func.__func__ + if not isfunction(func): + raise TypeError('{!r} is not a Python function'.format(func)) + + Parameter = self._parameter_cls + + # Parameter information. + func_code = func.__code__ + pos_count = func_code.co_argcount + arg_names = func_code.co_varnames + positional = tuple(arg_names[:pos_count]) + keyword_only_count = func_code.co_kwonlyargcount + keyword_only = arg_names[pos_count:(pos_count + keyword_only_count)] + annotations = func.__annotations__ + defaults = func.__defaults__ + kwdefaults = func.__kwdefaults__ + + if defaults: + pos_default_count = len(defaults) + else: + pos_default_count = 0 + + parameters = OrderedDict() + + # Non-keyword-only parameters w/o defaults. + non_default_count = pos_count - pos_default_count + for name in positional[:non_default_count]: + annotation = annotations.get(name, _void) + parameters[name] = Parameter(name, annotation=annotation) + + # ... w/ defaults. + for offset, name in enumerate(positional[non_default_count:]): + annotation = annotations.get(name, _void) + parameters[name] = Parameter(name, default=defaults[offset], + annotation=annotation) + + # *args + if func_code.co_flags & 0x04: + name = arg_names[pos_count + keyword_only_count] + annotation = annotations.get(name, _void) + parameters[name] = Parameter(name, annotation=annotation, + is_args=True) + + # Keyword-only parameters. + for name in keyword_only: + default = _void + if kwdefaults is not None: + default = kwdefaults.get(name, _void) + + annotation = annotations.get(name, _void) + parameters[name] = Parameter(name, is_keyword_only=True, + default=default, + annotation=annotation) + # **kwargs + if func_code.co_flags & 0x08: + index = pos_count + keyword_only_count + if func_code.co_flags & 0x04: + index += 1 + + name = arg_names[index] + annotation = annotations.get(name, _void) + parameters[name] = Parameter(name, annotation=annotation, + is_kwargs=True) + + self.name = func.__name__ + self.qualname = func.__qualname__ + self.parameters = parameters + + # Return annotation. + try: + self.return_annotation = annotations['return'] + except KeyError: + pass + + def bind(self, *args, **kwargs): + '''Returns a BoundArguments object, that maps passed args and kwargs to + the function's signature. + ''' + + arguments = OrderedDict() + + parameters = iter(self.parameters.values()) + parameters_ex = () + arg_vals = iter(args) + + while True: + # Let's iterate through the positional arguments and corresponding + # parameters + try: + arg_val = next(arg_vals) + except StopIteration: + # No more positional arguments + try: + param = next(parameters) + except StopIteration: + # No more parameters. That's it. Just need to check that + # we have no kwargs after this while loop + break + else: + if param.is_args: + # That's OK, just empty *args. Let's start parsing + # kwargs + break + elif param.is_kwargs or hasattr(param, 'default') \ + or param.name in kwargs: + # That's fine too - we have a default value for this + # parameter. So, lets start parsing kwargs, starting + # with the current parameter + parameters_ex = (param,) + break + else: + raise BindError('{func}: {arg!r} parameter lacking default value'. \ + format(func=self.qualname, arg=param.name)) + else: + # We have a positional argument to process + try: + param = next(parameters) + except StopIteration: + raise BindError('{func}: too many positional arguments'. \ + format(func=self.qualname)) + else: + if param.is_keyword_only or param.is_kwargs: + # Looks like we have no paameter for this positional argument + raise BindError('{func}: too many positional arguments'. \ + format(func=self.qualname)) + + if param.is_args: + # We have an '*args'-like argument, let's fill it with all + # positional arguments we have left and move on to the next + # phase + values = [arg_val] + values.extend(arg_vals) + arguments[param.name] = tuple(values) + break + + if param.name in kwargs: + raise BindError('{func}: multiple values for argument {arg!r}'. \ + format(arg=param.name, func=self.qualname)) + + arguments[param.name] = arg_val + + # Now, we iterate through the remaining parameters to process + # keyword arguments + kwargs_param = None + for param in itertools.chain(parameters_ex, parameters): + if param.is_kwargs: + # Memorize that we have a '**kwargs'-like parameter + kwargs_param = param + continue + + param_name = param.name + try: + arg_val = kwargs.pop(param_name) + except KeyError: + # We have no value for this parameter. It's fine though, + # if it has a default value, or it is an '*args'-like parameter, + # left alone by the processing of positional arguments. + if not hasattr(param, 'default') and not param.is_args: + raise BindError('{func}: {arg!r} parameter lacking default value'. \ + format(arg=param_name, func=self.qualname)) + else: + arguments[param_name] = arg_val + + if kwargs: + if kwargs_param is not None: + # Process our '**kwargs'-like parameter + arguments[kwargs_param.name] = kwargs + else: + raise BindError('{func}: too many keyword arguments'. \ + format(func=self.qualname)) + + return self._bound_arguments_cls(self, arguments) diff -r 5c0ee973a39a -r 6616f5041fc9 Lib/test/test_inspect.py --- a/Lib/test/test_inspect.py Sun Jun 03 14:40:32 2012 -0700 +++ b/Lib/test/test_inspect.py Tue Jun 05 14:48:00 2012 -0400 @@ -1170,13 +1170,262 @@ self.assertIn(name, str(state)) +class TestSignatureObject(unittest.TestCase): + @staticmethod + def signature(func): + sig = inspect.signature(func) + return (sig.name, + tuple((param.name, + getattr(param, 'default', ...), + getattr(param, 'annotation', ...), + param.is_keyword_only, + param.is_args, + param.is_kwargs) for param in sig.parameters.values()), + getattr(sig, 'return_annotation', ...)) + + def test_signature_object_empty(self): + def test(): + pass + self.assertEqual(self.signature(test), ('test', (), ...)) + + def test_signature_object_args(self): + def test(a, b:'foo') -> 123: + pass + self.assertEqual(self.signature(test), ('test', + (('a', ..., ..., False, False, False), + ('b', ..., 'foo', False, False, False)), + 123)) + + def test_signature_object_kwonly(self): + def test(*, a:float, b:str) -> int: + pass + self.assertEqual(self.signature(test), ('test', + (('a', ..., float, True, False, False), + ('b', ..., str, True, False, False)), + int)) + + def test_signature_object_complex(self): + def test(a, b:'foo'=10, *args:'bar', spam:'baz', ham=123, **kwargs:int): + pass + self.assertEqual(self.signature(test), ('test', + (('a', ..., ..., False, False, False), + ('b', 10, 'foo', False, False, False), + ('args', ..., 'bar', False, True, False), + ('spam', ..., 'baz', True, False, False), + ('ham', 123, ..., True, False, False), + ('kwargs', ..., int, False, False, True)), + ...)) + + def test_signature_object_qualname(self): + def test(): pass + self.assertEqual(test.__qualname__, inspect.signature(test).qualname) + + def test_signature_object_caching(self): + def test(): pass + self.assertIs(inspect.signature(test), inspect.signature(test)) + + def test_signature_object_builtin_function(self): + with self.assertRaisesRegexp(TypeError, 'is not a Python function'): + inspect.signature(type) + + def test_signature_object_non_function(self): + with self.assertRaisesRegexp(TypeError, 'is not a Python function'): + inspect.signature(42) + + def test_signature_object_implemented_param(self): + def test(spam): + pass + sig = inspect.signature(test) + self.assertTrue(sig.parameters['spam'].is_implemented) + sig.parameters['spam'].is_implemented = False + + sig = inspect.signature(test) + self.assertFalse(sig.parameters['spam'].is_implemented) + + def test_signature_on_c_functions(self): + self.assertTrue('__signature__' in dir(os.chmod)) + self.assertEqual(inspect.signature(os.chmod), os.chmod.__signature__) + cached = os.chmod.__signature__ + value = (1, 2, "3", 4) + try: + os.chmod.__signature__ = value + self.assertEqual(inspect.signature(os.chmod), value) + finally: + os.chmod.__signature__ = cached + + def test_signature_object_on_method(self): + class Test: + def foo(self, arg): + pass + + sig1 = inspect.signature(Test().foo) + sig2 = inspect.signature(Test.foo) + + self.assertIs(sig1, sig2) + + self.assertEqual(len(sig1.parameters), 2) + self.assertIn('self', sig1.parameters) + + +BindError = inspect.BindError + +class TestSignatureBind(unittest.TestCase): + @staticmethod + def call(func, *args, **kwargs): + sig = inspect.signature(func) + ba = sig.bind(*args, **kwargs) + return func(*ba.args, **ba.kwargs) + + def test_signature_bind_empty(self): + def test(): + return 42 + + self.assertEqual(self.call(test), 42) + with self.assertRaisesRegexp(BindError, 'too many positional arguments'): + self.call(test, 1) + with self.assertRaisesRegexp(BindError, 'too many positional arguments'): + self.call(test, 1, spam=10) + with self.assertRaisesRegexp(BindError, 'too many keyword arguments'): + self.call(test, spam=1) + + def test_signature_bind_var(self): + def test(*args, **kwargs): + return args, kwargs + + self.assertEqual(self.call(test), ((), {})) + self.assertEqual(self.call(test, 1), ((1,), {})) + self.assertEqual(self.call(test, 1, 2), ((1, 2), {})) + self.assertEqual(self.call(test, foo='bar'), ((), {'foo': 'bar'})) + self.assertEqual(self.call(test, 1, foo='bar'), ((1,), {'foo': 'bar'})) + self.assertEqual(self.call(test, 1, 2, foo='bar'), ((1, 2), {'foo': 'bar'})) + self.assertEqual(self.call(test, args=10), ((), {'args': 10})) + + def test_signature_bind_just_args(self): + def test(a, b, c): + return a, b, c + + self.assertEqual(self.call(test, 1, 2, 3), (1, 2, 3)) + + with self.assertRaisesRegexp(BindError, 'too many positional arguments'): + self.call(test, 1, 2, 3, 4) + + with self.assertRaisesRegexp(BindError, "'b' parameter lacking default"): + self.call(test, 1) + + with self.assertRaisesRegexp(BindError, "'a' parameter lacking default"): + self.call(test) + + def test(a, b, c=10): + return a, b, c + self.assertEqual(self.call(test, 1, 2, 3), (1, 2, 3)) + self.assertEqual(self.call(test, 1, 2), (1, 2, 10)) + + def test_signature_bind_varargs_order(self): + def test(*args): + return args + + self.assertEqual(self.call(test), ()) + self.assertEqual(self.call(test, 1, 2, 3), (1, 2, 3)) + + def test_signature_bind_args_and_varargs(self): + def test(a, b, c=3, *args): + return a, b, c, args + + self.assertEqual(self.call(test, 1, 2, 3, 4, 5), (1, 2, 3, (4, 5))) + self.assertEqual(self.call(test, 1, 2), (1, 2, 3, ())) + self.assertEqual(self.call(test, b=1, a=2), (2, 1, 3, ())) + self.assertEqual(self.call(test, 1, b=2), (1, 2, 3, ())) + + with self.assertRaisesRegexp(BindError, "multiple values for argument 'c'"): + self.call(test, 1, 2, 3, c=4) + + def test_signature_bind_just_kwargs(self): + def test(**kwargs): + return kwargs + + self.assertEqual(self.call(test), {}) + self.assertEqual(self.call(test, foo='bar', spam='ham'), + {'foo': 'bar', 'spam': 'ham'}) + + def test_signature_bind_args_and_kwargs(self): + def test(a, b, c=3, **kwargs): + return a, b, c, kwargs + + self.assertEqual(self.call(test, 1, 2), (1, 2, 3, {})) + self.assertEqual(self.call(test, 1, 2, foo='bar', spam='ham'), + (1, 2, 3, {'foo': 'bar', 'spam': 'ham'})) + self.assertEqual(self.call(test, b=2, a=1, foo='bar', spam='ham'), + (1, 2, 3, {'foo': 'bar', 'spam': 'ham'})) + self.assertEqual(self.call(test, a=1, b=2, foo='bar', spam='ham'), + (1, 2, 3, {'foo': 'bar', 'spam': 'ham'})) + self.assertEqual(self.call(test, 1, b=2, foo='bar', spam='ham'), + (1, 2, 3, {'foo': 'bar', 'spam': 'ham'})) + self.assertEqual(self.call(test, 1, b=2, c=4, foo='bar', spam='ham'), + (1, 2, 4, {'foo': 'bar', 'spam': 'ham'})) + self.assertEqual(self.call(test, 1, 2, 4, foo='bar'), + (1, 2, 4, {'foo': 'bar'})) + self.assertEqual(self.call(test, c=5, a=4, b=3), + (4, 3, 5, {})) + + def test_signature_bind_kwonly(self): + def test(*, foo): + return foo + with self.assertRaisesRegexp(BindError, 'too many positional arguments'): + self.call(test, 1) + self.assertEqual(self.call(test, foo=1), 1) + + def test(foo, *, bar): + return foo, bar + self.assertEqual(self.call(test, 1, bar=2), (1, 2)) + self.assertEqual(self.call(test, bar=2, foo=1), (1, 2)) + + with self.assertRaisesRegexp(BindError, 'too many keyword arguments'): + self.call(test, bar=2, foo=1, spam=10) + + with self.assertRaisesRegexp(BindError, 'too many positional arguments'): + self.call(test, 1, 2) + + with self.assertRaisesRegexp(BindError, 'too many positional arguments'): + self.call(test, 1, 2, bar=2) + + with self.assertRaisesRegexp(BindError, 'too many keyword arguments'): + self.call(test, 1, bar=2, spam='ham') + + with self.assertRaisesRegexp(BindError, "'bar' parameter lacking default value"): + self.call(test, 1) + + def test(foo, *, bar, **bin): + return foo, bar, bin + self.assertEqual(self.call(test, 1, bar=2), (1, 2, {})) + self.assertEqual(self.call(test, foo=1, bar=2), (1, 2, {})) + self.assertEqual(self.call(test, 1, bar=2, spam='ham'), (1, 2, {'spam': 'ham'})) + self.assertEqual(self.call(test, spam='ham', foo=1, bar=2), (1, 2, {'spam': 'ham'})) + with self.assertRaisesRegexp(BindError, "'foo' parameter lacking default value"): + self.call(test, spam='ham', bar=2) + self.assertEqual(self.call(test, 1, bar=2, bin=1, spam=10), + (1, 2, {'bin': 1, 'spam': 10})) + + def test_signature_bind_arguments(self): + def test(a, *args, b, z=100, **kwargs): + pass + sig = inspect.signature(test) + ba = sig.bind(10, 20, b=30, c=40, args=50, kwargs=60) + # we won't have 'z' argument in the bound arguments object, as we didn't + # pass it to the 'bind' + self.assertEqual(tuple(ba.arguments.items()), + (('a', 10), ('args', (20,)), ('b', 30), + ('kwargs', {'c': 40, 'args': 50, 'kwargs': 60}))) + self.assertEqual(ba.args, (10, 20)) + self.assertEqual(ba.kwargs, {'b': 30, 'c': 40, 'args': 50, 'kwargs': 60}) + + def test_main(): run_unittest( TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases, TestInterpreterStack, TestClassesAndFunctions, TestPredicates, TestGetcallargsFunctions, TestGetcallargsMethods, TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState, - TestNoEOL + TestNoEOL, TestSignatureObject, TestSignatureBind ) if __name__ == "__main__": diff -r 5c0ee973a39a -r 6616f5041fc9 Lib/test/test_sys.py --- a/Lib/test/test_sys.py Sun Jun 03 14:40:32 2012 -0700 +++ b/Lib/test/test_sys.py Tue Jun 05 14:48:00 2012 -0400 @@ -657,7 +657,7 @@ # buffer # XXX # builtin_function_or_method - check(len, size(h + '3P')) + check(len, size(h + '4P')) # bytearray samples = [b'', b'u'*100000] for sample in samples: diff -r 5c0ee973a39a -r 6616f5041fc9 Objects/methodobject.c --- a/Objects/methodobject.c Sun Jun 03 14:40:32 2012 -0700 +++ b/Objects/methodobject.c Tue Jun 05 14:48:00 2012 -0400 @@ -33,6 +33,7 @@ op->m_self = self; Py_XINCREF(module); op->m_module = module; + op->m_signature = NULL; _PyObject_GC_TRACK(op); return (PyObject *)op; } @@ -124,6 +125,7 @@ _PyObject_GC_UNTRACK(m); Py_XDECREF(m->m_self); Py_XDECREF(m->m_module); + Py_XDECREF(m->m_signature); if (numfree < PyCFunction_MAXFREELIST) { m->m_self = (PyObject *)free_list; free_list = m; @@ -191,6 +193,7 @@ { Py_VISIT(m->m_self); Py_VISIT(m->m_module); + Py_VISIT(m->m_signature); return 0; } @@ -218,6 +221,7 @@ static PyMemberDef meth_members[] = { {"__module__", T_OBJECT, OFF(m_module), PY_WRITE_RESTRICTED}, + {"__signature__", T_OBJECT, OFF(m_signature), 0}, {NULL} };