Fix #9194: autodoc: Prepend the "typing" module name on the signature

To create hyperlinks to container types automatically, this prepends the
module names for the types under "typing" module.
This commit is contained in:
Takeshi KOMIYA 2021-12-24 10:50:04 +09:00
parent 0a5783f75b
commit bdbad40f57
8 changed files with 90 additions and 51 deletions

View File

@ -774,7 +774,7 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,
if show_annotation and param.annotation is not param.empty:
arg.write(': ')
arg.write(stringify_annotation(param.annotation, unqualified_typehints))
arg.write(stringify_annotation(param.annotation, unqualified_typehints, True))
if param.default is not param.empty:
if show_annotation and param.annotation is not param.empty:
arg.write(' = ')
@ -794,7 +794,7 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,
show_return_annotation is False):
return '(%s)' % ', '.join(args)
else:
annotation = stringify_annotation(sig.return_annotation, unqualified_typehints)
annotation = stringify_annotation(sig.return_annotation, unqualified_typehints, True)
return '(%s) -> %s' % (', '.join(args), annotation)

View File

@ -299,11 +299,12 @@ def _restify_py36(cls: Optional[Type]) -> str:
return ':py:obj:`%s.%s`' % (cls.__module__, qualname)
def stringify(annotation: Any, smartref: bool = False) -> str:
def stringify(annotation: Any, smartref: bool = False, show_typing: bool = False) -> str:
"""Stringify type annotation object.
:param smartref: If true, add "~" prefix to the result to remove the leading
module and class names from the reference text
:param show_typing: If true, do not suppress the "typing" module name
"""
from sphinx.util import inspect # lazy loading
@ -319,7 +320,7 @@ def stringify(annotation: Any, smartref: bool = False) -> str:
else:
return annotation
elif isinstance(annotation, TypeVar):
if annotation.__module__ == 'typing':
if show_typing is False and annotation.__module__ == 'typing':
return annotation.__name__
else:
return prefix + '.'.join([annotation.__module__, annotation.__name__])
@ -347,12 +348,12 @@ def stringify(annotation: Any, smartref: bool = False) -> str:
return '...'
if sys.version_info >= (3, 7): # py37+
return _stringify_py37(annotation, smartref)
return _stringify_py37(annotation, smartref, show_typing)
else:
return _stringify_py36(annotation, smartref)
return _stringify_py36(annotation, smartref, show_typing)
def _stringify_py37(annotation: Any, smartref: bool = False) -> str:
def _stringify_py37(annotation: Any, smartref: bool = False, show_typing: bool = False) -> str:
"""stringify() for py37+."""
module = getattr(annotation, '__module__', None)
modprefix = ''
@ -364,10 +365,12 @@ def _stringify_py37(annotation: Any, smartref: bool = False) -> str:
elif getattr(annotation, '__qualname__', None):
qualname = annotation.__qualname__
else:
qualname = stringify(annotation.__origin__) # ex. Union
qualname = stringify(annotation.__origin__).replace('typing.', '') # ex. Union
if smartref:
modprefix = '~%s.' % module
elif show_typing:
modprefix = '%s.' % module
elif hasattr(annotation, '__qualname__'):
if smartref:
modprefix = '~%s.' % module
@ -376,7 +379,7 @@ def _stringify_py37(annotation: Any, smartref: bool = False) -> str:
qualname = annotation.__qualname__
elif hasattr(annotation, '__origin__'):
# instantiated generic provided by a user
qualname = stringify(annotation.__origin__, smartref)
qualname = stringify(annotation.__origin__, smartref, show_typing)
elif UnionType and isinstance(annotation, UnionType): # types.Union (for py3.10+)
qualname = 'types.Union'
else:
@ -391,13 +394,15 @@ def _stringify_py37(annotation: Any, smartref: bool = False) -> str:
elif qualname in ('Optional', 'Union'):
if len(annotation.__args__) > 1 and annotation.__args__[-1] is NoneType:
if len(annotation.__args__) > 2:
args = ', '.join(stringify(a, smartref) for a in annotation.__args__[:-1])
args = ', '.join(stringify(a, smartref, show_typing) for a
in annotation.__args__[:-1])
return '%sOptional[%sUnion[%s]]' % (modprefix, modprefix, args)
else:
return '%sOptional[%s]' % (modprefix,
stringify(annotation.__args__[0], smartref))
return '%sOptional[%s]' % (modprefix, stringify(annotation.__args__[0],
smartref, show_typing))
else:
args = ', '.join(stringify(a, smartref) for a in annotation.__args__)
args = ', '.join(stringify(a, smartref, show_typing) for a
in annotation.__args__)
return '%sUnion[%s]' % (modprefix, args)
elif qualname == 'types.Union':
if len(annotation.__args__) > 1 and None in annotation.__args__:
@ -406,25 +411,27 @@ def _stringify_py37(annotation: Any, smartref: bool = False) -> str:
else:
return ' | '.join(stringify(a) for a in annotation.__args__)
elif qualname == 'Callable':
args = ', '.join(stringify(a, smartref) for a in annotation.__args__[:-1])
returns = stringify(annotation.__args__[-1], smartref)
args = ', '.join(stringify(a, smartref, show_typing) for a
in annotation.__args__[:-1])
returns = stringify(annotation.__args__[-1], smartref, show_typing)
return '%s%s[[%s], %s]' % (modprefix, qualname, args, returns)
elif qualname == 'Literal':
args = ', '.join(repr(a) for a in annotation.__args__)
return '%s%s[%s]' % (modprefix, qualname, args)
elif str(annotation).startswith('typing.Annotated'): # for py39+
return stringify(annotation.__args__[0], smartref)
return stringify(annotation.__args__[0], smartref, show_typing)
elif all(is_system_TypeVar(a) for a in annotation.__args__):
# Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
return modprefix + qualname
else:
args = ', '.join(stringify(a, smartref) for a in annotation.__args__)
args = ', '.join(stringify(a, smartref, show_typing) for a
in annotation.__args__)
return '%s%s[%s]' % (modprefix, qualname, args)
return modprefix + qualname
def _stringify_py36(annotation: Any, smartref: bool = False) -> str:
def _stringify_py36(annotation: Any, smartref: bool = False, show_typing: bool = False) -> str:
"""stringify() for py36."""
module = getattr(annotation, '__module__', None)
modprefix = ''
@ -442,6 +449,8 @@ def _stringify_py36(annotation: Any, smartref: bool = False) -> str:
if smartref:
modprefix = '~%s.' % module
elif show_typing:
modprefix = '%s.' % module
elif hasattr(annotation, '__qualname__'):
if smartref:
modprefix = '~%s.' % module
@ -455,7 +464,7 @@ def _stringify_py36(annotation: Any, smartref: bool = False) -> str:
not hasattr(annotation, '__tuple_params__')): # for Python 3.6
params = annotation.__args__
if params:
param_str = ', '.join(stringify(p, smartref) for p in params)
param_str = ', '.join(stringify(p, smartref, show_typing) for p in params)
return '%s%s[%s]' % (modprefix, qualname, param_str)
else:
return modprefix + qualname
@ -466,12 +475,12 @@ def _stringify_py36(annotation: Any, smartref: bool = False) -> str:
elif annotation.__origin__ == Generator: # type: ignore
params = annotation.__args__ # type: ignore
else: # typing.Callable
args = ', '.join(stringify(arg, smartref) for arg
args = ', '.join(stringify(arg, smartref, show_typing) for arg
in annotation.__args__[:-1]) # type: ignore
result = stringify(annotation.__args__[-1]) # type: ignore
return '%s%s[[%s], %s]' % (modprefix, qualname, args, result)
if params is not None:
param_str = ', '.join(stringify(p, smartref) for p in params)
param_str = ', '.join(stringify(p, smartref, show_typing) for p in params)
return '%s%s[%s]' % (modprefix, qualname, param_str)
elif (hasattr(annotation, '__origin__') and
annotation.__origin__ is typing.Union):
@ -479,12 +488,13 @@ def _stringify_py36(annotation: Any, smartref: bool = False) -> str:
if params is not None:
if len(params) > 1 and params[-1] is NoneType:
if len(params) > 2:
param_str = ", ".join(stringify(p, smartref) for p in params[:-1])
param_str = ", ".join(stringify(p, smartref, show_typing) for p
in params[:-1])
return '%sOptional[%sUnion[%s]]' % (modprefix, modprefix, param_str)
else:
return '%sOptional[%s]' % (modprefix, stringify(params[0]))
else:
param_str = ', '.join(stringify(p, smartref) for p in params)
param_str = ', '.join(stringify(p, smartref, show_typing) for p in params)
return '%sUnion[%s]' % (modprefix, param_str)
return modprefix + qualname

View File

@ -162,7 +162,7 @@ def test_wrapped_function_contextmanager(app):
actual = do_autodoc(app, 'function', 'target.wrappedfunction.feeling_good')
assert list(actual) == [
'',
'.. py:function:: feeling_good(x: int, y: int) -> Generator',
'.. py:function:: feeling_good(x: int, y: int) -> typing.Generator',
' :module: target.wrappedfunction',
'',
" You'll feel better in this context!",

View File

@ -130,4 +130,4 @@ def test_subclass_of_mocked_object(app):
options = {'members': None}
actual = do_autodoc(app, 'module', 'target.need_mocks', options)
assert '.. py:class:: Inherited(*args: Any, **kwargs: Any)' in actual
assert '.. py:class:: Inherited(*args: typing.Any, **kwargs: typing.Any)' in actual

View File

@ -612,7 +612,7 @@ def test_autodoc_typehints_signature(app):
' :type: int',
'',
'',
'.. py:class:: Math(s: str, o: Optional[Any] = None)',
'.. py:class:: Math(s: str, o: typing.Optional[typing.Any] = None)',
' :module: target.typehints',
'',
'',
@ -677,7 +677,8 @@ def test_autodoc_typehints_signature(app):
' :module: target.typehints',
'',
'',
'.. py:function:: tuple_args(x: Tuple[int, Union[int, str]]) -> Tuple[int, int]',
'.. py:function:: tuple_args(x: typing.Tuple[int, typing.Union[int, str]]) '
'-> typing.Tuple[int, int]',
' :module: target.typehints',
'',
]

View File

@ -36,15 +36,15 @@ def test_preserve_defaults(app):
' docstring',
'',
'',
' .. py:method:: Class.meth(name: str = CONSTANT, sentinel: Any = SENTINEL, '
'now: datetime.datetime = datetime.now(), color: int = %s) -> None' % color,
' .. py:method:: Class.meth(name: str = CONSTANT, sentinel: typing.Any = '
'SENTINEL, now: datetime.datetime = datetime.now(), color: int = %s) -> None' % color,
' :module: target.preserve_defaults',
'',
' docstring',
'',
'',
'.. py:function:: foo(name: str = CONSTANT, sentinel: Any = SENTINEL, now: '
'datetime.datetime = datetime.now(), color: int = %s) -> None' % color,
'.. py:function:: foo(name: str = CONSTANT, sentinel: typing.Any = SENTINEL, '
'now: datetime.datetime = datetime.now(), color: int = %s) -> None' % color,
' :module: target.preserve_defaults',
'',
' docstring',

View File

@ -157,21 +157,22 @@ def test_signature_annotations():
# Generic types with concrete parameters
sig = inspect.signature(f1)
assert stringify_signature(sig) == '(x: List[int]) -> List[int]'
assert stringify_signature(sig) == '(x: typing.List[int]) -> typing.List[int]'
# TypeVars and generic types with TypeVars
sig = inspect.signature(f2)
if sys.version_info < (3, 7):
assert stringify_signature(sig) == '(x: List[T], y: List[T_co], z: T) -> List[T_contra]'
assert stringify_signature(sig) == ('(x: typing.List[T], y: typing.List[T_co], z: T) '
'-> typing.List[T_contra]')
else:
assert stringify_signature(sig) == ('(x: List[tests.typing_test_data.T],'
' y: List[tests.typing_test_data.T_co],'
assert stringify_signature(sig) == ('(x: typing.List[tests.typing_test_data.T],'
' y: typing.List[tests.typing_test_data.T_co],'
' z: tests.typing_test_data.T'
') -> List[tests.typing_test_data.T_contra]')
') -> typing.List[tests.typing_test_data.T_contra]')
# Union types
sig = inspect.signature(f3)
assert stringify_signature(sig) == '(x: Union[str, numbers.Integral]) -> None'
assert stringify_signature(sig) == '(x: typing.Union[str, numbers.Integral]) -> None'
# Quoted annotations
sig = inspect.signature(f4)
@ -187,18 +188,18 @@ def test_signature_annotations():
# Space around '=' for defaults
sig = inspect.signature(f7)
assert stringify_signature(sig) == '(x: Optional[int] = None, y: dict = {}) -> None'
assert stringify_signature(sig) == '(x: typing.Optional[int] = None, y: dict = {}) -> None'
# Callable types
sig = inspect.signature(f8)
assert stringify_signature(sig) == '(x: Callable[[int, str], int]) -> None'
assert stringify_signature(sig) == '(x: typing.Callable[[int, str], int]) -> None'
sig = inspect.signature(f9)
assert stringify_signature(sig) == '(x: Callable) -> None'
assert stringify_signature(sig) == '(x: typing.Callable) -> None'
# Tuple types
sig = inspect.signature(f10)
assert stringify_signature(sig) == '(x: Tuple[int, str], y: Tuple[int, ...]) -> None'
assert stringify_signature(sig) == '(x: typing.Tuple[int, str], y: typing.Tuple[int, ...]) -> None'
# Instance annotations
sig = inspect.signature(f11)
@ -206,24 +207,24 @@ def test_signature_annotations():
# tuple with more than two items
sig = inspect.signature(f12)
assert stringify_signature(sig) == '() -> Tuple[int, str, int]'
assert stringify_signature(sig) == '() -> typing.Tuple[int, str, int]'
# optional
sig = inspect.signature(f13)
assert stringify_signature(sig) == '() -> Optional[str]'
assert stringify_signature(sig) == '() -> typing.Optional[str]'
# optional union
sig = inspect.signature(f20)
assert stringify_signature(sig) in ('() -> Optional[Union[int, str]]',
'() -> Optional[Union[str, int]]')
assert stringify_signature(sig) in ('() -> typing.Optional[typing.Union[int, str]]',
'() -> typing.Optional[typing.Union[str, int]]')
# Any
sig = inspect.signature(f14)
assert stringify_signature(sig) == '() -> Any'
assert stringify_signature(sig) == '() -> typing.Any'
# ForwardRef
sig = inspect.signature(f15)
assert stringify_signature(sig) == '(x: Unknown, y: int) -> Any'
assert stringify_signature(sig) == '(x: Unknown, y: int) -> typing.Any'
# keyword only arguments (1)
sig = inspect.signature(f16)
@ -234,7 +235,8 @@ def test_signature_annotations():
assert stringify_signature(sig) == '(*, arg3, arg4)'
sig = inspect.signature(f18)
assert stringify_signature(sig) == '(self, arg1: Union[int, Tuple] = 10) -> List[Dict]'
assert stringify_signature(sig) == ('(self, arg1: typing.Union[int, typing.Tuple] = 10) -> '
'typing.List[typing.Dict]')
# annotations for variadic and keyword parameters
sig = inspect.signature(f19)
@ -246,10 +248,10 @@ def test_signature_annotations():
# type hints by string
sig = inspect.signature(Node.children)
assert stringify_signature(sig) == '(self) -> List[tests.typing_test_data.Node]'
assert stringify_signature(sig) == '(self) -> typing.List[tests.typing_test_data.Node]'
sig = inspect.signature(Node.__init__)
assert stringify_signature(sig) == '(self, parent: Optional[tests.typing_test_data.Node]) -> None'
assert stringify_signature(sig) == '(self, parent: typing.Optional[tests.typing_test_data.Node]) -> None'
# show_annotation is False
sig = inspect.signature(f7)
@ -257,7 +259,7 @@ def test_signature_annotations():
# show_return_annotation is False
sig = inspect.signature(f7)
assert stringify_signature(sig, show_return_annotation=False) == '(x: Optional[int] = None, y: dict = {})'
assert stringify_signature(sig, show_return_annotation=False) == '(x: typing.Optional[int] = None, y: dict = {})'
# unqualified_typehints is True
sig = inspect.signature(f7)

View File

@ -197,41 +197,53 @@ def test_stringify():
assert stringify(TracebackType, True) == "~types.TracebackType"
assert stringify(Any, False) == "Any"
assert stringify(Any, False, True) == "typing.Any"
assert stringify(Any, True) == "~typing.Any"
def test_stringify_type_hints_containers():
assert stringify(List, False) == "List"
assert stringify(List, False, True) == "typing.List"
assert stringify(List, True) == "~typing.List"
assert stringify(Dict, False) == "Dict"
assert stringify(Dict, False, True) == "typing.Dict"
assert stringify(Dict, True) == "~typing.Dict"
assert stringify(List[int], False) == "List[int]"
assert stringify(List[int], False, True) == "typing.List[int]"
assert stringify(List[int], True) == "~typing.List[int]"
assert stringify(List[str], False) == "List[str]"
assert stringify(List[str], False, True) == "typing.List[str]"
assert stringify(List[str], True) == "~typing.List[str]"
assert stringify(Dict[str, float], False) == "Dict[str, float]"
assert stringify(Dict[str, float], False, True) == "typing.Dict[str, float]"
assert stringify(Dict[str, float], True) == "~typing.Dict[str, float]"
assert stringify(Tuple[str, str, str], False) == "Tuple[str, str, str]"
assert stringify(Tuple[str, str, str], False, True) == "typing.Tuple[str, str, str]"
assert stringify(Tuple[str, str, str], True) == "~typing.Tuple[str, str, str]"
assert stringify(Tuple[str, ...], False) == "Tuple[str, ...]"
assert stringify(Tuple[str, ...], False, True) == "typing.Tuple[str, ...]"
assert stringify(Tuple[str, ...], True) == "~typing.Tuple[str, ...]"
assert stringify(Tuple[()], False) == "Tuple[()]"
assert stringify(Tuple[()], False, True) == "typing.Tuple[()]"
assert stringify(Tuple[()], True) == "~typing.Tuple[()]"
assert stringify(List[Dict[str, Tuple]], False) == "List[Dict[str, Tuple]]"
assert stringify(List[Dict[str, Tuple]], False, True) == "typing.List[typing.Dict[str, typing.Tuple]]"
assert stringify(List[Dict[str, Tuple]], True) == "~typing.List[~typing.Dict[str, ~typing.Tuple]]"
assert stringify(MyList[Tuple[int, int]], False) == "tests.test_util_typing.MyList[Tuple[int, int]]"
assert stringify(MyList[Tuple[int, int]], False, True) == "tests.test_util_typing.MyList[typing.Tuple[int, int]]"
assert stringify(MyList[Tuple[int, int]], True) == "~tests.test_util_typing.MyList[~typing.Tuple[int, int]]"
assert stringify(Generator[None, None, None], False) == "Generator[None, None, None]"
assert stringify(Generator[None, None, None], False, True) == "typing.Generator[None, None, None]"
assert stringify(Generator[None, None, None], True) == "~typing.Generator[None, None, None]"
@ -288,45 +300,58 @@ def test_stringify_type_hints_string():
def test_stringify_type_hints_Callable():
assert stringify(Callable, False) == "Callable"
assert stringify(Callable, False, True) == "typing.Callable"
assert stringify(Callable, True) == "~typing.Callable"
if sys.version_info >= (3, 7):
assert stringify(Callable[[str], int], False) == "Callable[[str], int]"
assert stringify(Callable[[str], int], False, True) == "typing.Callable[[str], int]"
assert stringify(Callable[[str], int], True) == "~typing.Callable[[str], int]"
assert stringify(Callable[..., int], False) == "Callable[[...], int]"
assert stringify(Callable[..., int], False, True) == "typing.Callable[[...], int]"
assert stringify(Callable[..., int], True) == "~typing.Callable[[...], int]"
else:
assert stringify(Callable[[str], int], False) == "Callable[str, int]"
assert stringify(Callable[[str], int], False, True) == "typing.Callable[str, int]"
assert stringify(Callable[[str], int], True) == "~typing.Callable[str, int]"
assert stringify(Callable[..., int], False) == "Callable[..., int]"
assert stringify(Callable[..., int], False, True) == "typing.Callable[..., int]"
assert stringify(Callable[..., int], True) == "~typing.Callable[..., int]"
def test_stringify_type_hints_Union():
assert stringify(Optional[int], False) == "Optional[int]"
assert stringify(Optional[int], False, True) == "typing.Optional[int]"
assert stringify(Optional[int], True) == "~typing.Optional[int]"
assert stringify(Union[str, None], False) == "Optional[str]"
assert stringify(Union[str, None], False, True) == "typing.Optional[str]"
assert stringify(Union[str, None], True) == "~typing.Optional[str]"
assert stringify(Union[int, str], False) == "Union[int, str]"
assert stringify(Union[int, str], False, True) == "typing.Union[int, str]"
assert stringify(Union[int, str], True) == "~typing.Union[int, str]"
if sys.version_info >= (3, 7):
assert stringify(Union[int, Integral], False) == "Union[int, numbers.Integral]"
assert stringify(Union[int, Integral], False, True) == "typing.Union[int, numbers.Integral]"
assert stringify(Union[int, Integral], True) == "~typing.Union[int, ~numbers.Integral]"
assert (stringify(Union[MyClass1, MyClass2], False) ==
"Union[tests.test_util_typing.MyClass1, tests.test_util_typing.<MyClass2>]")
assert (stringify(Union[MyClass1, MyClass2], False, True) ==
"typing.Union[tests.test_util_typing.MyClass1, tests.test_util_typing.<MyClass2>]")
assert (stringify(Union[MyClass1, MyClass2], True) ==
"~typing.Union[~tests.test_util_typing.MyClass1, ~tests.test_util_typing.<MyClass2>]")
else:
assert stringify(Union[int, Integral], False) == "numbers.Integral"
assert stringify(Union[int, Integral], False, True) == "numbers.Integral"
assert stringify(Union[int, Integral], True) == "~numbers.Integral"
assert stringify(Union[MyClass1, MyClass2], False) == "tests.test_util_typing.MyClass1"
assert stringify(Union[MyClass1, MyClass2], False, True) == "tests.test_util_typing.MyClass1"
assert stringify(Union[MyClass1, MyClass2], True) == "~tests.test_util_typing.MyClass1"
@ -391,6 +416,7 @@ def test_stringify_type_hints_alias():
def test_stringify_type_Literal():
from typing import Literal # type: ignore
assert stringify(Literal[1, "2", "\r"], False) == "Literal[1, '2', '\\r']"
assert stringify(Literal[1, "2", "\r"], False, True) == "typing.Literal[1, '2', '\\r']"
assert stringify(Literal[1, "2", "\r"], True) == "~typing.Literal[1, '2', '\\r']"