mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Fix #8219: autodoc: Parameters for generic base class are not shown
This commit is contained in:
parent
5337e3848c
commit
e2bf9166da
3
CHANGES
3
CHANGES
@ -54,6 +54,9 @@ Features added
|
||||
Bugs fixed
|
||||
----------
|
||||
|
||||
* #8219: autodoc: Parameters for generic class are not shown when super class is
|
||||
a generic class and show-inheritance option is given (in Python 3.7 or above)
|
||||
|
||||
Testing
|
||||
--------
|
||||
|
||||
|
@ -37,7 +37,7 @@ from sphinx.util.docstrings import extract_metadata, prepare_docstring
|
||||
from sphinx.util.inspect import (
|
||||
evaluate_signature, getdoc, object_description, safe_getattr, stringify_signature
|
||||
)
|
||||
from sphinx.util.typing import stringify as stringify_typehint
|
||||
from sphinx.util.typing import restify, stringify as stringify_typehint
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
@ -1574,13 +1574,16 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
if not self.doc_as_attr and self.options.show_inheritance:
|
||||
sourcename = self.get_sourcename()
|
||||
self.add_line('', sourcename)
|
||||
if hasattr(self.object, '__bases__') and len(self.object.__bases__):
|
||||
bases = [':class:`%s`' % b.__name__
|
||||
if b.__module__ in ('__builtin__', 'builtins')
|
||||
else ':class:`%s.%s`' % (b.__module__, b.__qualname__)
|
||||
for b in self.object.__bases__]
|
||||
self.add_line(' ' + _('Bases: %s') % ', '.join(bases),
|
||||
sourcename)
|
||||
|
||||
if hasattr(self.object, '__orig_bases__') and len(self.object.__orig_bases__):
|
||||
# A subclass of generic types
|
||||
# refs: PEP-560 <https://www.python.org/dev/peps/pep-0560/>
|
||||
bases = [restify(cls) for cls in self.object.__orig_bases__]
|
||||
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
|
||||
elif hasattr(self.object, '__bases__') and len(self.object.__bases__):
|
||||
# A normal class
|
||||
bases = [restify(cls) for cls in self.object.__bases__]
|
||||
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
|
||||
|
||||
def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
|
||||
if encoding is not None:
|
||||
|
@ -312,6 +312,9 @@ def isgenericalias(obj: Any) -> bool:
|
||||
elif (hasattr(types, 'GenericAlias') and # only for py39+
|
||||
isinstance(obj, types.GenericAlias)): # type: ignore
|
||||
return True
|
||||
elif (hasattr(typing, '_SpecialGenericAlias') and # for py39+
|
||||
isinstance(obj, typing._SpecialGenericAlias)): # type: ignore
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
import sys
|
||||
import typing
|
||||
from typing import Any, Callable, Dict, Generator, List, Tuple, TypeVar, Union
|
||||
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, TypeVar, Union
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst.states import Inliner
|
||||
@ -30,6 +30,10 @@ else:
|
||||
ref = _ForwardRef(self.arg)
|
||||
return ref._eval_type(globalns, localns)
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
from typing import Type # NOQA # for python3.5.1
|
||||
|
||||
|
||||
# An entry of Directive.option_spec
|
||||
DirectiveOption = Callable[[str], Any]
|
||||
@ -60,6 +64,195 @@ 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."""
|
||||
if cls is None or cls is NoneType:
|
||||
return ':obj:`None`'
|
||||
elif cls is Ellipsis:
|
||||
return '...'
|
||||
elif cls.__module__ in ('__builtin__', 'builtins'):
|
||||
return ':class:`%s`' % cls.__name__
|
||||
else:
|
||||
if sys.version_info >= (3, 7): # py37+
|
||||
return _restify_py37(cls)
|
||||
else:
|
||||
return _restify_py36(cls)
|
||||
|
||||
|
||||
def _restify_py37(cls: Optional["Type"]) -> str:
|
||||
"""Convert python class to a reST reference."""
|
||||
from sphinx.util import inspect # lazy loading
|
||||
|
||||
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])
|
||||
return ':obj:`Optional`\\ [:obj:`Union`\\ [%s]]' % args
|
||||
else:
|
||||
return ':obj:`Optional`\\ [%s]' % restify(cls.__args__[0])
|
||||
else:
|
||||
args = ', '.join(restify(a) for a in cls.__args__)
|
||||
return ':obj:`Union`\\ [%s]' % args
|
||||
elif inspect.isgenericalias(cls):
|
||||
if getattr(cls, '_name', None):
|
||||
if cls.__module__ == 'typing':
|
||||
text = ':class:`%s`' % cls._name
|
||||
else:
|
||||
text = ':class:`%s.%s`' % (cls.__module__, cls._name)
|
||||
else:
|
||||
text = restify(cls.__origin__)
|
||||
|
||||
if not hasattr(cls, '__args__'):
|
||||
pass
|
||||
elif all(is_system_TypeVar(a) for a in cls.__args__):
|
||||
# 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]))
|
||||
elif cls.__args__:
|
||||
text += r"\ [%s]" % ", ".join(restify(a) for a in cls.__args__)
|
||||
|
||||
return text
|
||||
elif hasattr(cls, '__qualname__'):
|
||||
if cls.__module__ == 'typing':
|
||||
return ':class:`%s`' % cls.__qualname__
|
||||
else:
|
||||
return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__)
|
||||
elif hasattr(cls, '_name'):
|
||||
# SpecialForm
|
||||
if cls.__module__ == 'typing':
|
||||
return ':obj:`%s`' % cls._name
|
||||
else:
|
||||
return ':obj:`%s.%s`' % (cls.__module__, cls._name)
|
||||
else:
|
||||
# not a class (ex. TypeVar)
|
||||
return ':obj:`%s.%s`' % (cls.__module__, cls.__name__)
|
||||
|
||||
|
||||
def _restify_py36(cls: Optional["Type"]) -> str:
|
||||
module = getattr(cls, '__module__', None)
|
||||
if module == 'typing':
|
||||
if getattr(cls, '_name', None):
|
||||
qualname = cls._name
|
||||
elif getattr(cls, '__qualname__', None):
|
||||
qualname = cls.__qualname__
|
||||
elif getattr(cls, '__forward_arg__', None):
|
||||
qualname = cls.__forward_arg__
|
||||
elif getattr(cls, '__origin__', None):
|
||||
qualname = stringify(cls.__origin__) # ex. Union
|
||||
else:
|
||||
qualname = repr(cls).replace('typing.', '')
|
||||
elif hasattr(cls, '__qualname__'):
|
||||
qualname = '%s.%s' % (module, cls.__qualname__)
|
||||
else:
|
||||
qualname = repr(cls)
|
||||
|
||||
if (isinstance(cls, typing.TupleMeta) and # type: ignore
|
||||
not hasattr(cls, '__tuple_params__')): # for Python 3.6
|
||||
params = cls.__args__
|
||||
if params:
|
||||
param_str = ', '.join(restify(p) for p in params)
|
||||
return ':class:`%s`\\ [%s]' % (qualname, param_str)
|
||||
else:
|
||||
return ':class:`%s`' % qualname
|
||||
elif isinstance(cls, typing.GenericMeta):
|
||||
params = None
|
||||
if hasattr(cls, '__args__'):
|
||||
# for Python 3.5.2+
|
||||
if cls.__args__ is None or len(cls.__args__) <= 2: # type: ignore # NOQA
|
||||
params = cls.__args__ # type: ignore
|
||||
elif cls.__origin__ == Generator: # type: ignore
|
||||
params = cls.__args__ # type: ignore
|
||||
else: # typing.Callable
|
||||
args = ', '.join(restify(arg) for arg in cls.__args__[:-1]) # type: ignore
|
||||
result = restify(cls.__args__[-1]) # type: ignore
|
||||
return ':class:`%s`\\ [[%s], %s]' % (qualname, args, result)
|
||||
elif hasattr(cls, '__parameters__'):
|
||||
# for Python 3.5.0 and 3.5.1
|
||||
params = cls.__parameters__ # type: ignore
|
||||
|
||||
if params:
|
||||
param_str = ', '.join(restify(p) for p in params)
|
||||
return ':class:`%s`\\ [%s]' % (qualname, param_str)
|
||||
else:
|
||||
return ':class:`%s`' % qualname
|
||||
elif (hasattr(typing, 'UnionMeta') and
|
||||
isinstance(cls, typing.UnionMeta) and # type: ignore
|
||||
hasattr(cls, '__union_params__')): # for Python 3.5
|
||||
params = cls.__union_params__
|
||||
if params is not None:
|
||||
if len(params) == 2 and params[1] is NoneType:
|
||||
return ':obj:`Optional`\\ [%s]' % restify(params[0])
|
||||
else:
|
||||
param_str = ', '.join(restify(p) for p in params)
|
||||
return ':obj:`%s`\\ [%s]' % (qualname, param_str)
|
||||
else:
|
||||
return ':obj:`%s`' % qualname
|
||||
elif (hasattr(cls, '__origin__') and
|
||||
cls.__origin__ is typing.Union): # for Python 3.5.2+
|
||||
params = cls.__args__
|
||||
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])
|
||||
return ':obj:`Optional`\\ [:obj:`Union`\\ [%s]]' % param_str
|
||||
else:
|
||||
return ':obj:`Optional`\\ [%s]' % restify(params[0])
|
||||
else:
|
||||
param_str = ', '.join(restify(p) for p in params)
|
||||
return ':obj:`Union`\\ [%s]' % param_str
|
||||
else:
|
||||
return ':obj:`Union`'
|
||||
elif (isinstance(cls, typing.CallableMeta) and # type: ignore
|
||||
getattr(cls, '__args__', None) is not None and
|
||||
hasattr(cls, '__result__')): # for Python 3.5
|
||||
# Skipped in the case of plain typing.Callable
|
||||
args = cls.__args__
|
||||
if args is None:
|
||||
return qualname
|
||||
elif args is Ellipsis:
|
||||
args_str = '...'
|
||||
else:
|
||||
formatted_args = (restify(a) for a in args) # type: ignore
|
||||
args_str = '[%s]' % ', '.join(formatted_args)
|
||||
|
||||
return ':class:`%s`\\ [%s, %s]' % (qualname, args_str, stringify(cls.__result__))
|
||||
elif (isinstance(cls, typing.TupleMeta) and # type: ignore
|
||||
hasattr(cls, '__tuple_params__') and
|
||||
hasattr(cls, '__tuple_use_ellipsis__')): # for Python 3.5
|
||||
params = cls.__tuple_params__
|
||||
if params is not None:
|
||||
param_strings = [restify(p) for p in params]
|
||||
if cls.__tuple_use_ellipsis__:
|
||||
param_strings.append('...')
|
||||
return ':class:`%s`\\ [%s]' % (qualname, ', '.join(param_strings))
|
||||
else:
|
||||
return ':class:`%s`' % qualname
|
||||
elif hasattr(cls, '__qualname__'):
|
||||
if cls.__module__ == 'typing':
|
||||
return ':class:`%s`' % cls.__qualname__
|
||||
else:
|
||||
return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__)
|
||||
elif hasattr(cls, '_name'):
|
||||
# SpecialForm
|
||||
if cls.__module__ == 'typing':
|
||||
return ':obj:`%s`' % cls._name
|
||||
else:
|
||||
return ':obj:`%s.%s`' % (cls.__module__, cls._name)
|
||||
elif hasattr(cls, '__name__'):
|
||||
# not a class (ex. TypeVar)
|
||||
return ':obj:`%s.%s`' % (cls.__module__, cls.__name__)
|
||||
else:
|
||||
# others (ex. Any)
|
||||
if cls.__module__ == 'typing':
|
||||
return ':obj:`%s`' % qualname
|
||||
else:
|
||||
return ':obj:`%s.%s`' % (cls.__module__, qualname)
|
||||
|
||||
|
||||
def stringify(annotation: Any) -> str:
|
||||
"""Stringify type annotation object."""
|
||||
if isinstance(annotation, str):
|
||||
|
@ -1,4 +1,5 @@
|
||||
from inspect import Parameter, Signature
|
||||
from typing import List, Union
|
||||
|
||||
|
||||
class Foo:
|
||||
@ -21,3 +22,8 @@ class Qux:
|
||||
|
||||
def __init__(self, x, y):
|
||||
pass
|
||||
|
||||
|
||||
class Quux(List[Union[int, float]]):
|
||||
"""A subclass of List[Union[int, float]]"""
|
||||
pass
|
||||
|
@ -9,6 +9,8 @@
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from test_ext_autodoc import do_autodoc
|
||||
@ -73,3 +75,20 @@ def test_decorators(app):
|
||||
' :module: target.decorator',
|
||||
'',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
|
||||
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
||||
def test_show_inheritance_for_subclass_of_generic_type(app):
|
||||
options = {'show-inheritance': True}
|
||||
actual = do_autodoc(app, 'class', 'target.classes.Quux', options)
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:class:: Quux(iterable=(), /)',
|
||||
' :module: target.classes',
|
||||
'',
|
||||
' Bases: :class:`List`\\ [:obj:`Union`\\ [:class:`int`, :class:`float`]]',
|
||||
'',
|
||||
' A subclass of List[Union[int, float]]',
|
||||
'',
|
||||
]
|
||||
|
@ -16,7 +16,7 @@ from typing import (
|
||||
|
||||
import pytest
|
||||
|
||||
from sphinx.util.typing import stringify
|
||||
from sphinx.util.typing import restify, stringify
|
||||
|
||||
|
||||
class MyClass1:
|
||||
@ -36,6 +36,80 @@ class BrokenType:
|
||||
__args__ = int
|
||||
|
||||
|
||||
def test_restify():
|
||||
assert restify(int) == ":class:`int`"
|
||||
assert restify(str) == ":class:`str`"
|
||||
assert restify(None) == ":obj:`None`"
|
||||
assert restify(Integral) == ":class:`numbers.Integral`"
|
||||
assert restify(Any) == ":obj:`Any`"
|
||||
|
||||
|
||||
def test_restify_type_hints_containers():
|
||||
assert restify(List) == ":class:`List`"
|
||||
assert restify(Dict) == ":class:`Dict`"
|
||||
assert restify(List[int]) == ":class:`List`\\ [:class:`int`]"
|
||||
assert restify(List[str]) == ":class:`List`\\ [:class:`str`]"
|
||||
assert restify(Dict[str, float]) == ":class:`Dict`\\ [:class:`str`, :class:`float`]"
|
||||
assert restify(Tuple[str, str, str]) == ":class:`Tuple`\\ [:class:`str`, :class:`str`, :class:`str`]"
|
||||
assert restify(Tuple[str, ...]) == ":class:`Tuple`\\ [:class:`str`, ...]"
|
||||
assert restify(List[Dict[str, Tuple]]) == ":class:`List`\\ [:class:`Dict`\\ [:class:`str`, :class:`Tuple`]]"
|
||||
assert restify(MyList[Tuple[int, int]]) == ":class:`test_util_typing.MyList`\\ [:class:`Tuple`\\ [:class:`int`, :class:`int`]]"
|
||||
assert restify(Generator[None, None, None]) == ":class:`Generator`\\ [:obj:`None`, :obj:`None`, :obj:`None`]"
|
||||
|
||||
|
||||
def test_restify_type_hints_Callable():
|
||||
assert restify(Callable) == ":class:`Callable`"
|
||||
|
||||
if sys.version_info >= (3, 7):
|
||||
assert restify(Callable[[str], int]) == ":class:`Callable`\\ [[:class:`str`], :class:`int`]"
|
||||
assert restify(Callable[..., int]) == ":class:`Callable`\\ [[...], :class:`int`]"
|
||||
else:
|
||||
assert restify(Callable[[str], int]) == ":class:`Callable`\\ [:class:`str`, :class:`int`]"
|
||||
assert restify(Callable[..., int]) == ":class:`Callable`\\ [..., :class:`int`]"
|
||||
|
||||
|
||||
def test_restify_type_hints_Union():
|
||||
assert restify(Optional[int]) == ":obj:`Optional`\\ [:class:`int`]"
|
||||
assert restify(Union[str, None]) == ":obj:`Optional`\\ [:class:`str`]"
|
||||
assert restify(Union[int, str]) == ":obj:`Union`\\ [:class:`int`, :class:`str`]"
|
||||
|
||||
if sys.version_info >= (3, 7):
|
||||
assert restify(Union[int, Integral]) == ":obj:`Union`\\ [:class:`int`, :class:`numbers.Integral`]"
|
||||
assert (restify(Union[MyClass1, MyClass2]) ==
|
||||
":obj:`Union`\\ [:class:`test_util_typing.MyClass1`, :class:`test_util_typing.<MyClass2>`]")
|
||||
else:
|
||||
assert restify(Union[int, Integral]) == ":class:`numbers.Integral`"
|
||||
assert restify(Union[MyClass1, MyClass2]) == ":class:`test_util_typing.MyClass1`"
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
|
||||
def test_restify_type_hints_typevars():
|
||||
T = TypeVar('T')
|
||||
T_co = TypeVar('T_co', covariant=True)
|
||||
T_contra = TypeVar('T_contra', contravariant=True)
|
||||
|
||||
assert restify(T) == ":obj:`test_util_typing.T`"
|
||||
assert restify(T_co) == ":obj:`test_util_typing.T_co`"
|
||||
assert restify(T_contra) == ":obj:`test_util_typing.T_contra`"
|
||||
assert restify(List[T]) == ":class:`List`\\ [:obj:`test_util_typing.T`]"
|
||||
|
||||
|
||||
def test_restify_type_hints_custom_class():
|
||||
assert restify(MyClass1) == ":class:`test_util_typing.MyClass1`"
|
||||
assert restify(MyClass2) == ":class:`test_util_typing.<MyClass2>`"
|
||||
|
||||
|
||||
def test_restify_type_hints_alias():
|
||||
MyStr = str
|
||||
MyTuple = Tuple[str, str]
|
||||
assert restify(MyStr) == ":class:`str`"
|
||||
assert restify(MyTuple) == ":class:`Tuple`\\ [:class:`str`, :class:`str`]" # type: ignore
|
||||
|
||||
|
||||
def test_restify_broken_type_hints():
|
||||
assert restify(BrokenType) == ':class:`test_util_typing.BrokenType`'
|
||||
|
||||
|
||||
def test_stringify():
|
||||
assert stringify(int) == "int"
|
||||
assert stringify(str) == "str"
|
||||
|
Loading…
Reference in New Issue
Block a user