Repository: sphinx-doc/sphinx

Issue:
Exception in util/typing.py for empty tuple type: Tuple[()]
**Describe the bug**
Exception occurs in `util/typing.py`, when I generate documentation with Sphinx autodoc extension. This seems to be caused by Python type hint `Tuple[()]`, that is the type of empty tuple `()`:
```
Exception occurred:
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/util/typing.py", line 116, in restify
    elif cls.__module__ in ('__builtin__', 'builtins'):
AttributeError: 'tuple' object has no attribute '__module__'
```
This behaviour started with Sphinx 3.4, whereas it worked as expected in Sphinx 3.3.

**To Reproduce**
- create some source file to be documented with autodoc extension
- add Python type hint
- run sphinx-build
- observe full trace
```
# Sphinx version: 3.5.4
# Python version: 3.6.9 (CPython)
# Docutils version: 0.16 release
# Jinja2 version: 2.11.3
# Last messages:
#   building [html]: targets for 5 source files that are out of date
#   
#   updating environment:
#   
#   [new config]
#   
#   5 added, 0 changed, 0 removed
#   
#   reading sources... [ 20%] dol_launcher
#   
# Loaded extensions:
#   sphinx.ext.mathjax (3.5.4) from /home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/ext/mathjax.py
#   sphinxcontrib.applehelp (1.0.2) from /home/ftr8fe/.local/lib/python3.6/site-packages/sphinxcontrib/applehelp/__init__.py
#   sphinxcontrib.devhelp (1.0.2) from /home/ftr8fe/.local/lib/python3.6/site-packages/sphinxcontrib/devhelp/__init__.py
#   sphinxcontrib.htmlhelp (2.0.0) from /home/ftr8fe/.local/lib/python3.6/site-packages/sphinxcontrib/htmlhelp/__init__.py
#   sphinxcontrib.serializinghtml (1.1.5) from /home/ftr8fe/.local/lib/python3.6/site-packages/sphinxcontrib/serializinghtml/__init__.py
#   sphinxcontrib.qthelp (1.0.3) from /home/ftr8fe/.local/lib/python3.6/site-packages/sphinxcontrib/qthelp/__init__.py
#   alabaster (0.7.12) from /home/ftr8fe/.local/lib/python3.6/site-packages/alabaster/__init__.py
#   sphinx.ext.autodoc.type_comment (3.5.4) from /home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/ext/autodoc/type_comment.py
#   sphinx.ext.autodoc (3.5.4) from /home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/ext/autodoc/__init__.py
Traceback (most recent call last):
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/cmd/build.py", line 280, in build_main
    app.build(args.force_all, filenames)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/application.py", line 352, in build
    self.builder.build_update()
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/builders/__init__.py", line 298, in build_update
    len(to_build))
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/builders/__init__.py", line 310, in build
    updated_docnames = set(self.read())
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/builders/__init__.py", line 417, in read
    self._read_serial(docnames)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/builders/__init__.py", line 438, in _read_serial
    self.read_doc(docname)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/builders/__init__.py", line 478, in read_doc
    doctree = read_doc(self.app, self.env, self.env.doc2path(docname))
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/io.py", line 221, in read_doc
    pub.publish()
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/core.py", line 218, in publish
    self.settings)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/io.py", line 126, in read
    self.parse()
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/readers/__init__.py", line 77, in parse
    self.parser.parse(self.input, document)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/parsers.py", line 104, in parse
    self.statemachine.run(inputlines, document, inliner=self.inliner)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 171, in run
    input_source=document['source'])
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/statemachine.py", line 242, in run
    context, state, transitions)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/statemachine.py", line 459, in check_line
    return method(match, context, next_state)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 2769, in underline
    self.section(title, source, style, lineno - 1, messages)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 327, in section
    self.new_subsection(title, lineno, messages)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 395, in new_subsection
    node=section_node, match_titles=True)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 282, in nested_parse
    node=node, match_titles=match_titles)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 196, in run
    results = StateMachineWS.run(self, input_lines, input_offset)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/statemachine.py", line 242, in run
    context, state, transitions)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/statemachine.py", line 459, in check_line
    return method(match, context, next_state)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 2769, in underline
    self.section(title, source, style, lineno - 1, messages)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 327, in section
    self.new_subsection(title, lineno, messages)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 395, in new_subsection
    node=section_node, match_titles=True)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 282, in nested_parse
    node=node, match_titles=match_titles)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 196, in run
    results = StateMachineWS.run(self, input_lines, input_offset)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/statemachine.py", line 242, in run
    context, state, transitions)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/statemachine.py", line 459, in check_line
    return method(match, context, next_state)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 2342, in explicit_markup
    nodelist, blank_finish = self.explicit_construct(match)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 2354, in explicit_construct
    return method(self, expmatch)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 2097, in directive
    directive_class, match, type_name, option_presets)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/docutils/parsers/rst/states.py", line 2146, in run_directive
    result = directive_instance.run()
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/ext/autodoc/directive.py", line 167, in run
    documenter.generate(more_content=self.content)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/ext/autodoc/__init__.py", line 970, in generate
    self.document_members(all_members)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/ext/autodoc/__init__.py", line 853, in document_members
    check_module=members_check_module and not isattr)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/ext/autodoc/__init__.py", line 1728, in generate
    all_members=all_members)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/ext/autodoc/__init__.py", line 960, in generate
    self.add_directive_header(sig)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/ext/autodoc/__init__.py", line 1626, in add_directive_header
    bases = [restify(cls) for cls in self.object.__orig_bases__]
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/ext/autodoc/__init__.py", line 1626, in <listcomp>
    bases = [restify(cls) for cls in self.object.__orig_bases__]
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/util/typing.py", line 122, in restify
    return _restify_py36(cls)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/util/typing.py", line 215, in _restify_py36
    args = ', '.join(restify(arg) for arg in cls.__args__[:-1])  # type: ignore
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/util/typing.py", line 215, in <genexpr>
    args = ', '.join(restify(arg) for arg in cls.__args__[:-1])  # type: ignore
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/util/typing.py", line 122, in restify
    return _restify_py36(cls)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/util/typing.py", line 202, in _restify_py36
    param_str = ', '.join(restify(p) for p in params)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/util/typing.py", line 202, in <genexpr>
    param_str = ', '.join(restify(p) for p in params)
  File "/home/ftr8fe/.local/lib/python3.6/site-packages/sphinx/util/typing.py", line 116, in restify
    elif cls.__module__ in ('__builtin__', 'builtins'):
AttributeError: 'tuple' object has no attribute '__module__'
```

**Expected behavior**
- sphinx-build completes without error

**Your project**
- I cannot share anything because of company policy

**Environment info**
- OS: Linux
- Python version: 3.6, 3.8
- Sphinx version: 3.5
- Sphinx extensions: sphinx.ext.autodoc

Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index 4a2f61e7c..ecea015bb 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 279268aee..1adbaa526 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:


Test patch:
diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py
index d3672f2cc..424715b39 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]"