Skip to content

Commit

Permalink
Fix sphinx-doc#9194: autodoc: Prepend the "typing" module name on the…
Browse files Browse the repository at this point in the history
… signature

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 0eb8d0e commit f0b6d5f
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 33 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

0 comments on commit f0b6d5f

Please sign in to comment.