summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakeshi KOMIYA <i.tkomiya@gmail.com>2020-07-26 16:05:14 +0900
committerTakeshi KOMIYA <i.tkomiya@gmail.com>2020-10-03 16:03:35 +0900
commitf2c0dfe7c454589d9a2681369e51a0d073bfd4ba (patch)
treed20ec4b8984321e8a679f2dd381c0f9be421b306
parent1ff1f3cf5b2f3402074d84395a0374fee8ac2a9e (diff)
downloadsphinx-git-f2c0dfe7c454589d9a2681369e51a0d073bfd4ba.tar.gz
Close #6518: autodoc: Add autodoc_type_aliases
autodoc_type_aliases allows to keep user defined type alises not evaluated in the generated document.
-rw-r--r--doc/usage/extensions/autodoc.rst38
-rw-r--r--sphinx/ext/autodoc/__init__.py37
-rw-r--r--sphinx/util/inspect.py6
-rw-r--r--sphinx/util/typing.py6
-rw-r--r--tests/roots/test-ext-autodoc/target/annotations.py25
-rw-r--r--tests/test_ext_autodoc_configs.py48
6 files changed, 144 insertions, 16 deletions
diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst
index 802be3bd0..2d8a216c0 100644
--- a/doc/usage/extensions/autodoc.rst
+++ b/doc/usage/extensions/autodoc.rst
@@ -515,6 +515,44 @@ There are also config values that you can set:
New option ``'description'`` is added.
+.. confval:: autodoc_type_aliases
+
+ A dictionary for users defined `type aliases`__ that maps a type name to the
+ full-qualified object name. It is used to keep type aliases not evaluated in
+ the document. Defaults to empty (``{}``).
+
+ The type aliases are only available if your program enables `Postponed
+ Evaluation of Annotations (PEP 563)`__ feature via ``from __future__ import
+ annotations``.
+
+ For example, there is code using a type alias::
+
+ from __future__ import annotations
+
+ AliasType = Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]]
+
+ def f() -> AliasType:
+ ...
+
+ If ``autodoc_type_aliases`` is not set, autodoc will generate internal mark-up
+ from this code as following::
+
+ .. py:function:: f() -> Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]]
+
+ ...
+
+ If you set ``autodoc_type_aliases`` as
+ ``{'AliasType': 'your.module.TypeAlias'}``, it generates a following document
+ internally::
+
+ .. py:function:: f() -> your.module.AliasType:
+
+ ...
+
+ .. __: https://www.python.org/dev/peps/pep-0563/
+ .. __: https://mypy.readthedocs.io/en/latest/kinds_of_types.html#type-aliases
+ .. versionadded:: 3.3
+
.. confval:: autodoc_warningiserror
This value controls the behavior of :option:`sphinx-build -W` during
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 35bb1c90d..d7c5d2242 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -1213,7 +1213,8 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
try:
self.env.app.emit('autodoc-before-process-signature', self.object, False)
- sig = inspect.signature(self.object, follow_wrapped=True)
+ sig = inspect.signature(self.object, follow_wrapped=True,
+ type_aliases=self.env.config.autodoc_type_aliases)
args = stringify_signature(sig, **kwargs)
except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
@@ -1262,7 +1263,9 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
if overloaded:
__globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
- overload = evaluate_signature(overload, __globals__)
+ overload = evaluate_signature(overload, __globals__,
+ self.env.config.autodoc_type_aliases)
+
sig = stringify_signature(overload, **kwargs)
sigs.append(sig)
@@ -1271,7 +1274,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed."""
try:
- sig = inspect.signature(func)
+ sig = inspect.signature(func, type_aliases=self.env.config.autodoc_type_aliases)
except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc)
@@ -1392,7 +1395,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if call is not None:
self.env.app.emit('autodoc-before-process-signature', call, True)
try:
- sig = inspect.signature(call, bound_method=True)
+ sig = inspect.signature(call, bound_method=True,
+ type_aliases=self.env.config.autodoc_type_aliases)
return type(self.object), '__call__', sig
except ValueError:
pass
@@ -1407,7 +1411,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if new is not None:
self.env.app.emit('autodoc-before-process-signature', new, True)
try:
- sig = inspect.signature(new, bound_method=True)
+ sig = inspect.signature(new, bound_method=True,
+ type_aliases=self.env.config.autodoc_type_aliases)
return self.object, '__new__', sig
except ValueError:
pass
@@ -1417,7 +1422,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if init is not None:
self.env.app.emit('autodoc-before-process-signature', init, True)
try:
- sig = inspect.signature(init, bound_method=True)
+ sig = inspect.signature(init, bound_method=True,
+ type_aliases=self.env.config.autodoc_type_aliases)
return self.object, '__init__', sig
except ValueError:
pass
@@ -1428,7 +1434,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
# the signature from, so just pass the object itself to our hook.
self.env.app.emit('autodoc-before-process-signature', self.object, False)
try:
- sig = inspect.signature(self.object, bound_method=False)
+ sig = inspect.signature(self.object, bound_method=False,
+ type_aliases=self.env.config.autodoc_type_aliases)
return None, None, sig
except ValueError:
pass
@@ -1475,7 +1482,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
method = safe_getattr(self._signature_class, self._signature_method_name, None)
__globals__ = safe_getattr(method, '__globals__', {})
for overload in self.analyzer.overloads.get(qualname):
- overload = evaluate_signature(overload, __globals__)
+ overload = evaluate_signature(overload, __globals__,
+ self.env.config.autodoc_type_aliases)
parameters = list(overload.parameters.values())
overload = overload.replace(parameters=parameters[1:],
@@ -1820,11 +1828,13 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
else:
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
self.env.app.emit('autodoc-before-process-signature', self.object, False)
- sig = inspect.signature(self.object, bound_method=False)
+ sig = inspect.signature(self.object, bound_method=False,
+ type_aliases=self.env.config.autodoc_type_aliases)
else:
self.env.app.emit('autodoc-before-process-signature', self.object, True)
sig = inspect.signature(self.object, bound_method=True,
- follow_wrapped=True)
+ follow_wrapped=True,
+ type_aliases=self.env.config.autodoc_type_aliases)
args = stringify_signature(sig, **kwargs)
except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"),
@@ -1884,7 +1894,9 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
if overloaded:
__globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
- overload = evaluate_signature(overload, __globals__)
+ overload = evaluate_signature(overload, __globals__,
+ self.env.config.autodoc_type_aliases)
+
if not inspect.isstaticmethod(self.object, cls=self.parent,
name=self.object_name):
parameters = list(overload.parameters.values())
@@ -1897,7 +1909,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed."""
try:
- sig = inspect.signature(func)
+ sig = inspect.signature(func, type_aliases=self.env.config.autodoc_type_aliases)
except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"),
self.fullname, exc)
@@ -2237,6 +2249,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value('autodoc_mock_imports', [], True)
app.add_config_value('autodoc_typehints', "signature", True,
ENUM("signature", "description", "none"))
+ app.add_config_value('autodoc_type_aliases', {}, True)
app.add_config_value('autodoc_warningiserror', True, True)
app.add_config_value('autodoc_inherit_docstrings', True, True)
app.add_event('autodoc-before-process-signature')
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
index 37997e6b2..378174993 100644
--- a/sphinx/util/inspect.py
+++ b/sphinx/util/inspect.py
@@ -439,8 +439,8 @@ def _should_unwrap(subject: Callable) -> bool:
return False
-def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False
- ) -> inspect.Signature:
+def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False,
+ type_aliases: Dict = {}) -> inspect.Signature:
"""Return a Signature object for the given *subject*.
:param bound_method: Specify *subject* is a bound method or not
@@ -470,7 +470,7 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo
try:
# Update unresolved annotations using ``get_type_hints()``.
- annotations = typing.get_type_hints(subject)
+ annotations = typing.get_type_hints(subject, None, type_aliases)
for i, param in enumerate(parameters):
if isinstance(param.annotation, str) and param.name in annotations:
parameters[i] = param.replace(annotation=annotations[param.name])
diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py
index d71ca1b2d..4dac3b695 100644
--- a/sphinx/util/typing.py
+++ b/sphinx/util/typing.py
@@ -63,7 +63,11 @@ def is_system_TypeVar(typ: Any) -> bool:
def stringify(annotation: Any) -> str:
"""Stringify type annotation object."""
if isinstance(annotation, str):
- return annotation
+ if annotation.startswith("'") and annotation.endswith("'"):
+ # might be a double Forward-ref'ed type. Go unquoting.
+ return annotation[1:-2]
+ else:
+ return annotation
elif isinstance(annotation, TypeVar): # type: ignore
return annotation.__name__
elif not annotation:
diff --git a/tests/roots/test-ext-autodoc/target/annotations.py b/tests/roots/test-ext-autodoc/target/annotations.py
new file mode 100644
index 000000000..667149b26
--- /dev/null
+++ b/tests/roots/test-ext-autodoc/target/annotations.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+from typing import overload
+
+
+myint = int
+
+
+def sum(x: myint, y: myint) -> myint:
+ """docstring"""
+ return x + y
+
+
+@overload
+def mult(x: myint, y: myint) -> myint:
+ ...
+
+
+@overload
+def mult(x: float, y: float) -> float:
+ ...
+
+
+def mult(x, y):
+ """docstring"""
+ return x, y
diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py
index 3d0bfd395..7d51b7f0e 100644
--- a/tests/test_ext_autodoc_configs.py
+++ b/tests/test_ext_autodoc_configs.py
@@ -642,6 +642,54 @@ def test_autodoc_typehints_description_for_invalid_node(app):
restructuredtext.parse(app, text) # raises no error
+@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
+@pytest.mark.sphinx('text', testroot='ext-autodoc')
+def test_autodoc_type_aliases(app):
+ # default
+ options = {"members": None}
+ actual = do_autodoc(app, 'module', 'target.annotations', options)
+ assert list(actual) == [
+ '',
+ '.. py:module:: target.annotations',
+ '',
+ '',
+ '.. py:function:: mult(x: int, y: int) -> int',
+ ' mult(x: float, y: float) -> float',
+ ' :module: target.annotations',
+ '',
+ ' docstring',
+ '',
+ '',
+ '.. py:function:: sum(x: int, y: int) -> int',
+ ' :module: target.annotations',
+ '',
+ ' docstring',
+ '',
+ ]
+
+ # define aliases
+ app.config.autodoc_type_aliases = {'myint': 'myint'}
+ actual = do_autodoc(app, 'module', 'target.annotations', options)
+ assert list(actual) == [
+ '',
+ '.. py:module:: target.annotations',
+ '',
+ '',
+ '.. py:function:: mult(x: myint, y: myint) -> myint',
+ ' mult(x: float, y: float) -> float',
+ ' :module: target.annotations',
+ '',
+ ' docstring',
+ '',
+ '',
+ '.. py:function:: sum(x: myint, y: myint) -> myint',
+ ' :module: target.annotations',
+ '',
+ ' docstring',
+ '',
+ ]
+
+
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_default_options(app):
# no settings