From add2f9e5c8aec013d5a914d29c76043a736e830d Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 28 Jan 2021 01:57:46 +0900 Subject: [PATCH] Fix #759: autodoc: Add sphinx.ext.autodoc.preserve_defaults extension Add a new extension `sphinx.ext.autodoc.preserve_defaults`. It preserves the default argument values of function signatures in source code and keep them not evaluated for readability. This is an experimental extension and it will be integrated into autodoc core in Sphinx-4.0. --- CHANGES | 3 + doc/usage/extensions/autodoc.rst | 10 +++ sphinx/ext/autodoc/__init__.py | 1 + sphinx/ext/autodoc/preserve_defaults.py | 88 +++++++++++++++++++ .../target/preserve_defaults.py | 19 ++++ tests/test_ext_autodoc_preserve_defaults.py | 45 ++++++++++ 6 files changed, 166 insertions(+) create mode 100644 sphinx/ext/autodoc/preserve_defaults.py create mode 100644 tests/roots/test-ext-autodoc/target/preserve_defaults.py create mode 100644 tests/test_ext_autodoc_preserve_defaults.py diff --git a/CHANGES b/CHANGES index ef7c4137d1d..902ad9b1499 100644 --- a/CHANGES +++ b/CHANGES @@ -189,6 +189,9 @@ Features added * #8775: autodoc: Support type union operator (PEP-604) in Python 3.10 or above * #8297: autodoc: Allow to extend :confval:`autodoc_default_options` via directive options +* #759: autodoc: Add a new configuration :confval:`autodoc_preserve_defaults` as + an experimental feature. It preserves the default argument values of + functions in source code and keep them not evaluated for readability. * #8619: html: kbd role generates customizable HTML tags for compound keys * #8634: html: Allow to change the order of JS/CSS via ``priority`` parameter for :meth:`Sphinx.add_js_file()` and :meth:`Sphinx.add_css_file()` diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index f69ac8c5c05..ad58a1eb2b7 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -586,6 +586,16 @@ There are also config values that you can set: .. __: https://mypy.readthedocs.io/en/latest/kinds_of_types.html#type-aliases .. versionadded:: 3.3 +.. confval:: autodoc_preserve_defaults + + If True, the default argument values of functions will be not evaluated on + generating document. It preserves them as is in the source code. + + .. versionadded:: 4.0 + + Added as an experimental feature. This will be integrated into autodoc core + in the future. + .. 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 8d6781a1a5c..0b570930149 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -2634,6 +2634,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.connect('config-inited', migrate_autodoc_member_order, priority=800) + app.setup_extension('sphinx.ext.autodoc.preserve_defaults') app.setup_extension('sphinx.ext.autodoc.type_comment') app.setup_extension('sphinx.ext.autodoc.typehints') diff --git a/sphinx/ext/autodoc/preserve_defaults.py b/sphinx/ext/autodoc/preserve_defaults.py new file mode 100644 index 00000000000..3d859fe8eee --- /dev/null +++ b/sphinx/ext/autodoc/preserve_defaults.py @@ -0,0 +1,88 @@ +""" + sphinx.ext.autodoc.preserve_defaults + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Preserve the default argument values of function signatures in source code + and keep them not evaluated for readability. + + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import ast +import inspect +from typing import Any, Dict + +from sphinx.application import Sphinx +from sphinx.locale import __ +from sphinx.pycode.ast import parse as ast_parse +from sphinx.pycode.ast import unparse as ast_unparse +from sphinx.util import logging + +logger = logging.getLogger(__name__) + + +class DefaultValue: + def __init__(self, name: str) -> None: + self.name = name + + def __repr__(self) -> str: + return self.name + + +def get_function_def(obj: Any) -> ast.FunctionDef: + """Get FunctionDef object from living object. + This tries to parse original code for living object and returns + AST node for given *obj*. + """ + try: + source = inspect.getsource(obj) + if source.startswith((' ', r'\t')): + # subject is placed inside class or block. To read its docstring, + # this adds if-block before the declaration. + module = ast_parse('if True:\n' + source) + return module.body[0].body[0] # type: ignore + else: + module = ast_parse(source) + return module.body[0] # type: ignore + except (OSError, TypeError): # failed to load source code + return None + + +def update_defvalue(app: Sphinx, obj: Any, bound_method: bool) -> None: + """Update defvalue info of *obj* using type_comments.""" + if not app.config.autodoc_preserve_defaults: + return + + try: + function = get_function_def(obj) + if function.args.defaults or function.args.kw_defaults: + sig = inspect.signature(obj) + defaults = list(function.args.defaults) + kw_defaults = list(function.args.kw_defaults) + parameters = list(sig.parameters.values()) + for i, param in enumerate(parameters): + if param.default is not param.empty: + if param.kind in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD): + value = DefaultValue(ast_unparse(defaults.pop(0))) # type: ignore + parameters[i] = param.replace(default=value) + else: + value = DefaultValue(ast_unparse(kw_defaults.pop(0))) # type: ignore + parameters[i] = param.replace(default=value) + sig = sig.replace(parameters=parameters) + obj.__signature__ = sig + except (AttributeError, TypeError): + # failed to update signature (ex. built-in or extension types) + pass + except NotImplementedError as exc: # failed to ast.unparse() + logger.warning(__("Failed to parse a default argument value for %r: %s"), obj, exc) + + +def setup(app: Sphinx) -> Dict[str, Any]: + app.add_config_value('autodoc_preserve_defaults', False, True) + app.connect('autodoc-before-process-signature', update_defvalue) + + return { + 'version': '1.0', + 'parallel_read_safe': True + } diff --git a/tests/roots/test-ext-autodoc/target/preserve_defaults.py b/tests/roots/test-ext-autodoc/target/preserve_defaults.py new file mode 100644 index 00000000000..c927fa03521 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/preserve_defaults.py @@ -0,0 +1,19 @@ +from datetime import datetime +from typing import Any + +CONSTANT = 'foo' +SENTINEL = object() + + +def foo(name: str = CONSTANT, + sentinal: Any = SENTINEL, + now: datetime = datetime.now()) -> None: + """docstring""" + + +class Class: + """docstring""" + + def meth(self, name: str = CONSTANT, sentinal: Any = SENTINEL, + now: datetime = datetime.now()) -> None: + """docstring""" diff --git a/tests/test_ext_autodoc_preserve_defaults.py b/tests/test_ext_autodoc_preserve_defaults.py new file mode 100644 index 00000000000..e918bf40095 --- /dev/null +++ b/tests/test_ext_autodoc_preserve_defaults.py @@ -0,0 +1,45 @@ +""" + test_ext_autodoc_preserve_defaults + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test the autodoc extension. + + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import pytest + +from .test_ext_autodoc import do_autodoc + + +@pytest.mark.sphinx('html', testroot='ext-autodoc', + confoverrides={'autodoc_preserve_defaults': True}) +def test_preserve_defaults(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.preserve_defaults', options) + assert list(actual) == [ + '', + '.. py:module:: target.preserve_defaults', + '', + '', + '.. py:class:: Class', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + ' .. py:method:: Class.meth(name: str = CONSTANT, sentinal: Any = SENTINEL, ' + 'now: datetime.datetime = datetime.now()) -> None', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + '', + '.. py:function:: foo(name: str = CONSTANT, sentinal: Any = SENTINEL, now: ' + 'datetime.datetime = datetime.now()) -> None', + ' :module: target.preserve_defaults', + '', + ' docstring', + '', + ]