diff -r 5c0ee973a39a -r 71ad0d817106 Lib/inspect.py --- a/Lib/inspect.py Sun Jun 03 14:40:32 2012 -0700 +++ b/Lib/inspect.py Tue Jun 19 15:22:35 2012 -0400 @@ -22,12 +22,14 @@ getouterframes(), getinnerframes() - get info about frames currentframe() - get the current stack frame stack(), trace() - get info about frames on the stack or in a traceback + + signature() - get a Signature object for the callable """ # 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 @@ -39,8 +41,9 @@ import tokenize import types import warnings +import functools 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 +1226,645 @@ if generator.gi_frame.f_lasti == -1: return GEN_CREATED return GEN_SUSPENDED + + +############################################################################### +### Function Signature Object (PEP 362) +############################################################################### + + +_WrapperDescriptor = type(type.__call__) +_MethodWrapper = type(all.__call__) + +_NonUserDefinedCallables = (_WrapperDescriptor, + _MethodWrapper, + types.BuiltinFunctionType) + + +def _get_user_defined_method(cls, method_name): + try: + meth = getattr(cls, method_name) + except AttributeError: + return + else: + if not isinstance(meth, _NonUserDefinedCallables): + # Once '__signature__' will be added to 'C'-level + # callables, this check won't be necessary + return meth + + +def signature(obj): + '''Get a signature object for the passed callable.''' + + if not callable(obj): + raise TypeError('{!r} is not a callable object'.format(obj)) + + if isinstance(obj, (types.MethodType, classmethod)): + # In this case we skip the first parameter of the underlying + # function (usually `self` or `cls`). + sig = signature(obj.__func__) + sig.parameters.popitem(last=False) + return sig + + try: + sig = obj.__signature__ + except AttributeError: + pass + else: + if sig is not None: + return sig.__copy__() + + try: + # Was this function wrapped by a decorator? + wrapped = obj.__wrapped__ + except AttributeError: + pass + else: + return signature(wrapped) + + if isinstance(obj, types.FunctionType): + return Signature(obj) + + if isinstance(obj, staticmethod): + return signature(obj.__func__) + + if isinstance(obj, functools.partial): + sig = signature(obj.func) + + partial_args = obj.args or () + partial_keywords = obj.keywords or {} + try: + ba = sig.bind_partial(*partial_args, **partial_keywords) + except TypeError as ex: + raise ValueError('partial object {!r} has incorrect arguments'. \ + format(obj)) from ex + + for arg_name, arg_value in ba.arguments.items(): + param = sig.parameters[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 + param.default = arg_value + # 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. + param.__partial_kwarg__ = True + + elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and + not getattr(param, '__partial_kwarg__', False)): + sig.parameters.pop(arg_name) + + return sig + + sig = None + if isinstance(obj, type): + # obj is a class or a metaclass + + # First, let's see if it has an overloaded __call__ defined + # in its metaclass + call = _get_user_defined_method(type(obj), '__call__') + if call is not None: + sig = signature(call) + else: + # Now we check if the 'obj' class has a '__new__' method + new = _get_user_defined_method(obj, '__new__') + if new is not None: + if isinstance(new, staticmethod): + new = new.__func__ + sig = signature(new) + else: + # Finally, we should have at least __init__ implemented + init = _get_user_defined_method(obj, '__init__') + if init is not None: + sig = signature(init) + elif not isinstance(obj, _NonUserDefinedCallables): + # An object with __call__ + # We also check that the 'obj' is not an instance of + # _WrapperDescriptor or _MethodWrapper to avoid + # infinite recursion (and even potential segfault) + call = _get_user_defined_method(type(obj), '__call__') + if call is not None: + sig = signature(call) + + if sig is not None: + # For classes and objects we skip the first parameter of their + # __call__, __new__, or __init__ methods + sig.parameters.popitem(last=False) + return sig + + if isinstance(obj, types.BuiltinFunctionType): + # Raise a nicer error message for builtins + raise ValueError('no signature found for builtin ' + 'function {!r}'.format(obj)) from None + + raise ValueError('callable {!r} is not supported by signature'.format(obj)) + + +class _void: + '''A private marker - used in Parameter''' + + +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. + * kind : str + Describes how argument values are bound to the parameter. + Possible values: `Parameter.POSITIONAL_ONLY`, + `Parameter.POSITIONAL_OR_KEYWORD`, `Parameter.VAR_POSITIONAL`, + `Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`. + ''' + + POSITIONAL_ONLY = "positional_only" + POSITIONAL_OR_KEYWORD = "positional_or_keyword" + VAR_POSITIONAL = "var_positional" + KEYWORD_ONLY = "keyword_only" + VAR_KEYWORD = "var_keyword" + + def __init__(self, name, *, kind, default=_void, annotation=_void): + + self.name = name + if default is not _void: + self.default = default + if annotation is not _void: + self.annotation = annotation + if kind not in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD, + _VAR_POSITIONAL, _KEYWORD_ONLY, _VAR_KEYWORD): + raise ValueError("invalid value for 'Parameter.kind' attribute") + self.kind = kind + + def __repr__(self): + return '<{} at {:#x} {!r}>'.format(self.__class__.__name__, + id(self), self.name) + + def __copy__(self): + cls = type(self) + copy = cls.__new__(cls) + copy.__dict__.update(self.__dict__) + return copy + + def __eq__(self, other): + return (issubclass(other.__class__, Parameter) and + self.name == other.name and + self.kind == other.kind and + getattr(self, 'default', _void) == + getattr(other, 'default', _void) and + getattr(self, 'annotation', _void) == + getattr(other, 'annotation', _void)) + + def __ne__(self, other): + return not self.__eq__(other) + + +_POSITIONAL_ONLY = Parameter.POSITIONAL_ONLY +_POSITIONAL_OR_KEYWORD = Parameter.POSITIONAL_OR_KEYWORD +_VAR_POSITIONAL = Parameter.VAR_POSITIONAL +_KEYWORD_ONLY = Parameter.KEYWORD_ONLY +_VAR_KEYWORD = Parameter.VAR_KEYWORD + + +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. + * signature : Signature + The Signature object that created this instance. + * 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(): + # Is this has a default value that was assigned to it + # by 'functools.partial'? + partial = getattr(param, '__partial_kwarg__', False) + + if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or partial: + # We ignore keyword arguments mapped by 'functools.partial'. + # Those will go to 'BoundArguments.kwargs' + # var_keyword & keyword_only arguments are handled by + # 'BoundArguments.kwargs' too + break + + try: + arg = self.arguments[param_name] + except KeyError: + # We're done here. Other arguments + # will be mapped in 'BoundArguments.kwargs' + break + else: + if param.kind == _VAR_POSITIONAL: + # *args + args.extend(arg) + else: + # plain argument + args.append(arg) + + return tuple(args) + + @property + def kwargs(self): + kwargs = {} + kwargs_started = False + for param_name, param in self.signature.parameters.items(): + partial = getattr(param, '__partial_kwarg__', False) + if not kwargs_started: + if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or partial: + kwargs_started = True + else: + if param_name not in self.arguments: + kwargs_started = True + continue + + if not kwargs_started and not partial: + continue + + try: + arg = self.arguments[param_name] + except KeyError: + continue + else: + if param.kind == _VAR_KEYWORD: + # **kwargs + kwargs.update(arg) + else: + # plain keyword argument + kwargs[param_name] = arg + + return kwargs + + def __eq__(self, other): + return (issubclass(other.__class__, BoundArguments) and + self.signature == other.signature and + self.arguments == other.arguments) + + def __ne__(self, other): + return not self.__eq__(other) + + +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: + + * 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`). + * 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. + * bind(*args, **kwargs) -> BoundArguments + Creates a mapping from positional and keyword arguments to + parameters. + * bind_partial(*args, **kwargs) -> BoundArguments + Creates a partial mapping from positional and keyword arguments + to parameters (simulating 'functools.partial' behavior.) + ''' + + _parameter_cls = Parameter + _bound_arguments_cls = BoundArguments + + def __init__(self, func): + if not isinstance(func, types.FunctionType): + 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, + kind=_POSITIONAL_OR_KEYWORD) + + # ... w/ defaults. + for offset, name in enumerate(positional[non_default_count:]): + annotation = annotations.get(name, _void) + parameters[name] = Parameter(name, annotation=annotation, + kind=_POSITIONAL_OR_KEYWORD, + default=defaults[offset]) + + # *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, + kind=_VAR_POSITIONAL) + + # 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, annotation=annotation, + kind=_KEYWORD_ONLY, + default=default) + # **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, + kind=_VAR_KEYWORD) + + self.parameters = parameters + + # Return annotation. + try: + self.return_annotation = annotations['return'] + except KeyError: + pass + + def __copy__(self): + cls = type(self) + sig = cls.__new__(cls) + sig.parameters = OrderedDict((name, param.__copy__()) + for name, param in self.parameters.items()) + try: + sig.return_annotation = self.return_annotation + except AttributeError: + pass + return sig + + def __eq__(self, other): + if (not issubclass(type(other), Signature) or + getattr(self, 'return_annotation', _void) != + getattr(other, 'return_annotation', _void) or + len(self.parameters) != len(other.parameters)): + return False + + other_positions = {param: idx + for idx, param in enumerate(other.parameters.keys())} + + for idx, (param_name, param) in enumerate(self.parameters.items()): + if param.kind == _KEYWORD_ONLY: + try: + other_param = other.parameters[param_name] + except KeyError: + return False + else: + if param != other_param: + return False + else: + try: + other_idx = other_positions[param_name] + except KeyError: + return False + else: + if idx != other_idx or param != other.parameters[param_name]: + return False + + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def _bind(self, args, kwargs, *, partial=False): + '''Private method. Don't use directly.''' + + arguments = OrderedDict() + + parameters = iter(self.parameters.values()) + parameters_ex = () + arg_vals = iter(args) + + if partial: + # Support for binding arguments to 'functools.partial' objects. + # See 'functools.partial' case in 'signature()' implementation + # for details. + for param_name, param in self.parameters.items(): + if (getattr(param, '__partial_kwarg__', False) and + param_name not in kwargs): + # Simulating 'functools.partial' behavior + kwargs[param_name] = param.default + + 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.kind == _VAR_POSITIONAL: + # That's OK, just empty *args. Let's start parsing + # kwargs + break + elif param.name in kwargs: + if param.kind == _POSITIONAL_ONLY: + raise TypeError('{arg!r} parameter is positional only, ' + 'but was passed as a keyword'. \ + format(arg=param.name)) from None + parameters_ex = (param,) + break + elif param.kind == _VAR_KEYWORD or hasattr(param, 'default'): + # 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: + if partial: + parameters_ex = (param,) + break + else: + raise TypeError('{arg!r} parameter lacking default' + ' value'.format(arg=param.name)) from None + else: + # We have a positional argument to process + try: + param = next(parameters) + except StopIteration: + raise TypeError('too many positional arguments') from None + else: + if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY): + # Looks like we have no parameter for this positional + # argument + raise TypeError('too many positional arguments') + + if param.kind == _VAR_POSITIONAL: + # 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 TypeError('multiple values for argument ' + '{arg!r}'.format(arg=param.name)) + + 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.kind == _POSITIONAL_ONLY: + raise TypeError('{arg!r} parameter is positional only, ' + 'but was passed as a keyword'. \ + format(arg=param.name)) + + if param.kind == _VAR_KEYWORD: + # 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 partial and param.kind != _VAR_POSITIONAL and + not hasattr(param, 'default')): + raise TypeError('{arg!r} parameter lacking default value'. \ + format(arg=param_name)) from None + 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 TypeError('too many keyword arguments') + + return self._bound_arguments_cls(self, arguments) + + def bind(self, *args, **kwargs): + '''Get a BoundArguments object, that maps the passed `args` + and `kwargs` to the function's signature. Raises `TypeError` + if the passed arguments can not be bound. + ''' + return self._bind(args, kwargs) + + def bind_partial(self, *args, **kwargs): + '''Get a BoundArguments object, that partially maps the + passed `args` and `kwargs` to the function's signature. + Raises `TypeError` if the passed arguments can not be bound. + ''' + return self._bind(args, kwargs, partial=True) + + def __str__(self): + result = [] + render_kw_only_separator = True + for param in self.parameters.values(): + formatted = param.name + + # Add annotation and default value + try: + annotatation = param.annotation + except AttributeError: + pass + else: + formatted = '{}:{}'.format(formatted, + formatannotation(annotatation)) + + try: + default = param.default + except AttributeError: + pass + else: + formatted = '{}={}'.format(formatted, repr(default)) + + kind = param.kind + if kind == _VAR_POSITIONAL: + # OK, we have an '*args'-like parameter, so we won't need + # a '*' to separate keyword-only arguments + render_kw_only_separator = False + formatted = '*' + formatted + + elif kind == _VAR_KEYWORD: + formatted = '**' + formatted + + elif kind == _KEYWORD_ONLY and render_kw_only_separator: + # We have a keyword-only parameter to render and we haven't + # rendered an '*args'-like parameter before, so add a '*' + # separator to the parameters list ("foo(arg1, *, arg2)" case) + result.append('*') + # This condition should be only triggered once, so + # reset the flag + render_kw_only_separator = False + + result.append(formatted) + + rendered = '({})'.format(', '.join(result)) + + try: + ra = self.return_annotation + except AttributeError: + pass + else: + rendered += ' -> {}'.format(formatannotation(ra)) + + return rendered diff -r 5c0ee973a39a -r 71ad0d817106 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 19 15:22:35 2012 -0400 @@ -1170,13 +1170,775 @@ self.assertIn(name, str(state)) +class TestSignatureObject(unittest.TestCase): + @staticmethod + def signature(func): + sig = inspect.signature(func) + return (tuple((param.name, + getattr(param, 'default', ...), + getattr(param, 'annotation', ...), + param.kind) for param in sig.parameters.values()), + getattr(sig, 'return_annotation', ...)) + + def test_signature_on_noarg(self): + def test(): + pass + self.assertEqual(self.signature(test), ((), ...)) + + def test_signature_on_wargs(self): + def test(a, b:'foo') -> 123: + pass + self.assertEqual(self.signature(test), ((('a', ..., ..., "positional_or_keyword"), + ('b', ..., 'foo', "positional_or_keyword")), + 123)) + + def test_signature_on_wkwonly(self): + def test(*, a:float, b:str) -> int: + pass + self.assertEqual(self.signature(test), ((('a', ..., float, "keyword_only"), + ('b', ..., str, "keyword_only")), + int)) + + def test_signature_on_complex_args(self): + def test(a, b:'foo'=10, *args:'bar', spam:'baz', ham=123, **kwargs:int): + pass + self.assertEqual(self.signature(test), ((('a', ..., ..., "positional_or_keyword"), + ('b', 10, 'foo', "positional_or_keyword"), + ('args', ..., 'bar', "var_positional"), + ('spam', ..., 'baz', "keyword_only"), + ('ham', 123, ..., "keyword_only"), + ('kwargs', ..., int, "var_keyword")), + ...)) + + def test_signature_on_builtin_function(self): + with self.assertRaisesRegexp(ValueError, 'is not supported by signature'): + inspect.signature(type) + with self.assertRaisesRegexp(ValueError, 'is not supported by signature'): + # support for 'wrapper_descriptor' + inspect.signature(type.__call__) + with self.assertRaisesRegexp(ValueError, 'is not supported by signature'): + # support for 'method-wrapper' + inspect.signature(min.__call__) + with self.assertRaisesRegexp(ValueError, 'no signature found for builtin function'): + # support for 'method-wrapper' + inspect.signature(min) + + def test_signature_on_non_function(self): + with self.assertRaisesRegexp(TypeError, 'is not a callable object'): + inspect.signature(42) + + with self.assertRaisesRegexp(TypeError, 'is not a Python function'): + inspect.Signature(42) + + def test_signature_on_method(self): + class Test: + def foo(self, arg1, arg2=1) -> int: + pass + + meth = Test().foo + + self.assertEqual(self.signature(meth), ((('arg1', ..., ..., "positional_or_keyword"), + ('arg2', 1, ..., "positional_or_keyword")), + int)) + + def test_signature_on_classmethod(self): + class Test: + @classmethod + def foo(cls, arg1, *, arg2=1): + pass + + meth = Test().foo + self.assertEqual(self.signature(meth), ((('arg1', ..., ..., "positional_or_keyword"), + ('arg2', 1, ..., "keyword_only")), + ...)) + + meth = Test.foo + self.assertEqual(self.signature(meth), ((('arg1', ..., ..., "positional_or_keyword"), + ('arg2', 1, ..., "keyword_only")), + ...)) + + def test_signature_on_staticmethod(self): + class Test: + @staticmethod + def foo(cls, *, arg): + pass + + meth = Test().foo + self.assertEqual(self.signature(meth), ((('cls', ..., ..., "positional_or_keyword"), + ('arg', ..., ..., "keyword_only")), + ...)) + + meth = Test.foo + self.assertEqual(self.signature(meth), ((('cls', ..., ..., "positional_or_keyword"), + ('arg', ..., ..., "keyword_only")), + ...)) + + def test_signature_on_partial(self): + from functools import partial + + def test(): + pass + + self.assertEqual(self.signature(partial(test)), ((), ...)) + + with self.assertRaisesRegexp(ValueError, "has incorrect arguments"): + inspect.signature(partial(test, 1)) + + with self.assertRaisesRegexp(ValueError, "has incorrect arguments"): + inspect.signature(partial(test, a=1)) + + def test(a, b, *, c, d): + pass + + self.assertEqual(self.signature(partial(test)), + ((('a', ..., ..., "positional_or_keyword"), + ('b', ..., ..., "positional_or_keyword"), + ('c', ..., ..., "keyword_only"), + ('d', ..., ..., "keyword_only")), + ...)) + + self.assertEqual(self.signature(partial(test, 1)), + ((('b', ..., ..., "positional_or_keyword"), + ('c', ..., ..., "keyword_only"), + ('d', ..., ..., "keyword_only")), + ...)) + + self.assertEqual(self.signature(partial(test, 1, c=2)), + ((('b', ..., ..., "positional_or_keyword"), + ('c', 2, ..., "keyword_only"), + ('d', ..., ..., "keyword_only")), + ...)) + + self.assertEqual(self.signature(partial(test, b=1, c=2)), + ((('a', ..., ..., "positional_or_keyword"), + ('b', 1, ..., "positional_or_keyword"), + ('c', 2, ..., "keyword_only"), + ('d', ..., ..., "keyword_only")), + ...)) + + self.assertEqual(self.signature(partial(test, 0, b=1, c=2)), + ((('b', 1, ..., "positional_or_keyword"), + ('c', 2, ..., "keyword_only"), + ('d', ..., ..., "keyword_only"),), + ...)) + + def test(a, *args, b, **kwargs): + pass + + self.assertEqual(self.signature(partial(test, 1)), + ((('args', ..., ..., "var_positional"), + ('b', ..., ..., "keyword_only"), + ('kwargs', ..., ..., "var_keyword")), + ...)) + + self.assertEqual(self.signature(partial(test, 1, 2, 3)), + ((('args', ..., ..., "var_positional"), + ('b', ..., ..., "keyword_only"), + ('kwargs', ..., ..., "var_keyword")), + ...)) + + + self.assertEqual(self.signature(partial(test, 1, 2, 3, test=True)), + ((('args', ..., ..., "var_positional"), + ('b', ..., ..., "keyword_only"), + ('kwargs', ..., ..., "var_keyword")), + ...)) + + self.assertEqual(self.signature(partial(test, 1, 2, 3, test=1, b=0)), + ((('args', ..., ..., "var_positional"), + ('b', 0, ..., "keyword_only"), + ('kwargs', ..., ..., "var_keyword")), + ...)) + + self.assertEqual(self.signature(partial(test, b=0)), + ((('a', ..., ..., "positional_or_keyword"), + ('args', ..., ..., "var_positional"), + ('b', 0, ..., "keyword_only"), + ('kwargs', ..., ..., "var_keyword")), + ...)) + + self.assertEqual(self.signature(partial(test, b=0, test=1)), + ((('a', ..., ..., "positional_or_keyword"), + ('args', ..., ..., "var_positional"), + ('b', 0, ..., "keyword_only"), + ('kwargs', ..., ..., "var_keyword")), + ...)) + + def test(a, b, c:int) -> 42: + pass + + sig = test.__signature__ = inspect.signature(test) + + self.assertEqual(self.signature(partial(partial(test, 1))), + ((('b', ..., ..., "positional_or_keyword"), + ('c', ..., int, "positional_or_keyword")), + 42)) + + self.assertEqual(self.signature(partial(partial(test, 1), 2)), + ((('c', ..., int, "positional_or_keyword"),), + 42)) + + psig = inspect.signature(partial(partial(test, 1), 2)) + + def foo(a): + return a + _foo = partial(partial(foo, a=10), a=20) + self.assertEqual(self.signature(_foo), + ((('a', 20, ..., "positional_or_keyword"),), + ...)) + # check that we don't have any side-effects in signature(), + # and the partial object is still functioning + self.assertEqual(_foo(), 20) + + def foo(a, b, c): + return a, b, c + _foo = partial(partial(foo, 1, b=20), b=30) + self.assertEqual(self.signature(_foo), + ((('b', 30, ..., "positional_or_keyword"), + ('c', ..., ..., "positional_or_keyword")), + ...)) + self.assertEqual(_foo(c=10), (1, 30, 10)) + _foo = partial(_foo, 2) # now 'b' has two values - positional and keyword + with self.assertRaisesRegexp(ValueError, "has incorrect arguments"): + inspect.signature(_foo) + + def foo(a, b, c, *, d): + return a, b, c, d + _foo = partial(partial(foo, d=20, c=20), b=10, d=30) + self.assertEqual(self.signature(_foo), + ((('a', ..., ..., "positional_or_keyword"), + ('b', 10, ..., "positional_or_keyword"), + ('c', 20, ..., "positional_or_keyword"), + ('d', 30, ..., "keyword_only")), + ...)) + ba = inspect.signature(_foo).bind(a=200, b=11) + self.assertEqual(_foo(*ba.args, **ba.kwargs), (200, 11, 20, 30)) + + def foo(a=1, b=2, c=3): + return a, b, c + _foo = partial(foo, a=10, c=13) + ba = inspect.signature(_foo).bind(11) + self.assertEqual(_foo(*ba.args, **ba.kwargs), (11, 2, 13)) + ba = inspect.signature(_foo).bind(11, 12) + self.assertEqual(_foo(*ba.args, **ba.kwargs), (11, 12, 13)) + ba = inspect.signature(_foo).bind(11, b=12) + self.assertEqual(_foo(*ba.args, **ba.kwargs), (11, 12, 13)) + ba = inspect.signature(_foo).bind(b=12) + self.assertEqual(_foo(*ba.args, **ba.kwargs), (10, 12, 13)) + _foo = partial(_foo, b=10) + ba = inspect.signature(_foo).bind(12, 14) + self.assertEqual(_foo(*ba.args, **ba.kwargs), (12, 14, 13)) + + def test_signature_on_decorated(self): + import functools + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs) -> int: + return func(*args, **kwargs) + return wrapper + + class Foo: + @decorator + def bar(self, a, b): + pass + + self.assertEqual(self.signature(Foo.bar), + ((('self', ..., ..., "positional_or_keyword"), + ('a', ..., ..., "positional_or_keyword"), + ('b', ..., ..., "positional_or_keyword")), + ...)) + + self.assertEqual(self.signature(Foo().bar), + ((('a', ..., ..., "positional_or_keyword"), + ('b', ..., ..., "positional_or_keyword")), + ...)) + + # Test that we handle method wrappers correctly + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs) -> int: + return func(42, *args, **kwargs) + sig = inspect.signature(func) + sig.parameters.popitem(last=False) + wrapper.__signature__ = sig + return wrapper + + class Foo: + @decorator + def __call__(self, a, b): + pass + + self.assertEqual(self.signature(Foo.__call__), + ((('a', ..., ..., "positional_or_keyword"), + ('b', ..., ..., "positional_or_keyword")), + ...)) + + self.assertEqual(self.signature(Foo().__call__), + ((('b', ..., ..., "positional_or_keyword"),), + ...)) + + def test_signature_on_class(self): + class C: + def __init__(self, a): + pass + + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + class CM(type): + def __call__(cls, a): + pass + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + class CM(type): + def __new__(mcls, name, bases, dct, *, foo=1): + return super().__new__(mcls, name, bases, dct) + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(self.signature(C), + ((('b', ..., ..., "positional_or_keyword"),), + ...)) + + self.assertEqual(self.signature(CM), + ((('name', ..., ..., "positional_or_keyword"), + ('bases', ..., ..., "positional_or_keyword"), + ('dct', ..., ..., "positional_or_keyword"), + ('foo', 1, ..., "keyword_only")), + ...)) + + class CMM(type): + def __new__(mcls, name, bases, dct, *, foo=1): + return super().__new__(mcls, name, bases, dct) + def __call__(cls, nm, bs, dt): + return type(nm, bs, dt) + class CM(type, metaclass=CMM): + def __new__(mcls, name, bases, dct, *, bar=2): + return super().__new__(mcls, name, bases, dct) + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(self.signature(CMM), + ((('name', ..., ..., "positional_or_keyword"), + ('bases', ..., ..., "positional_or_keyword"), + ('dct', ..., ..., "positional_or_keyword"), + ('foo', 1, ..., "keyword_only")), + ...)) + + self.assertEqual(self.signature(CM), + ((('nm', ..., ..., "positional_or_keyword"), + ('bs', ..., ..., "positional_or_keyword"), + ('dt', ..., ..., "positional_or_keyword")), + ...)) + + self.assertEqual(self.signature(C), + ((('b', ..., ..., "positional_or_keyword"),), + ...)) + + class CM(type): + def __init__(cls, name, bases, dct, *, bar=2): + return super().__init__(name, bases, dct) + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(self.signature(CM), + ((('name', ..., ..., "positional_or_keyword"), + ('bases', ..., ..., "positional_or_keyword"), + ('dct', ..., ..., "positional_or_keyword"), + ('bar', 2, ..., "keyword_only")), + ...)) + + def test_signature_on_callable_objects(self): + class Foo: + def __call__(self, a): + pass + + self.assertEqual(self.signature(Foo()), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + class Spam: + pass + with self.assertRaisesRegexp(TypeError, "is not a callable object"): + inspect.signature(Spam()) + + class Bar(Spam, Foo): + pass + + self.assertEqual(self.signature(Bar()), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + class ToFail: + __call__ = type + with self.assertRaisesRegexp(ValueError, "is not supported by signature"): + inspect.signature(ToFail()) + + + class Wrapped: + pass + Wrapped.__wrapped__ = lambda a: None + self.assertEqual(self.signature(Wrapped), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + def test_signature_on_lambdas(self): + self.assertEqual(self.signature((lambda a=10: a)), + ((('a', 10, ..., "positional_or_keyword"),), + ...)) + + def test_signature_equality(self): + def foo(a, *, b:int) -> float: pass + self.assertNotEqual(inspect.signature(foo), 42) + + def bar(a, *, b:int) -> float: pass + self.assertEqual(inspect.signature(foo), inspect.signature(bar)) + + def bar(a, *, b:int) -> int: pass + self.assertNotEqual(inspect.signature(foo), inspect.signature(bar)) + + def bar(a, *, b:int): pass + self.assertNotEqual(inspect.signature(foo), inspect.signature(bar)) + + def bar(a, *, b:int=42) -> float: pass + self.assertNotEqual(inspect.signature(foo), inspect.signature(bar)) + + def bar(a, *, c) -> float: pass + self.assertNotEqual(inspect.signature(foo), inspect.signature(bar)) + + def bar(a, b:int) -> float: pass + self.assertNotEqual(inspect.signature(foo), inspect.signature(bar)) + def spam(b:int, a) -> float: pass + self.assertNotEqual(inspect.signature(spam), inspect.signature(bar)) + + def foo(*, a, b, c): pass + def bar(*, c, b, a): pass + self.assertEqual(inspect.signature(foo), inspect.signature(bar)) + + def foo(*, a=1, b, c): pass + def bar(*, c, b, a=1): pass + self.assertEqual(inspect.signature(foo), inspect.signature(bar)) + + def foo(pos, *, a=1, b, c): pass + def bar(pos, *, c, b, a=1): pass + self.assertEqual(inspect.signature(foo), inspect.signature(bar)) + + def foo(pos, *, a, b, c): pass + def bar(pos, *, c, b, a=1): pass + self.assertNotEqual(inspect.signature(foo), inspect.signature(bar)) + + def foo(pos, *args, a=42, b, c, **kwargs:int): pass + def bar(pos, *args, c, b, a=42, **kwargs:int): pass + self.assertEqual(inspect.signature(foo), inspect.signature(bar)) + + def test_signature_unhashable(self): + def foo(a): pass + sig = inspect.signature(foo) + with self.assertRaisesRegexp(TypeError, 'unhashable type'): + hash(sig) + + def test_signature_str(self): + def foo(a:int=1, *, b, c=None, **kwargs) -> 42: + pass + self.assertEqual(str(inspect.signature(foo)), + '(a:int=1, *, b, c=None, **kwargs) -> 42') + + def foo(a:int=1, *args, b, c=None, **kwargs) -> 42: + pass + self.assertEqual(str(inspect.signature(foo)), + '(a:int=1, *args, b, c=None, **kwargs) -> 42') + + def foo(): + pass + self.assertEqual(str(inspect.signature(foo)), '()') + + def test_signature_str_positional_only(self): + def test(a_po, *, b, **kwargs): + return a_po, kwargs + + sig = test.__signature__ = inspect.signature(test) + sig.parameters['a_po'].kind = inspect.Parameter.POSITIONAL_ONLY + self.assertEqual(str(inspect.signature(test)), '(a_po, *, b, **kwargs)') + + +class TestParameterObject(unittest.TestCase): + def test_signature_parameter_object(self): + p = inspect.Parameter('foo', default=10, kind=inspect.Parameter.VAR_KEYWORD) + self.assertEqual(p.name, 'foo') + self.assertEqual(p.default, 10) + self.assertIs(getattr(p, 'annotation', ...), ...) + self.assertEqual(p.kind, inspect.Parameter.VAR_KEYWORD) + + with self.assertRaisesRegexp(ValueError, 'invalid value'): + inspect.Parameter('foo', default=10, kind='123') + + def test_signature_parameter_custom_attr(self): + p = inspect.Parameter('foo', kind=inspect.Parameter.POSITIONAL_OR_KEYWORD) + p.custom_attr = 42 + self.assertEqual(getattr(p, 'custom_attr', None), 42) + + def test_signature_parameter_copy(self): + import copy + p = inspect.Parameter('foo', default=int, kind=inspect.Parameter.VAR_KEYWORD) + p.custom_attr = 42 + + p2 = copy.copy(p) + self.assertFalse(p is p2) + self.assertIs(getattr(p2, 'default', None), int) + self.assertEqual(getattr(p2, 'custom_attr', None), 42) + self.assertEqual(p2.kind, inspect.Parameter.VAR_KEYWORD) + p2.custom_attr = 10 + self.assertEqual(p2.custom_attr, 10) + self.assertEqual(p.custom_attr, 42) + + def test_signature_parameter_equality(self): + p = inspect.Parameter('foo', default=42, + kind=inspect.Parameter.VAR_POSITIONAL) + + self.assertEqual(p, p) + self.assertNotEqual(p, 42) + + self.assertEqual(p, inspect.Parameter('foo', default=42, + kind=inspect.Parameter.VAR_POSITIONAL)) + + import copy + p2 = copy.copy(p) + self.assertEqual(p, p2) + p.custom_attr = 42 + self.assertEqual(p, p2) + + def test_signature_parameter_unhashable(self): + p = inspect.Parameter('foo', default=42, + kind=inspect.Parameter.VAR_POSITIONAL) + + with self.assertRaisesRegexp(TypeError, 'unhashable type'): + hash(p) + + +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(TypeError, 'too many positional arguments'): + self.call(test, 1) + with self.assertRaisesRegexp(TypeError, 'too many positional arguments'): + self.call(test, 1, spam=10) + with self.assertRaisesRegexp(TypeError, '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(TypeError, 'too many positional arguments'): + self.call(test, 1, 2, 3, 4) + + with self.assertRaisesRegexp(TypeError, "'b' parameter lacking default"): + self.call(test, 1) + + with self.assertRaisesRegexp(TypeError, "'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(a=1, b=2, c=3): + return a, b, c + self.assertEqual(self.call(test, a=10, c=13), (10, 2, 13)) + self.assertEqual(self.call(test, a=10), (10, 2, 3)) + self.assertEqual(self.call(test, b=10), (1, 10, 3)) + + 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(TypeError, "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(TypeError, 'too many positional arguments'): + self.call(test, 1) + self.assertEqual(self.call(test, foo=1), 1) + + def test(a, *, foo=1, bar): + return foo + with self.assertRaisesRegexp(TypeError, "'bar' parameter lacking default value"): + self.call(test, 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(TypeError, 'too many keyword arguments'): + self.call(test, bar=2, foo=1, spam=10) + + with self.assertRaisesRegexp(TypeError, 'too many positional arguments'): + self.call(test, 1, 2) + + with self.assertRaisesRegexp(TypeError, 'too many positional arguments'): + self.call(test, 1, 2, bar=2) + + with self.assertRaisesRegexp(TypeError, 'too many keyword arguments'): + self.call(test, 1, bar=2, spam='ham') + + with self.assertRaisesRegexp(TypeError, "'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(TypeError, "'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_signature_bind_positional_only(self): + def test(a_po, b_po, c_po=3, foo=42, *, bar=50, **kwargs): + return a_po, b_po, c_po, foo, bar, kwargs + + sig = test.__signature__ = inspect.signature(test) + sig.parameters['a_po'].kind = inspect.Parameter.POSITIONAL_ONLY + sig.parameters['b_po'].kind = inspect.Parameter.POSITIONAL_ONLY + sig.parameters['c_po'].kind = inspect.Parameter.POSITIONAL_ONLY + + self.assertEqual(self.call(test, 1, 2, 4, 5, bar=6), + (1, 2, 4, 5, 6, {})) + + with self.assertRaisesRegexp(TypeError, "parameter is positional only"): + self.call(test, 1, 2, c_po=4) + + with self.assertRaisesRegexp(TypeError, "parameter is positional only"): + self.call(test, a_po=1, b_po=2) + + +class TestBoundArguments(unittest.TestCase): + def test_signature_bound_arguments_unhashable(self): + def foo(a): pass + ba = inspect.signature(foo).bind(1) + + with self.assertRaisesRegexp(TypeError, 'unhashable type'): + hash(ba) + + def test_signature_bound_arguments_equality(self): + def foo(a): pass + ba = inspect.signature(foo).bind(1) + self.assertEqual(ba, ba) + + ba2 = inspect.signature(foo).bind(1) + self.assertEqual(ba, ba2) + + ba3 = inspect.signature(foo).bind(2) + self.assertNotEqual(ba, ba3) + ba3.arguments['a'] = 1 + self.assertEqual(ba, ba3) + + def bar(b): pass + ba4 = inspect.signature(bar).bind(1) + self.assertNotEqual(ba, ba4) + + def test_main(): run_unittest( TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases, TestInterpreterStack, TestClassesAndFunctions, TestPredicates, TestGetcallargsFunctions, TestGetcallargsMethods, TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState, - TestNoEOL + TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject, + TestBoundArguments ) if __name__ == "__main__":