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.
This commit is contained in:
Takeshi KOMIYA
2021-01-28 01:57:46 +09:00
parent 647510e184
commit 1ea11b1e48
6 changed files with 166 additions and 0 deletions

View File

@@ -192,6 +192,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()`

View File

@@ -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

View File

@@ -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')

View File

@@ -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
}

View File

@@ -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"""

View File

@@ -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',
'',
]