diff --git a/CHANGES b/CHANGES index 4a2f61e7cb7..ecea015bb68 100644 --- a/CHANGES +++ b/CHANGES @@ -88,6 +88,7 @@ Bugs fixed attribute not having any comment * #9364: autodoc: single element tuple on the default argument value is wrongly rendered +* #9362: autodoc: AttributeError is raised on processing a subclass of Tuple[()] * #9317: html: Pushing left key causes visiting the next page at the first page * #9381: html: URL for html_favicon and html_log does not work * #9270: html theme : pyramid theme generates incorrect logo links diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 279268aee32..1adbaa52686 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -105,27 +105,30 @@ def restify(cls: Optional[Type]) -> str: """Convert python class to a reST reference.""" from sphinx.util import inspect # lazy loading - if cls is None or cls is NoneType: - return ':obj:`None`' - elif cls is Ellipsis: - return '...' - elif cls in INVALID_BUILTIN_CLASSES: - return ':class:`%s`' % INVALID_BUILTIN_CLASSES[cls] - elif inspect.isNewType(cls): - return ':class:`%s`' % cls.__name__ - elif types_Union and isinstance(cls, types_Union): - if len(cls.__args__) > 1 and None in cls.__args__: - args = ' | '.join(restify(a) for a in cls.__args__ if a) - return 'Optional[%s]' % args - else: - return ' | '.join(restify(a) for a in cls.__args__) - elif cls.__module__ in ('__builtin__', 'builtins'): - return ':class:`%s`' % cls.__name__ - else: - if sys.version_info >= (3, 7): # py37+ - return _restify_py37(cls) + try: + if cls is None or cls is NoneType: + return ':obj:`None`' + elif cls is Ellipsis: + return '...' + elif cls in INVALID_BUILTIN_CLASSES: + return ':class:`%s`' % INVALID_BUILTIN_CLASSES[cls] + elif inspect.isNewType(cls): + return ':class:`%s`' % cls.__name__ + elif types_Union and isinstance(cls, types_Union): + if len(cls.__args__) > 1 and None in cls.__args__: + args = ' | '.join(restify(a) for a in cls.__args__ if a) + return 'Optional[%s]' % args + else: + return ' | '.join(restify(a) for a in cls.__args__) + elif cls.__module__ in ('__builtin__', 'builtins'): + return ':class:`%s`' % cls.__name__ else: - return _restify_py36(cls) + if sys.version_info >= (3, 7): # py37+ + return _restify_py37(cls) + else: + return _restify_py36(cls) + except AttributeError: + return repr(cls) def _restify_py37(cls: Optional[Type]) -> str: diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index d3672f2cc64..424715b39a2 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -60,6 +60,7 @@ def test_restify_type_hints_containers(): assert restify(Tuple[str, str, str]) == (":class:`~typing.Tuple`\\ " "[:class:`str`, :class:`str`, :class:`str`]") assert restify(Tuple[str, ...]) == ":class:`~typing.Tuple`\\ [:class:`str`, ...]" + assert restify(Tuple[()]) == ":class:`~typing.Tuple`\\ [()]" assert restify(List[Dict[str, Tuple]]) == (":class:`~typing.List`\\ " "[:class:`~typing.Dict`\\ " "[:class:`str`, :class:`~typing.Tuple`]]") @@ -168,6 +169,7 @@ def test_stringify_type_hints_containers(): assert stringify(Dict[str, float]) == "Dict[str, float]" assert stringify(Tuple[str, str, str]) == "Tuple[str, str, str]" assert stringify(Tuple[str, ...]) == "Tuple[str, ...]" + assert stringify(Tuple[()]) == "Tuple[()]" assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]" assert stringify(MyList[Tuple[int, int]]) == "tests.test_util_typing.MyList[Tuple[int, int]]" assert stringify(Generator[None, None, None]) == "Generator[None, None, None]"