Skip to content

Commit

Permalink
Fix #759: autodoc: Add sphinx.ext.autodoc.preserve_defaults extension
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tk0miya committed Jan 27, 2021
1 parent 62dad2f commit a6e9251
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Features added
info-field-list
* #8514: autodoc: Default values of overloaded functions are taken from actual
implementation if they're ellipsis
* #759: autodoc: 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
* #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
82 changes: 82 additions & 0 deletions sphinx/ext/autodoc/preserve_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
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."""
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)))
parameters[i] = param.replace(default=value)
else:
value = DefaultValue(ast_unparse(kw_defaults.pop(0)))
parameters[i] = param.replace(default=value)
sig = sig.replace(parameters=parameters)
obj.__signature__ = sig
except NotImplementedError as exc: # failed to ast.unparse()
logger.warning(__("Failed to parse type_comment for %r: %s"), obj, exc)


def setup(app: Sphinx) -> Dict[str, Any]:
app.setup_extension('sphinx.ext.autodoc')
app.connect('autodoc-before-process-signature', update_defvalue)

return {
'version': '1.0',
'parallel_read_safe': True
}
20 changes: 20 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,20 @@
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={'extensions': ['sphinx.ext.autodoc.preserve_defaults']})
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 a6e9251

Please sign in to comment.