diff options
author | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-07-26 16:05:14 +0900 |
---|---|---|
committer | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-10-03 16:03:35 +0900 |
commit | f2c0dfe7c454589d9a2681369e51a0d073bfd4ba (patch) | |
tree | d20ec4b8984321e8a679f2dd381c0f9be421b306 | |
parent | 1ff1f3cf5b2f3402074d84395a0374fee8ac2a9e (diff) | |
download | sphinx-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.rst | 38 | ||||
-rw-r--r-- | sphinx/ext/autodoc/__init__.py | 37 | ||||
-rw-r--r-- | sphinx/util/inspect.py | 6 | ||||
-rw-r--r-- | sphinx/util/typing.py | 6 | ||||
-rw-r--r-- | tests/roots/test-ext-autodoc/target/annotations.py | 25 | ||||
-rw-r--r-- | tests/test_ext_autodoc_configs.py | 48 |
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 |