Skip to content

Commit

Permalink
Merge pull request #8771 from tk0miya/759_preserve_defaults
Browse files Browse the repository at this point in the history
Fix #759: autodoc: Add sphinx.ext.autodoc.preserve_defaults extension
  • Loading branch information
tk0miya authored Mar 6, 2021
2 parents 647510e + 1ea11b1 commit c85d269
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -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()`
Expand Down
10 changes: 10 additions & 0 deletions doc/usage/extensions/autodoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
88 changes: 88 additions & 0 deletions sphinx/ext/autodoc/preserve_defaults.py
Original file line number Diff line number Diff line change
@@ -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
}
19 changes: 19 additions & 0 deletions tests/roots/test-ext-autodoc/target/preserve_defaults.py
Original file line number Diff line number Diff line change
@@ -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"""
45 changes: 45 additions & 0 deletions tests/test_ext_autodoc_preserve_defaults.py
Original file line number Diff line number Diff line change
@@ -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',
'',
]

0 comments on commit c85d269

Please sign in to comment.