Add Signature class

This commit is contained in:
Takeshi KOMIYA 2017-06-13 00:49:39 +09:00
parent 2317df9c84
commit 3be5eebd6b
2 changed files with 326 additions and 3 deletions

View File

@ -11,16 +11,18 @@
from __future__ import absolute_import
import re
import typing
import inspect
from collections import OrderedDict
from six import PY3, binary_type
from six import PY2, PY3, StringIO, binary_type, string_types, itervalues
from six.moves import builtins
from sphinx.util import force_decode
if False:
# For type annotation
from typing import Any, Callable, List, Tuple, Type # NOQA
from typing import Any, Callable, Dict, List, Tuple, Type # NOQA
memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)
@ -111,7 +113,7 @@ else: # 2.7
func = func.func
if not inspect.isfunction(func):
raise TypeError('%r is not a Python function' % func)
args, varargs, varkw = inspect.getargs(func.__code__)
args, varargs, varkw = inspect.getargs(func.__code__) # type: ignore
func_defaults = func.__defaults__
if func_defaults is None:
func_defaults = []
@ -229,3 +231,206 @@ def is_builtin_class_method(obj, attr_name):
if not hasattr(builtins, safe_getattr(cls, '__name__', '')): # type: ignore
return False
return getattr(builtins, safe_getattr(cls, '__name__', '')) is cls # type: ignore
class Parameter(object):
"""Fake parameter class for python2."""
POSITIONAL_ONLY = 0
POSITIONAL_OR_KEYWORD = 1
VAR_POSITIONAL = 2
KEYWORD_ONLY = 3
VAR_KEYWORD = 4
empty = object()
def __init__(self, name, kind=POSITIONAL_OR_KEYWORD, default=empty):
# type: (str, int, Any) -> None
self.name = name
self.kind = kind
self.default = default
self.annotation = self.empty
class Signature(object):
"""The Signature object represents the call signature of a callable object and
its return annotation.
"""
def __init__(self, subject):
# type: (Callable) -> None
# check subject is not a built-in class (ex. int, str)
if (isinstance(subject, type) and
is_builtin_class_method(subject, "__new__") and
is_builtin_class_method(subject, "__init__")):
raise TypeError("can't compute signature for built-in type {}".format(subject))
self.subject = subject
if PY3:
self.signatures = inspect.signature(subject)
else:
self.argspec = getargspec(subject)
try:
self.annotations = typing.get_type_hints(subject)
except:
self.annotations = None
@property
def parameters(self):
# type: () -> Dict
if PY3:
return self.signatures.parameters
else:
params = OrderedDict() # type: Dict
positionals = len(self.argspec.args) - len(self.argspec.defaults)
for i, arg in enumerate(self.argspec.args):
if i < positionals:
params[arg] = Parameter(arg)
else:
default = self.argspec.defaults[i - positionals]
params[arg] = Parameter(arg, default=default)
if self.argspec.varargs:
params[self.argspec.varargs] = Parameter(self.argspec.varargs,
Parameter.VAR_POSITIONAL)
if self.argspec.keywords:
params[self.argspec.keywords] = Parameter(self.argspec.keywords,
Parameter.VAR_KEYWORD)
return params
@property
def return_annotation(self):
# type: () -> Any
if PY3:
return self.signatures.return_annotation
else:
return None
def format_args(self):
# type: () -> unicode
args = []
last_kind = None
for param in itervalues(self.parameters):
arg = StringIO()
# insert '*' between POSITIONAL args and KEYWORD_ONLY args::
# func(a, b, *, c, d):
if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD,
param.POSITIONAL_ONLY):
args.append('*')
if param.kind in (param.POSITIONAL_ONLY,
param.POSITIONAL_OR_KEYWORD,
param.KEYWORD_ONLY):
arg.write(param.name)
if param.annotation is not param.empty:
if isinstance(param.annotation, string_types) and \
param.name in self.annotations:
arg.write(': ')
arg.write(self.format_annotation(self.annotations[param.name]))
else:
arg.write(': ')
arg.write(self.format_annotation(param.annotation))
if param.default is not param.empty:
if param.annotation is param.empty:
arg.write('=')
arg.write(object_description(param.default)) # type: ignore
else:
arg.write(' = ')
arg.write(object_description(param.default)) # type: ignore
elif param.kind == param.VAR_POSITIONAL:
arg.write('*')
arg.write(param.name)
elif param.kind == param.VAR_KEYWORD:
arg.write('**')
arg.write(param.name)
args.append(arg.getvalue())
last_kind = param.kind
if PY2 or self.return_annotation is inspect.Parameter.empty:
return '(%s)' % ', '.join(args)
else:
if isinstance(self.return_annotation, string_types) and \
'return' in self.annotations:
annotation = self.format_annotation(self.annotations['return'])
else:
annotation = self.format_annotation(self.return_annotation)
return '(%s) -> %s' % (', '.join(args), annotation)
def format_annotation(self, annotation):
# type: (Any) -> str
"""Return formatted representation of a type annotation.
Show qualified names for types and additional details for types from
the ``typing`` module.
Displaying complex types from ``typing`` relies on its private API.
"""
if isinstance(annotation, string_types):
return annotation # type: ignore
if isinstance(annotation, typing.TypeVar): # type: ignore
return annotation.__name__
if annotation == Ellipsis:
return '...'
if not isinstance(annotation, type):
return repr(annotation)
qualified_name = (annotation.__module__ + '.' + annotation.__qualname__ # type: ignore
if annotation else repr(annotation))
if annotation.__module__ == 'builtins':
return annotation.__qualname__ # type: ignore
elif isinstance(annotation, typing.GenericMeta):
# In Python 3.5.2+, all arguments are stored in __args__,
# whereas __parameters__ only contains generic parameters.
#
# Prior to Python 3.5.2, __args__ is not available, and all
# arguments are in __parameters__.
params = None
if hasattr(annotation, '__args__'):
if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA
params = annotation.__args__ # type: ignore
else: # typing.Callable
args = ', '.join(self.format_annotation(arg) for arg
in annotation.__args__[:-1]) # type: ignore
result = self.format_annotation(annotation.__args__[-1]) # type: ignore
return '%s[[%s], %s]' % (qualified_name, args, result)
elif hasattr(annotation, '__parameters__'):
params = annotation.__parameters__ # type: ignore
if params is not None:
param_str = ', '.join(self.format_annotation(p) for p in params)
return '%s[%s]' % (qualified_name, param_str)
elif (isinstance(annotation, typing.UnionMeta) and # type: ignore
hasattr(annotation, '__union_params__')):
params = annotation.__union_params__ # type: ignore
if params is not None:
param_str = ', '.join(self.format_annotation(p) for p in params)
return '%s[%s]' % (qualified_name, param_str)
elif (isinstance(annotation, typing.CallableMeta) and # type: ignore
getattr(annotation, '__args__', None) is not None and
hasattr(annotation, '__result__')):
# Skipped in the case of plain typing.Callable
args = annotation.__args__ # type: ignore
if args is None:
return qualified_name
elif args is Ellipsis:
args_str = '...'
else:
formatted_args = (self.format_annotation(a) for a in args)
args_str = '[%s]' % ', '.join(formatted_args)
return '%s[%s, %s]' % (qualified_name,
args_str,
self.format_annotation(annotation.__result__)) # type: ignore # NOQA
elif (isinstance(annotation, typing.TupleMeta) and # type: ignore
hasattr(annotation, '__tuple_params__') and
hasattr(annotation, '__tuple_use_ellipsis__')):
params = annotation.__tuple_params__ # type: ignore
if params is not None:
param_strings = [self.format_annotation(p) for p in params]
if annotation.__tuple_use_ellipsis__: # type: ignore
param_strings.append('...')
return '%s[%s]' % (qualified_name,
', '.join(param_strings))
return qualified_name

View File

@ -113,6 +113,124 @@ def test_getargspec_bound_methods():
assert expected_bound == inspect.getargspec(wrapped_bound_method)
def test_Signature():
# literals
with pytest.raises(TypeError):
inspect.Signature(1)
with pytest.raises(TypeError):
inspect.Signature('')
# builitin classes
with pytest.raises(TypeError):
inspect.Signature(int)
with pytest.raises(TypeError):
inspect.Signature(str)
# normal function
def func(a, b, c=1, d=2, *e, **f):
pass
sig = inspect.Signature(func).format_args()
assert sig == '(a, b, c=1, d=2, *e, **f)'
def test_Signature_partial():
def fun(a, b, c=1, d=2):
pass
p = functools.partial(fun, 10, c=11)
sig = inspect.Signature(p).format_args()
if sys.version_info < (3,):
assert sig == '(b, d=2)'
else:
assert sig == '(b, *, c=11, d=2)'
def test_Signature_bound_methods():
class Foo:
def method(self, arg1, **kwargs):
pass
bound_method = Foo().method
@functools.wraps(bound_method)
def wrapped_bound_method(*args, **kwargs):
pass
# class method
sig = inspect.Signature(Foo.method).format_args()
assert sig == '(self, arg1, **kwargs)'
# bound method
sig = inspect.Signature(bound_method).format_args()
if sys.version_info < (3, 4, 4):
assert sig == '(self, arg1, **kwargs)'
else:
assert sig == '(arg1, **kwargs)'
# wrapped bound method
sig = inspect.Signature(wrapped_bound_method).format_args()
if sys.version_info < (3, 4, 4):
assert sig == '(*args, **kwargs)'
else:
assert sig == '(arg1, **kwargs)'
@pytest.mark.skipif(sys.version_info < (3, 5),
reason='type annotation test is available on py35 or above')
def test_Signature_annotations():
from typing_test_data import f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11
# Class annotations
sig = inspect.Signature(f0).format_args()
assert sig == '(x: int, y: numbers.Integral) -> None'
# Generic types with concrete parameters
sig = inspect.Signature(f1).format_args()
assert sig == '(x: typing.List[int]) -> typing.List[int]'
# TypeVars and generic types with TypeVars
sig = inspect.Signature(f2).format_args()
assert sig == '(x: typing.List[T], y: typing.List[T_co], z: T) -> typing.List[T_contra]'
# Union types
sig = inspect.Signature(f3).format_args()
assert sig == '(x: typing.Union[str, numbers.Integral]) -> None'
# Quoted annotations
sig = inspect.Signature(f4).format_args()
assert sig == '(x: str, y: str) -> None'
# Keyword-only arguments
sig = inspect.Signature(f5).format_args()
assert sig == '(x: int, *, y: str, z: str) -> None'
# Keyword-only arguments with varargs
sig = inspect.Signature(f6).format_args()
assert sig == '(x: int, *args, y: str, z: str) -> None'
# Space around '=' for defaults
sig = inspect.Signature(f7).format_args()
assert sig == '(x: int = None, y: dict = {}) -> None'
# Callable types
sig = inspect.Signature(f8).format_args()
assert sig == '(x: typing.Callable[[int, str], int]) -> None'
sig = inspect.Signature(f9).format_args()
assert sig == '(x: typing.Callable) -> None'
# Tuple types
sig = inspect.Signature(f10).format_args()
assert sig == '(x: typing.Tuple[int, str], y: typing.Tuple[int, ...]) -> None'
# Instance annotations
sig = inspect.Signature(f11).format_args()
assert sig == '(x: CustomAnnotation, y: 123) -> None'
def test_safe_getattr_with_default():
class Foo(object):
def __getattr__(self, item):