Closes #1968: Show extended type hints for function annotations that use 'typing' module

This commit is contained in:
Andrey Vlasovskikh 2015-07-29 23:10:02 +03:00
parent f53fb63627
commit dd32b7fdb2
3 changed files with 222 additions and 8 deletions

View File

@ -17,7 +17,8 @@ import inspect
import traceback
from types import FunctionType, BuiltinFunctionType, MethodType
from six import iteritems, itervalues, text_type, class_types, string_types
from six import iteritems, itervalues, text_type, class_types, string_types, \
StringIO
from docutils import nodes
from docutils.utils import assemble_option_dict
from docutils.statemachine import ViewList
@ -33,6 +34,10 @@ from sphinx.util.inspect import getargspec, isdescriptor, safe_getmembers, \
safe_getattr, object_description, is_builtin_class_method
from sphinx.util.docstrings import prepare_docstring
try:
import typing
except ImportError:
typing = None
#: extended signature RE: with explicit module name separated by ::
py_ext_sig_re = re.compile(
@ -245,9 +250,126 @@ def between(marker, what=None, keepempty=False, exclude=False):
return process
def formatargspec(*argspec):
return inspect.formatargspec(*argspec,
formatvalue=lambda x: '=' + object_description(x))
def format_annotation(annotation):
"""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.
"""
qualified_name = (annotation.__module__ + '.' + annotation.__qualname__
if annotation else repr(annotation))
if not isinstance(annotation, type):
return repr(annotation)
elif annotation.__module__ == 'builtins':
return annotation.__qualname__
elif typing:
if isinstance(annotation, typing.TypeVar):
return annotation.__name__
elif hasattr(typing, 'GenericMeta') and \
isinstance(annotation, typing.GenericMeta) and \
hasattr(annotation, '__parameters__'):
params = annotation.__parameters__
if params is not None:
param_str = ', '.join(format_annotation(p) for p in params)
return '%s[%s]' % (qualified_name, param_str)
elif hasattr(typing, 'UnionMeta') and \
isinstance(annotation, typing.UnionMeta) and \
hasattr(annotation, '__union_params__'):
params = annotation.__union_params__
if params is not None:
param_str = ', '.join(format_annotation(p) for p in params)
return '%s[%s]' % (qualified_name, param_str)
elif hasattr(typing, 'CallableMeta') and \
isinstance(annotation, typing.CallableMeta) and \
hasattr(annotation, '__args__') and \
hasattr(annotation, '__result__'):
args = annotation.__args__
if args is Ellipsis:
args_str = '...'
else:
formatted_args = (format_annotation(a) for a in args)
args_str = '[%s]' % ', '.join(formatted_args)
return '%s[%s, %s]' % (qualified_name,
args_str,
format_annotation(annotation.__result__))
elif hasattr(typing, 'TupleMeta') and \
isinstance(annotation, typing.TupleMeta) and \
hasattr(annotation, '__tuple_params__') and \
hasattr(annotation, '__tuple_use_ellipsis__'):
params = annotation.__tuple_params__
if params is not None:
param_strings = [format_annotation(p) for p in params]
if annotation.__tuple_use_ellipsis__:
param_strings.append('...')
return '%s[%s]' % (qualified_name,
', '.join(param_strings))
return qualified_name
def formatargspec(function, args, varargs=None, varkw=None, defaults=None,
kwonlyargs=(), kwonlydefaults={}, annotations={}):
"""Return a string representation of an ``inspect.FullArgSpec`` tuple.
An enhanced version of ``inspect.formatargspec()`` that handles typing
annotations better.
"""
def format_arg_with_annotation(name):
if name in annotations:
return '%s: %s' % (name, format_annotation(get_annotation(name)))
return name
def get_annotation(name):
value = annotations[name]
if isinstance(value, string_types):
return introspected_hints.get(name, value)
else:
return value
introspected_hints = (typing.get_type_hints(function)
if typing and hasattr(function, '__code__') else {})
fd = StringIO()
fd.write('(')
formatted = []
defaults_start = len(args) - len(defaults) if defaults else len(args)
for i, arg in enumerate(args):
arg_fd = StringIO()
arg_fd.write(format_arg_with_annotation(arg))
if defaults and i >= defaults_start:
arg_fd.write(' = ' if arg in annotations else '=')
arg_fd.write(object_description(defaults[i - defaults_start]))
formatted.append(arg_fd.getvalue())
if varargs:
formatted.append('*' + format_arg_with_annotation(varargs))
if kwonlyargs:
formatted.append('*')
for kwarg in kwonlyargs:
arg_fd = StringIO()
arg_fd.write(format_arg_with_annotation(kwarg))
if kwonlydefaults and kwarg in kwonlydefaults:
arg_fd.write(' = ' if kwarg in annotations else '=')
arg_fd.write(object_description(kwonlydefaults[kwarg]))
formatted.append(arg_fd.getvalue())
if varkw:
formatted.append('**' + format_arg_with_annotation(varkw))
fd.write(', '.join(formatted))
fd.write(')')
if 'return' in annotations:
fd.write(' -> ')
fd.write(format_annotation(get_annotation('return')))
return fd.getvalue()
class Documenter(object):
@ -1061,7 +1183,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter):
argspec = getargspec(self.object.__init__)
if argspec[0]:
del argspec[0][0]
args = formatargspec(*argspec)
args = formatargspec(self.object, *argspec)
# escape backslashes for reST
args = args.replace('\\', '\\\\')
return args
@ -1116,7 +1238,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter):
return None
if argspec[0] and argspec[0][0] in ('cls', 'self'):
del argspec[0][0]
return formatargspec(*argspec)
return formatargspec(initmeth, *argspec)
def format_signature(self):
if self.doc_as_attr:
@ -1283,7 +1405,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter):
argspec = getargspec(self.object)
if argspec[0] and argspec[0][0] in ('cls', 'self'):
del argspec[0][0]
args = formatargspec(*argspec)
args = formatargspec(self.object, *argspec)
# escape backslashes for reST
args = args.replace('\\', '\\\\')
return args

View File

@ -11,8 +11,9 @@
"""
# "raises" imported for usage by autodoc
from unittest import SkipTest
from util import TestApp, Struct, raises
from nose.tools import with_setup
from nose.tools import with_setup, eq_
from six import StringIO
from docutils.statemachine import ViewList
@ -968,3 +969,46 @@ class InstAttCls(object):
self.ia2 = 'e'
"""Docstring for instance attribute InstAttCls.ia2."""
def test_type_hints():
from sphinx.ext.autodoc import formatargspec
from sphinx.util.inspect import getargspec
try:
from typing_test_data import f0, f1, f2, f3, f4, f5, f6, f7, f8
except ImportError:
raise SkipTest
def verify_arg_spec(f, expected):
eq_(formatargspec(f, *getargspec(f)), expected)
# Class annotations
verify_arg_spec(f0, '(x: int, y: numbers.Integral) -> None')
# Generic types with concrete parameters
verify_arg_spec(f1, '(x: typing.List[int]) -> typing.List[int]')
# TypeVars and generic types with TypeVars
verify_arg_spec(f2, '(x: typing.List[T],'
' y: typing.List[T_co],'
' z: T) -> typing.List[T_contra]')
# Union types
verify_arg_spec(f3, '(x: typing.Union[str, numbers.Integral]) -> None')
# Quoted annotations
verify_arg_spec(f4, '(x: str, y: str) -> None')
# Keyword-only arguments
verify_arg_spec(f5, '(x: int, *, y: str, z: str) -> None')
# Space around '=' for defaults
verify_arg_spec(f6, '(x: int = None, y: dict = {}) -> None')
# Callable types
verify_arg_spec(f7, '(x: typing.Callable[[int, str], int]) -> None')
# Tuple types
verify_arg_spec(f8, '(x: typing.Tuple[int, str],'
' y: typing.Tuple[int, ...]) -> None')

48
tests/typing_test_data.py Normal file
View File

@ -0,0 +1,48 @@
from typing import List, TypeVar, Union, Callable, Tuple
from numbers import Integral
def f0(x: int, y: Integral) -> None:
pass
def f1(x: List[int]) -> List[int]:
pass
T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
def f2(x: List[T], y: List[T_co], z: T) -> List[T_contra]:
pass
def f3(x: Union[str, Integral]) -> None:
pass
MyStr = str
def f4(x: 'MyStr', y: MyStr) -> None:
pass
def f5(x: int, *, y: str, z: str) -> None:
pass
def f6(x: int = None, y: dict = {}) -> None:
pass
def f7(x: Callable[[int, str], int]) -> None:
# See https://github.com/ambv/typehinting/issues/149 for Callable[..., int]
pass
def f8(x: Tuple[int, str], y: Tuple[int, ...]) -> None:
pass