mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Closes #1968: Show extended type hints for function annotations that use 'typing' module
This commit is contained in:
parent
f53fb63627
commit
dd32b7fdb2
@ -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
|
||||
|
@ -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
48
tests/typing_test_data.py
Normal 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
|
Loading…
Reference in New Issue
Block a user