diff --git a/CHANGES b/CHANGES
index 53567756f91..cd065a3a7d1 100644
--- a/CHANGES
+++ b/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
--------
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index a0c5cf61f1d..1cb8475df4a 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -37,7 +37,7 @@
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 @@ def add_directive_header(self, sig: str) -> None:
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
+ 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:
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
index 27f478675e1..1eaccb6789e 100644
--- a/sphinx/util/inspect.py
+++ b/sphinx/util/inspect.py
@@ -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
diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py
index 76438889b38..7b3509b62d6 100644
--- a/sphinx/util/typing.py
+++ b/sphinx/util/typing.py
@@ -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 @@ def _evaluate(self, globalns: Dict, localns: Dict) -> Any:
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):
diff --git a/tests/roots/test-ext-autodoc/target/classes.py b/tests/roots/test-ext-autodoc/target/classes.py
index 52c23748b77..7e7d7bcd30e 100644
--- a/tests/roots/test-ext-autodoc/target/classes.py
+++ b/tests/roots/test-ext-autodoc/target/classes.py
@@ -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
diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py
index 76f01f5b36d..fdbff1186b4 100644
--- a/tests/test_ext_autodoc_autoclass.py
+++ b/tests/test_ext_autodoc_autoclass.py
@@ -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]]',
+ '',
+ ]
diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py
index 3f8ddbb37dc..4059dc6bdda 100644
--- a/tests/test_util_typing.py
+++ b/tests/test_util_typing.py
@@ -16,7 +16,7 @@
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.`]")
+ 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.`"
+
+
+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"