Issue #15008: Implement PEP 362 "Signature Objects".

Patch by Yury Selivanov.
This commit is contained in:
Larry Hastings 2012-06-22 15:19:35 -07:00
parent 8e0d25504c
commit 7c7cbfc00f
3 changed files with 1697 additions and 4 deletions

View File

@ -22,12 +22,14 @@ Here are some of the useful functions provided by this module:
getouterframes(), getinnerframes() - get info about frames getouterframes(), getinnerframes() - get info about frames
currentframe() - get the current stack frame currentframe() - get the current stack frame
stack(), trace() - get info about frames on the stack or in a traceback 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. # This module is in the public domain. No warranties.
__author__ = 'Ka-Ping Yee <ping@lfw.org>' __author__ = ('Ka-Ping Yee <ping@lfw.org>',
__date__ = '1 Jan 2001' 'Yury Selivanov <yselivanov@sprymix.com>')
import imp import imp
import importlib.machinery import importlib.machinery
@ -39,8 +41,9 @@ import sys
import tokenize import tokenize
import types import types
import warnings import warnings
import functools
from operator import attrgetter from operator import attrgetter
from collections import namedtuple from collections import namedtuple, OrderedDict
# Create constants for the compiler flags in Include/code.h # Create constants for the compiler flags in Include/code.h
# We try to get them from dis to avoid duplication, but fall # We try to get them from dis to avoid duplication, but fall
@ -1223,3 +1226,769 @@ def getgeneratorstate(generator):
if generator.gi_frame.f_lasti == -1: if generator.gi_frame.f_lasti == -1:
return GEN_CREATED return GEN_CREATED
return GEN_SUSPENDED 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):
# 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:])
try:
sig = obj.__signature__
except AttributeError:
pass
else:
if sig is not None:
return sig
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.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())
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:
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
return sig.replace(parameters=tuple(sig.parameters.values())[1:])
if isinstance(obj, types.BuiltinFunctionType):
# Raise a nicer error message for builtins
msg = 'no signature found for builtin function {!r}'.format(obj)
raise ValueError(msg)
raise ValueError('callable {!r} is not supported by signature'.format(obj))
class _void:
'''A private marker - used in Parameter & Signature'''
class _empty:
pass
class _ParameterKind(int):
def __new__(self, *args, name):
obj = int.__new__(self, *args)
obj._name = name
return obj
def __str__(self):
return self._name
def __repr__(self):
return '<_ParameterKind: {!r}>'.format(self._name)
_POSITIONAL_ONLY = _ParameterKind(0, name='POSITIONAL_ONLY')
_POSITIONAL_OR_KEYWORD = _ParameterKind(1, name='POSITIONAL_OR_KEYWORD')
_VAR_POSITIONAL = _ParameterKind(2, name='VAR_POSITIONAL')
_KEYWORD_ONLY = _ParameterKind(3, name='KEYWORD_ONLY')
_VAR_KEYWORD = _ParameterKind(4, name='VAR_KEYWORD')
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`.
'''
__slots__ = ('_name', '_kind', '_default', '_annotation', '_partial_kwarg')
POSITIONAL_ONLY = _POSITIONAL_ONLY
POSITIONAL_OR_KEYWORD = _POSITIONAL_OR_KEYWORD
VAR_POSITIONAL = _VAR_POSITIONAL
KEYWORD_ONLY = _KEYWORD_ONLY
VAR_KEYWORD = _VAR_KEYWORD
empty = _empty
def __init__(self, name, kind, *, default=_empty, annotation=_empty,
_partial_kwarg=False):
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
if default is not _empty:
if kind in (_VAR_POSITIONAL, _VAR_KEYWORD):
msg = '{} parameters cannot have default values'.format(kind)
raise ValueError(msg)
self._default = default
self._annotation = annotation
if name is None:
if kind != _POSITIONAL_ONLY:
raise ValueError("None is not a valid name for a "
"non-positional-only parameter")
self._name = name
else:
name = str(name)
if kind != _POSITIONAL_ONLY and not name.isidentifier():
msg = '{!r} is not a valid parameter name'.format(name)
raise ValueError(msg)
self._name = name
self._partial_kwarg = _partial_kwarg
@property
def name(self):
return self._name
@property
def default(self):
return self._default
@property
def annotation(self):
return self._annotation
@property
def kind(self):
return self._kind
def replace(self, *, name=_void, kind=_void, annotation=_void,
default=_void, _partial_kwarg=_void):
'''Creates a customized copy of the Parameter.'''
if name is _void:
name = self._name
if kind is _void:
kind = self._kind
if annotation is _void:
annotation = self._annotation
if default is _void:
default = self._default
if _partial_kwarg is _void:
_partial_kwarg = self._partial_kwarg
return type(self)(name, kind, default=default, annotation=annotation,
_partial_kwarg=_partial_kwarg)
def __str__(self):
kind = self.kind
formatted = self._name
if kind == _POSITIONAL_ONLY:
if formatted is None:
formatted = ''
formatted = '<{}>'.format(formatted)
# Add annotation and default value
if self._annotation is not _empty:
formatted = '{}:{}'.format(formatted,
formatannotation(self._annotation))
if self._default is not _empty:
formatted = '{}={}'.format(formatted, repr(self._default))
if kind == _VAR_POSITIONAL:
formatted = '*' + formatted
elif kind == _VAR_KEYWORD:
formatted = '**' + formatted
return formatted
def __repr__(self):
return '<{} at {:#x} {!r}>'.format(self.__class__.__name__,
id(self), self.name)
def __eq__(self, other):
return (issubclass(other.__class__, Parameter) and
self._name == other._name and
self._kind == other._kind and
self._default == other._default and
self._annotation == other._annotation)
def __ne__(self, other):
return not self.__eq__(other)
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 signature(self):
return self._signature
@property
def args(self):
args = []
for param_name, param in self._signature.parameters.items():
if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
param._partial_kwarg):
# Keyword arguments mapped by 'functools.partial'
# (Parameter._partial_kwarg is True) are mapped
# in 'BoundArguments.kwargs', along with VAR_KEYWORD &
# KEYWORD_ONLY
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():
if not kwargs_started:
if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
param._partial_kwarg):
kwargs_started = True
else:
if param_name not in self.arguments:
kwargs_started = True
continue
if not kwargs_started:
continue
try:
arg = self.arguments[param_name]
except KeyError:
pass
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.)
'''
__slots__ = ('_return_annotation', '_parameters')
_parameter_cls = Parameter
_bound_arguments_cls = BoundArguments
empty = _empty
def __init__(self, parameters=None, *, return_annotation=_empty,
__validate_parameters__=True):
'''Constructs Signature from the given list of Parameter
objects and 'return_annotation'. All arguments are optional.
'''
if parameters is None:
params = OrderedDict()
else:
if __validate_parameters__:
params = OrderedDict()
top_kind = _POSITIONAL_ONLY
for idx, param in enumerate(parameters):
kind = param.kind
if kind < top_kind:
msg = 'wrong parameter order: {} before {}'
msg = msg.format(top_kind, param.kind)
raise ValueError(msg)
else:
top_kind = kind
name = param.name
if name is None:
name = str(idx)
param = param.replace(name=name)
if name in params:
msg = 'duplicate parameter name: {!r}'.format(name)
raise ValueError(msg)
params[name] = param
else:
params = OrderedDict(((param.name, param)
for param in parameters))
self._parameters = types.MappingProxyType(params)
self._return_annotation = return_annotation
@classmethod
def from_function(cls, func):
'''Constructs Signature for the given python function'''
if not isinstance(func, types.FunctionType):
raise TypeError('{!r} is not a Python function'.format(func))
Parameter = cls._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 = []
# 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, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_POSITIONAL_OR_KEYWORD))
# ... w/ defaults.
for offset, name in enumerate(positional[non_default_count:]):
annotation = annotations.get(name, _empty)
parameters.append(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, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_VAR_POSITIONAL))
# Keyword-only parameters.
for name in keyword_only:
default = _empty
if kwdefaults is not None:
default = kwdefaults.get(name, _empty)
annotation = annotations.get(name, _empty)
parameters.append(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, _empty)
parameters.append(Parameter(name, annotation=annotation,
kind=_VAR_KEYWORD))
return cls(parameters,
return_annotation=annotations.get('return', _empty),
__validate_parameters__=False)
@property
def parameters(self):
return self._parameters
@property
def return_annotation(self):
return self._return_annotation
def replace(self, *, parameters=_void, return_annotation=_void):
'''Creates a customized copy of the Signature.
Pass 'parameters' and/or 'return_annotation' arguments
to override them in the new copy.
'''
if parameters is _void:
parameters = self.parameters.values()
if return_annotation is _void:
return_annotation = self._return_annotation
return type(self)(parameters,
return_annotation=return_annotation)
def __eq__(self, other):
if (not issubclass(type(other), Signature) or
self.return_annotation != other.return_annotation 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 (param._partial_kwarg 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:
msg = '{arg!r} parameter is positional only, ' \
'but was passed as a keyword'
msg = msg.format(arg=param.name)
raise TypeError(msg) from None
parameters_ex = (param,)
break
elif (param.kind == _VAR_KEYWORD or
param.default is not _empty):
# 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:
msg = '{arg!r} parameter lacking default value'
msg = msg.format(arg=param.name)
raise TypeError(msg) 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:
# This should never happen in case of a properly built
# Signature object (but let's have this check here
# to ensure correct behaviour just in case)
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
param.default is _empty):
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 idx, param in enumerate(self.parameters.values()):
formatted = str(param)
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
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))
if self.return_annotation is not _empty:
anno = formatannotation(self.return_annotation)
rendered += ' -> {}'.format(anno)
return rendered

View File

@ -1173,13 +1173,934 @@ class TestGetGeneratorState(unittest.TestCase):
self.assertIn(name, str(state)) self.assertIn(name, str(state))
class TestSignatureObject(unittest.TestCase):
@staticmethod
def signature(func):
sig = inspect.signature(func)
return (tuple((param.name,
(... if param.default is param.empty else param.default),
(... if param.annotation is param.empty
else param.annotation),
str(param.kind).lower())
for param in sig.parameters.values()),
(... if sig.return_annotation is sig.empty
else sig.return_annotation))
def test_signature_object(self):
S = inspect.Signature
P = inspect.Parameter
self.assertEqual(str(S()), '()')
def test(po, pk, *args, ko, **kwargs):
pass
sig = inspect.signature(test)
po = sig.parameters['po'].replace(kind=P.POSITIONAL_ONLY)
pk = sig.parameters['pk']
args = sig.parameters['args']
ko = sig.parameters['ko']
kwargs = sig.parameters['kwargs']
S((po, pk, args, ko, kwargs))
with self.assertRaisesRegexp(ValueError, 'wrong parameter order'):
S((pk, po, args, ko, kwargs))
with self.assertRaisesRegexp(ValueError, 'wrong parameter order'):
S((po, args, pk, ko, kwargs))
with self.assertRaisesRegexp(ValueError, 'wrong parameter order'):
S((args, po, pk, ko, kwargs))
with self.assertRaisesRegexp(ValueError, 'wrong parameter order'):
S((po, pk, args, kwargs, ko))
kwargs2 = kwargs.replace(name='args')
with self.assertRaisesRegexp(ValueError, 'duplicate parameter name'):
S((po, pk, args, kwargs2, ko))
def test_signature_immutability(self):
def test(a):
pass
sig = inspect.signature(test)
with self.assertRaises(AttributeError):
sig.foo = 'bar'
with self.assertRaises(TypeError):
sig.parameters['a'] = None
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, 'not supported by signature'):
inspect.signature(type)
with self.assertRaisesRegexp(ValueError, 'not supported by signature'):
# support for 'wrapper_descriptor'
inspect.signature(type.__call__)
with self.assertRaisesRegexp(ValueError, '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.from_function(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)
new_params = tuple(sig.parameters.values())[1:]
wrapper.__signature__ = sig.replace(parameters=new_params)
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, "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):
P = inspect.Parameter
def test(a_po, *, b, **kwargs):
return a_po, kwargs
sig = inspect.signature(test)
new_params = list(sig.parameters.values())
new_params[0] = new_params[0].replace(kind=P.POSITIONAL_ONLY)
test.__signature__ = sig.replace(parameters=new_params)
self.assertEqual(str(inspect.signature(test)),
'(<a_po>, *, b, **kwargs)')
sig = inspect.signature(test)
new_params = list(sig.parameters.values())
new_params[0] = new_params[0].replace(name=None)
test.__signature__ = sig.replace(parameters=new_params)
self.assertEqual(str(inspect.signature(test)),
'(<0>, *, b, **kwargs)')
def test_signature_replace_anno(self):
def test() -> 42:
pass
sig = inspect.signature(test)
sig = sig.replace(return_annotation=None)
self.assertIs(sig.return_annotation, None)
sig = sig.replace(return_annotation=sig.empty)
self.assertIs(sig.return_annotation, sig.empty)
sig = sig.replace(return_annotation=42)
self.assertEqual(sig.return_annotation, 42)
self.assertEqual(sig, inspect.signature(test))
class TestParameterObject(unittest.TestCase):
def test_signature_parameter_kinds(self):
P = inspect.Parameter
self.assertTrue(P.POSITIONAL_ONLY < P.POSITIONAL_OR_KEYWORD < \
P.VAR_POSITIONAL < P.KEYWORD_ONLY < P.VAR_KEYWORD)
self.assertEqual(str(P.POSITIONAL_ONLY), 'POSITIONAL_ONLY')
self.assertTrue('POSITIONAL_ONLY' in repr(P.POSITIONAL_ONLY))
def test_signature_parameter_object(self):
p = inspect.Parameter('foo', default=10,
kind=inspect.Parameter.POSITIONAL_ONLY)
self.assertEqual(p.name, 'foo')
self.assertEqual(p.default, 10)
self.assertIs(p.annotation, p.empty)
self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY)
with self.assertRaisesRegexp(ValueError, 'invalid value'):
inspect.Parameter('foo', default=10, kind='123')
with self.assertRaisesRegexp(ValueError, 'not a valid parameter name'):
inspect.Parameter('1', kind=inspect.Parameter.VAR_KEYWORD)
with self.assertRaisesRegexp(ValueError,
'non-positional-only parameter'):
inspect.Parameter(None, kind=inspect.Parameter.VAR_KEYWORD)
with self.assertRaisesRegexp(ValueError, 'cannot have default values'):
inspect.Parameter('a', default=42,
kind=inspect.Parameter.VAR_KEYWORD)
with self.assertRaisesRegexp(ValueError, 'cannot have default values'):
inspect.Parameter('a', default=42,
kind=inspect.Parameter.VAR_POSITIONAL)
p = inspect.Parameter('a', default=42,
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD)
with self.assertRaisesRegexp(ValueError, 'cannot have default values'):
p.replace(kind=inspect.Parameter.VAR_POSITIONAL)
self.assertTrue(repr(p).startswith('<Parameter'))
def test_signature_parameter_equality(self):
P = inspect.Parameter
p = P('foo', default=42, kind=inspect.Parameter.KEYWORD_ONLY)
self.assertEqual(p, p)
self.assertNotEqual(p, 42)
self.assertEqual(p, P('foo', default=42,
kind=inspect.Parameter.KEYWORD_ONLY))
def test_signature_parameter_unhashable(self):
p = inspect.Parameter('foo', default=42,
kind=inspect.Parameter.KEYWORD_ONLY)
with self.assertRaisesRegexp(TypeError, 'unhashable type'):
hash(p)
def test_signature_parameter_replace(self):
p = inspect.Parameter('foo', default=42,
kind=inspect.Parameter.KEYWORD_ONLY)
self.assertIsNot(p, p.replace())
self.assertEqual(p, p.replace())
p2 = p.replace(annotation=1)
self.assertEqual(p2.annotation, 1)
p2 = p2.replace(annotation=p2.empty)
self.assertEqual(p, p2)
p2 = p2.replace(name='bar')
self.assertEqual(p2.name, 'bar')
self.assertNotEqual(p2, p)
with self.assertRaisesRegexp(ValueError, 'not a valid parameter name'):
p2 = p2.replace(name=p2.empty)
p2 = p2.replace(name='foo', default=None)
self.assertIs(p2.default, None)
self.assertNotEqual(p2, p)
p2 = p2.replace(name='foo', default=p2.empty)
self.assertIs(p2.default, p2.empty)
p2 = p2.replace(default=42, kind=p2.POSITIONAL_OR_KEYWORD)
self.assertEqual(p2.kind, p2.POSITIONAL_OR_KEYWORD)
self.assertNotEqual(p2, p)
with self.assertRaisesRegexp(ValueError, 'invalid value for'):
p2 = p2.replace(kind=p2.empty)
p2 = p2.replace(kind=p2.KEYWORD_ONLY)
self.assertEqual(p2, p)
def test_signature_parameter_positional_only(self):
p = inspect.Parameter(None, kind=inspect.Parameter.POSITIONAL_ONLY)
self.assertEqual(str(p), '<>')
p = p.replace(name='1')
self.assertEqual(str(p), '<1>')
def test_signature_parameter_immutability(self):
p = inspect.Parameter(None, kind=inspect.Parameter.POSITIONAL_ONLY)
with self.assertRaises(AttributeError):
p.foo = 'bar'
with self.assertRaises(AttributeError):
p.kind = 123
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, args=10), ((), {'args': 10}))
self.assertEqual(self.call(test, 1, 2, foo='bar'),
((1, 2), {'foo': 'bar'}))
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.kwargs,
{'b': 30, 'c': 40, 'args': 50, 'kwargs': 60})
self.assertEqual(ba.args, (10, 20))
def test_signature_bind_positional_only(self):
P = inspect.Parameter
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 = inspect.signature(test)
new_params = collections.OrderedDict(tuple(sig.parameters.items()))
for name in ('a_po', 'b_po', 'c_po'):
new_params[name] = new_params[name].replace(kind=P.POSITIONAL_ONLY)
new_sig = sig.replace(parameters=new_params.values())
test.__signature__ = new_sig
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(): def test_main():
run_unittest( run_unittest(
TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases, TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases,
TestInterpreterStack, TestClassesAndFunctions, TestPredicates, TestInterpreterStack, TestClassesAndFunctions, TestPredicates,
TestGetcallargsFunctions, TestGetcallargsMethods, TestGetcallargsFunctions, TestGetcallargsMethods,
TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState, TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState,
TestNoEOL TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject,
TestBoundArguments
) )
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -40,6 +40,9 @@ Core and Builtins
Library Library
------- -------
- Issue #15008: Implement PEP 362 "Signature Objects".
Patch by Yury Selivanov.
- Issue: #15138: base64.urlsafe_{en,de}code() are now 3-4x faster. - Issue: #15138: base64.urlsafe_{en,de}code() are now 3-4x faster.
- Issue #444582: Add shutil.which, for finding programs on the system path. - Issue #444582: Add shutil.which, for finding programs on the system path.