Skip to content

Commit

Permalink
Fix #9194: autodoc: Prepend the "typing" module name on the signature
Browse files Browse the repository at this point in the history
To create hyperlinks to container types automatically, this prepends the
module names for the types under "typing" module.
  • Loading branch information
tk0miya committed Dec 24, 2021
1 parent 0a5783f commit 54045f3
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 53 deletions.
4 changes: 2 additions & 2 deletions sphinx/util/inspect.py
Expand Up @@ -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(' = ')
Expand All @@ -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)


Expand Down
53 changes: 30 additions & 23 deletions sphinx/util/typing.py
Expand Up @@ -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

Expand All @@ -319,10 +320,7 @@ def stringify(annotation: Any, smartref: bool = False) -> str:
else:
return annotation
elif isinstance(annotation, TypeVar):
if annotation.__module__ == 'typing':
return annotation.__name__
else:
return prefix + '.'.join([annotation.__module__, annotation.__name__])
return prefix + '.'.join([annotation.__module__, annotation.__name__])
elif inspect.isNewType(annotation):
if sys.version_info > (3, 10):
# newtypes have correct module info since Python 3.10+
Expand All @@ -347,12 +345,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 = ''
Expand All @@ -364,10 +362,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
Expand All @@ -376,7 +376,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:
Expand All @@ -391,13 +391,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))
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__:
Expand All @@ -406,25 +408,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 = ''
Expand All @@ -442,6 +446,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
Expand All @@ -455,7 +461,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
Expand All @@ -466,25 +472,26 @@ 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):
params = annotation.__args__
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
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ext_autodoc_autofunction.py
Expand Up @@ -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!",
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ext_autodoc_automodule.py
Expand Up @@ -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
5 changes: 3 additions & 2 deletions tests/test_ext_autodoc_configs.py
Expand Up @@ -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',
'',
'',
Expand Down Expand Up @@ -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',
'',
]
Expand Down
8 changes: 4 additions & 4 deletions tests/test_ext_autodoc_preserve_defaults.py
Expand Up @@ -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',
Expand Down
42 changes: 22 additions & 20 deletions tests/test_util_inspect.py
Expand Up @@ -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)
Expand All @@ -187,43 +188,43 @@ 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)
assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None'

# 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)
Expand All @@ -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)
Expand All @@ -246,18 +248,18 @@ 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)
assert stringify_signature(sig, show_annotation=False) == '(x=None, y={})'

# 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)
Expand Down

0 comments on commit 54045f3

Please sign in to comment.