Merge pull request #10034 from tk0miya/10027_autodoc_typehints_format_for_bases

Fix #10027: autodoc_typehints_format does not work with :show-inheritance:
This commit is contained in:
Takeshi KOMIYA 2022-01-02 11:39:22 +09:00 committed by GitHub
commit e1c090d9f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 207 additions and 48 deletions

View File

@ -1676,7 +1676,11 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
self.env.events.emit('autodoc-process-bases',
self.fullname, self.object, self.options, bases)
base_classes = [restify(cls) for cls in bases]
if self.config.autodoc_typehints_format == "short":
base_classes = [restify(cls, "smart") for cls in bases]
else:
base_classes = [restify(cls) for cls in bases]
sourcename = self.get_sourcename()
self.add_line('', sourcename)
self.add_line(' ' + _('Bases: %s') % ', '.join(base_classes), sourcename)
@ -1773,7 +1777,11 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if self.doc_as_attr and not self.get_variable_comment():
try:
more_content = StringList([_('alias of %s') % restify(self.object)], source='')
if self.config.autodoc_typehints_format == "short":
alias = restify(self.object, "smart")
else:
alias = restify(self.object)
more_content = StringList([_('alias of %s') % alias], source='')
except AttributeError:
pass # Invalid class object is passed.
@ -1846,7 +1854,12 @@ class GenericAliasMixin(DataDocumenterMixinBase):
def update_content(self, more_content: StringList) -> None:
if inspect.isgenericalias(self.object):
more_content.append(_('alias of %s') % restify(self.object), '')
if self.config.autodoc_typehints_format == "short":
alias = restify(self.object, "smart")
else:
alias = restify(self.object)
more_content.append(_('alias of %s') % alias, '')
more_content.append('', '')
super().update_content(more_content)
@ -1864,7 +1877,11 @@ class NewTypeMixin(DataDocumenterMixinBase):
def update_content(self, more_content: StringList) -> None:
if inspect.isNewType(self.object):
supertype = restify(self.object.__supertype__)
if self.config.autodoc_typehints_format == "short":
supertype = restify(self.object.__supertype__, "smart")
else:
supertype = restify(self.object.__supertype__)
more_content.append(_('alias of %s') % supertype, '')
more_content.append('', '')
@ -1901,7 +1918,11 @@ class TypeVarMixin(DataDocumenterMixinBase):
for constraint in self.object.__constraints__:
attrs.append(stringify_typehint(constraint))
if self.object.__bound__:
attrs.append(r"bound=\ " + restify(self.object.__bound__))
if self.config.autodoc_typehints_format == "short":
bound = restify(self.object.__bound__, "smart")
else:
bound = restify(self.object.__bound__)
attrs.append(r"bound=\ " + bound)
if self.object.__covariant__:
attrs.append("covariant=True")
if self.object.__contravariant__:

View File

@ -105,10 +105,24 @@ def is_system_TypeVar(typ: Any) -> bool:
return modname == 'typing' and isinstance(typ, TypeVar)
def restify(cls: Optional[Type]) -> str:
"""Convert python class to a reST reference."""
def restify(cls: Optional[Type], mode: str = 'fully-qualified-except-typing') -> str:
"""Convert python class to a reST reference.
:param mode: Specify a method how annotations will be stringified.
'fully-qualified-except-typing'
Show the module name and qualified name of the annotation except
the "typing" module.
'smart'
Show the name of the annotation.
"""
from sphinx.util import inspect # lazy loading
if mode == 'smart':
modprefix = '~'
else:
modprefix = ''
try:
if cls is None or cls is NoneType:
return ':py:obj:`None`'
@ -117,63 +131,67 @@ def restify(cls: Optional[Type]) -> str:
elif isinstance(cls, str):
return cls
elif cls in INVALID_BUILTIN_CLASSES:
return ':py:class:`%s`' % INVALID_BUILTIN_CLASSES[cls]
return ':py:class:`%s%s`' % (modprefix, INVALID_BUILTIN_CLASSES[cls])
elif inspect.isNewType(cls):
if sys.version_info > (3, 10):
# newtypes have correct module info since Python 3.10+
print(cls, type(cls), dir(cls))
return ':py:class:`%s.%s`' % (cls.__module__, cls.__name__)
return ':py:class:`%s%s.%s`' % (modprefix, cls.__module__, cls.__name__)
else:
return ':py:class:`%s`' % cls.__name__
elif UnionType and isinstance(cls, UnionType):
if len(cls.__args__) > 1 and None in cls.__args__:
args = ' | '.join(restify(a) for a in cls.__args__ if a)
args = ' | '.join(restify(a, mode) for a in cls.__args__ if a)
return 'Optional[%s]' % args
else:
return ' | '.join(restify(a) for a in cls.__args__)
return ' | '.join(restify(a, mode) for a in cls.__args__)
elif cls.__module__ in ('__builtin__', 'builtins'):
if hasattr(cls, '__args__'):
return ':py:class:`%s`\\ [%s]' % (
cls.__name__,
', '.join(restify(arg) for arg in cls.__args__),
', '.join(restify(arg, mode) for arg in cls.__args__),
)
else:
return ':py:class:`%s`' % cls.__name__
else:
if sys.version_info >= (3, 7): # py37+
return _restify_py37(cls)
return _restify_py37(cls, mode)
else:
return _restify_py36(cls)
return _restify_py36(cls, mode)
except (AttributeError, TypeError):
return inspect.object_description(cls)
def _restify_py37(cls: Optional[Type]) -> str:
def _restify_py37(cls: Optional[Type], mode: str = 'fully-qualified-except-typing') -> str:
"""Convert python class to a reST reference."""
from sphinx.util import inspect # lazy loading
if mode == 'smart':
modprefix = '~'
else:
modprefix = ''
if (inspect.isgenericalias(cls) and
cls.__module__ == 'typing' and cls.__origin__ is Union):
# Union
if len(cls.__args__) > 1 and cls.__args__[-1] is NoneType:
if len(cls.__args__) > 2:
args = ', '.join(restify(a) for a in cls.__args__[:-1])
args = ', '.join(restify(a, mode) for a in cls.__args__[:-1])
return ':py:obj:`~typing.Optional`\\ [:obj:`~typing.Union`\\ [%s]]' % args
else:
return ':py:obj:`~typing.Optional`\\ [%s]' % restify(cls.__args__[0])
return ':py:obj:`~typing.Optional`\\ [%s]' % restify(cls.__args__[0], mode)
else:
args = ', '.join(restify(a) for a in cls.__args__)
args = ', '.join(restify(a, mode) for a in cls.__args__)
return ':py:obj:`~typing.Union`\\ [%s]' % args
elif inspect.isgenericalias(cls):
if isinstance(cls.__origin__, typing._SpecialForm):
text = restify(cls.__origin__) # type: ignore
text = restify(cls.__origin__, mode) # type: ignore
elif getattr(cls, '_name', None):
if cls.__module__ == 'typing':
text = ':py:class:`~%s.%s`' % (cls.__module__, cls._name)
else:
text = ':py:class:`%s.%s`' % (cls.__module__, cls._name)
text = ':py:class:`%s%s.%s`' % (modprefix, cls.__module__, cls._name)
else:
text = restify(cls.__origin__)
text = restify(cls.__origin__, mode)
origin = getattr(cls, '__origin__', None)
if not hasattr(cls, '__args__'):
@ -182,12 +200,12 @@ def _restify_py37(cls: Optional[Type]) -> str:
# Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
pass
elif cls.__module__ == 'typing' and cls._name == 'Callable':
args = ', '.join(restify(a) for a in cls.__args__[:-1])
text += r"\ [[%s], %s]" % (args, restify(cls.__args__[-1]))
args = ', '.join(restify(a, mode) for a in cls.__args__[:-1])
text += r"\ [[%s], %s]" % (args, restify(cls.__args__[-1], mode))
elif cls.__module__ == 'typing' and getattr(origin, '_name', None) == 'Literal':
text += r"\ [%s]" % ', '.join(repr(a) for a in cls.__args__)
elif cls.__args__:
text += r"\ [%s]" % ", ".join(restify(a) for a in cls.__args__)
text += r"\ [%s]" % ", ".join(restify(a, mode) for a in cls.__args__)
return text
elif isinstance(cls, typing._SpecialForm):
@ -196,7 +214,7 @@ def _restify_py37(cls: Optional[Type]) -> str:
if cls.__module__ == 'typing':
return ':py:class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
else:
return ':py:class:`%s.%s`' % (cls.__module__, cls.__qualname__)
return ':py:class:`%s%s.%s`' % (modprefix, cls.__module__, cls.__qualname__)
elif isinstance(cls, ForwardRef):
return ':py:class:`%s`' % cls.__forward_arg__
else:
@ -204,10 +222,15 @@ def _restify_py37(cls: Optional[Type]) -> str:
if cls.__module__ == 'typing':
return ':py:obj:`~%s.%s`' % (cls.__module__, cls.__name__)
else:
return ':py:obj:`%s.%s`' % (cls.__module__, cls.__name__)
return ':py:obj:`%s%s.%s`' % (modprefix, cls.__module__, cls.__name__)
def _restify_py36(cls: Optional[Type]) -> str:
def _restify_py36(cls: Optional[Type], mode: str = 'fully-qualified-except-typing') -> str:
if mode == 'smart':
modprefix = '~'
else:
modprefix = ''
module = getattr(cls, '__module__', None)
if module == 'typing':
if getattr(cls, '_name', None):
@ -221,7 +244,7 @@ def _restify_py36(cls: Optional[Type]) -> str:
else:
qualname = repr(cls).replace('typing.', '')
elif hasattr(cls, '__qualname__'):
qualname = '%s.%s' % (module, cls.__qualname__)
qualname = '%s%s.%s' % (modprefix, module, cls.__qualname__)
else:
qualname = repr(cls)
@ -230,11 +253,11 @@ def _restify_py36(cls: Optional[Type]) -> str:
if module == 'typing':
reftext = ':py:class:`~typing.%s`' % qualname
else:
reftext = ':py:class:`%s`' % qualname
reftext = ':py:class:`%s%s`' % (modprefix, qualname)
params = cls.__args__
if params:
param_str = ', '.join(restify(p) for p in params)
param_str = ', '.join(restify(p, mode) for p in params)
return reftext + '\\ [%s]' % param_str
else:
return reftext
@ -242,19 +265,19 @@ def _restify_py36(cls: Optional[Type]) -> str:
if module == 'typing':
reftext = ':py:class:`~typing.%s`' % qualname
else:
reftext = ':py:class:`%s`' % qualname
reftext = ':py:class:`%s%s`' % (modprefix, qualname)
if cls.__args__ is None or len(cls.__args__) <= 2:
params = cls.__args__
elif cls.__origin__ == Generator:
params = cls.__args__
else: # typing.Callable
args = ', '.join(restify(arg) for arg in cls.__args__[:-1])
result = restify(cls.__args__[-1])
args = ', '.join(restify(arg, mode) for arg in cls.__args__[:-1])
result = restify(cls.__args__[-1], mode)
return reftext + '\\ [[%s], %s]' % (args, result)
if params:
param_str = ', '.join(restify(p) for p in params)
param_str = ', '.join(restify(p, mode) for p in params)
return reftext + '\\ [%s]' % (param_str)
else:
return reftext
@ -264,13 +287,13 @@ def _restify_py36(cls: Optional[Type]) -> str:
if params is not None:
if len(params) > 1 and params[-1] is NoneType:
if len(params) > 2:
param_str = ", ".join(restify(p) for p in params[:-1])
param_str = ", ".join(restify(p, mode) for p in params[:-1])
return (':py:obj:`~typing.Optional`\\ '
'[:py:obj:`~typing.Union`\\ [%s]]' % param_str)
else:
return ':py:obj:`~typing.Optional`\\ [%s]' % restify(params[0])
return ':py:obj:`~typing.Optional`\\ [%s]' % restify(params[0], mode)
else:
param_str = ', '.join(restify(p) for p in params)
param_str = ', '.join(restify(p, mode) for p in params)
return ':py:obj:`~typing.Union`\\ [%s]' % param_str
else:
return ':py:obj:`Union`'
@ -278,25 +301,25 @@ def _restify_py36(cls: Optional[Type]) -> str:
if cls.__module__ == 'typing':
return ':py:class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
else:
return ':py:class:`%s.%s`' % (cls.__module__, cls.__qualname__)
return ':py:class:`%s%s.%s`' % (modprefix, cls.__module__, cls.__qualname__)
elif hasattr(cls, '_name'):
# SpecialForm
if cls.__module__ == 'typing':
return ':py:obj:`~%s.%s`' % (cls.__module__, cls._name)
else:
return ':py:obj:`%s.%s`' % (cls.__module__, cls._name)
return ':py:obj:`%s%s.%s`' % (modprefix, cls.__module__, cls._name)
elif hasattr(cls, '__name__'):
# not a class (ex. TypeVar)
if cls.__module__ == 'typing':
return ':py:obj:`~%s.%s`' % (cls.__module__, cls.__name__)
else:
return ':py:obj:`%s.%s`' % (cls.__module__, cls.__name__)
return ':py:obj:`%s%s.%s`' % (modprefix, cls.__module__, cls.__name__)
else:
# others (ex. Any)
if cls.__module__ == 'typing':
return ':py:obj:`~%s.%s`' % (cls.__module__, qualname)
else:
return ':py:obj:`%s.%s`' % (cls.__module__, qualname)
return ':py:obj:`%s%s.%s`' % (modprefix, cls.__module__, qualname)
def stringify(annotation: Any, mode: str = 'fully-qualified-except-typing') -> str:

View File

@ -9,3 +9,6 @@ C = Callable[[int], None] # a generic alias not having a doccomment
class Class:
#: A list of int
T = List[int]
#: A list of Class
L = List[Class]

View File

@ -1,3 +1,4 @@
from datetime import date
from typing import NewType, TypeVar
#: T1
@ -15,7 +16,7 @@ T4 = TypeVar("T4", covariant=True)
T5 = TypeVar("T5", contravariant=True)
#: T6
T6 = NewType("T6", int)
T6 = NewType("T6", date)
#: T7
T7 = TypeVar("T7", bound=int)
@ -26,4 +27,4 @@ class Class:
T1 = TypeVar("T1")
#: T6
T6 = NewType("T6", int)
T6 = NewType("T6", date)

View File

@ -1874,6 +1874,12 @@ def test_autodoc_GenericAlias(app):
'',
' alias of :py:class:`~typing.List`\\ [:py:class:`int`]',
'',
'.. py:attribute:: L',
' :module: target.genericalias',
'',
' A list of Class',
'',
'',
'.. py:attribute:: T',
' :module: target.genericalias',
'',
@ -1898,6 +1904,15 @@ def test_autodoc_GenericAlias(app):
' alias of :py:class:`~typing.List`\\ [:py:class:`int`]',
'',
'',
'.. py:data:: L',
' :module: target.genericalias',
'',
' A list of Class',
'',
' alias of :py:class:`~typing.List`\\ '
'[:py:class:`target.genericalias.Class`]',
'',
'',
'.. py:data:: T',
' :module: target.genericalias',
'',
@ -1935,7 +1950,7 @@ def test_autodoc_TypeVar(app):
'',
' T6',
'',
' alias of :py:class:`int`',
' alias of :py:class:`datetime.date`',
'',
'',
'.. py:data:: T1',
@ -1975,7 +1990,7 @@ def test_autodoc_TypeVar(app):
'',
' T6',
'',
' alias of :py:class:`int`',
' alias of :py:class:`datetime.date`',
'',
'',
'.. py:data:: T7',

View File

@ -183,7 +183,7 @@ def test_autoattribute_NewType(app):
'',
' T6',
'',
' alias of :py:class:`int`',
' alias of :py:class:`datetime.date`',
'',
]

View File

@ -111,7 +111,7 @@ def test_autodata_NewType(app):
'',
' T6',
'',
' alias of :py:class:`int`',
' alias of :py:class:`datetime.date`',
'',
]

View File

@ -1231,6 +1231,62 @@ def test_autodoc_typehints_format_short(app):
]
@pytest.mark.sphinx('html', testroot='ext-autodoc',
confoverrides={'autodoc_typehints_format': "short"})
def test_autodoc_typehints_format_short_for_class_alias(app):
actual = do_autodoc(app, 'class', 'target.classes.Alias')
assert list(actual) == [
'',
'.. py:attribute:: Alias',
' :module: target.classes',
'',
' alias of :py:class:`~target.classes.Foo`',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc',
confoverrides={'autodoc_typehints_format': "short"})
def test_autodoc_typehints_format_short_for_generic_alias(app):
actual = do_autodoc(app, 'data', 'target.genericalias.L')
if sys.version_info < (3, 7):
assert list(actual) == [
'',
'.. py:data:: L',
' :module: target.genericalias',
' :value: typing.List[target.genericalias.Class]',
'',
' A list of Class',
'',
]
else:
assert list(actual) == [
'',
'.. py:data:: L',
' :module: target.genericalias',
'',
' A list of Class',
'',
' alias of :py:class:`~typing.List`\\ [:py:class:`~target.genericalias.Class`]',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc',
confoverrides={'autodoc_typehints_format': "short"})
def test_autodoc_typehints_format_short_for_newtype_alias(app):
actual = do_autodoc(app, 'data', 'target.typevar.T6')
assert list(actual) == [
'',
'.. py:data:: T6',
' :module: target.typevar',
'',
' T6',
'',
' alias of :py:class:`~datetime.date`',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_default_options(app):
# no settings

View File

@ -43,13 +43,28 @@ class BrokenType:
def test_restify():
assert restify(int) == ":py:class:`int`"
assert restify(int, "smart") == ":py:class:`int`"
assert restify(str) == ":py:class:`str`"
assert restify(str, "smart") == ":py:class:`str`"
assert restify(None) == ":py:obj:`None`"
assert restify(None, "smart") == ":py:obj:`None`"
assert restify(Integral) == ":py:class:`numbers.Integral`"
assert restify(Integral, "smart") == ":py:class:`~numbers.Integral`"
assert restify(Struct) == ":py:class:`struct.Struct`"
assert restify(Struct, "smart") == ":py:class:`~struct.Struct`"
assert restify(TracebackType) == ":py:class:`types.TracebackType`"
assert restify(TracebackType, "smart") == ":py:class:`~types.TracebackType`"
assert restify(Any) == ":py:obj:`~typing.Any`"
assert restify(Any, "smart") == ":py:obj:`~typing.Any`"
assert restify('str') == "str"
assert restify('str', "smart") == "str"
def test_restify_type_hints_containers():
@ -99,13 +114,24 @@ def test_restify_type_hints_Union():
if sys.version_info >= (3, 7):
assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ "
"[:py:class:`int`, :py:class:`numbers.Integral`]")
assert restify(Union[int, Integral], "smart") == (":py:obj:`~typing.Union`\\ "
"[:py:class:`int`,"
" :py:class:`~numbers.Integral`]")
assert (restify(Union[MyClass1, MyClass2]) ==
(":py:obj:`~typing.Union`\\ "
"[:py:class:`tests.test_util_typing.MyClass1`, "
":py:class:`tests.test_util_typing.<MyClass2>`]"))
assert (restify(Union[MyClass1, MyClass2], "smart") ==
(":py:obj:`~typing.Union`\\ "
"[:py:class:`~tests.test_util_typing.MyClass1`,"
" :py:class:`~tests.test_util_typing.<MyClass2>`]"))
else:
assert restify(Union[int, Integral]) == ":py:class:`numbers.Integral`"
assert restify(Union[int, Integral], "smart") == ":py:class:`~numbers.Integral`"
assert restify(Union[MyClass1, MyClass2]) == ":py:class:`tests.test_util_typing.MyClass1`"
assert restify(Union[MyClass1, MyClass2], "smart") == ":py:class:`~tests.test_util_typing.MyClass1`"
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
@ -115,19 +141,31 @@ def test_restify_type_hints_typevars():
T_contra = TypeVar('T_contra', contravariant=True)
assert restify(T) == ":py:obj:`tests.test_util_typing.T`"
assert restify(T, "smart") == ":py:obj:`~tests.test_util_typing.T`"
assert restify(T_co) == ":py:obj:`tests.test_util_typing.T_co`"
assert restify(T_co, "smart") == ":py:obj:`~tests.test_util_typing.T_co`"
assert restify(T_contra) == ":py:obj:`tests.test_util_typing.T_contra`"
assert restify(T_contra, "smart") == ":py:obj:`~tests.test_util_typing.T_contra`"
assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util_typing.T`]"
assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util_typing.T`]"
if sys.version_info >= (3, 10):
assert restify(MyInt) == ":py:class:`tests.test_util_typing.MyInt`"
assert restify(MyInt, "smart") == ":py:class:`~tests.test_util_typing.MyInt`"
else:
assert restify(MyInt) == ":py:class:`MyInt`"
assert restify(MyInt, "smart") == ":py:class:`MyInt`"
def test_restify_type_hints_custom_class():
assert restify(MyClass1) == ":py:class:`tests.test_util_typing.MyClass1`"
assert restify(MyClass1, "smart") == ":py:class:`~tests.test_util_typing.MyClass1`"
assert restify(MyClass2) == ":py:class:`tests.test_util_typing.<MyClass2>`"
assert restify(MyClass2, "smart") == ":py:class:`~tests.test_util_typing.<MyClass2>`"
def test_restify_type_hints_alias():
@ -169,12 +207,14 @@ def test_restify_type_union_operator():
def test_restify_broken_type_hints():
assert restify(BrokenType) == ':py:class:`tests.test_util_typing.BrokenType`'
assert restify(BrokenType, "smart") == ':py:class:`~tests.test_util_typing.BrokenType`'
def test_restify_mock():
with mock(['unknown']):
import unknown
assert restify(unknown.secret.Class) == ':py:class:`unknown.secret.Class`'
assert restify(unknown.secret.Class, "smart") == ':py:class:`~unknown.secret.Class`'
def test_stringify():