Skip to content

Commit 71d6b34

Browse files
committed
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.
1 parent 0a3f897 commit 71d6b34

File tree

6 files changed

+163
-0
lines changed

6 files changed

+163
-0
lines changed

CHANGES

+3
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ Features added
189189
* #8775: autodoc: Support type union operator (PEP-604) in Python 3.10 or above
190190
* #8297: autodoc: Allow to extend :confval:`autodoc_default_options` via
191191
directive options
192+
* #759: autodoc: Add a new configuration :confval:`autodoc_preserve_defaults` as
193+
an experimental feature. It preserves the default argument values of
194+
functions in source code and keep them not evaluated for readability.
192195
* #8619: html: kbd role generates customizable HTML tags for compound keys
193196
* #8634: html: Allow to change the order of JS/CSS via ``priority`` parameter
194197
for :meth:`Sphinx.add_js_file()` and :meth:`Sphinx.add_css_file()`

doc/usage/extensions/autodoc.rst

+10
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,16 @@ There are also config values that you can set:
586586
.. __: https://mypy.readthedocs.io/en/latest/kinds_of_types.html#type-aliases
587587
.. versionadded:: 3.3
588588

589+
.. confval:: autodoc_preserve_defaults
590+
591+
If True, the default argument values of functions will be not evaluated on
592+
generating document. It preserves them as is in the source code.
593+
594+
.. versionadded:: 4.0
595+
596+
Added as an experimental feature. This will be integrated into autodoc core
597+
in the future.
598+
589599
.. confval:: autodoc_warningiserror
590600

591601
This value controls the behavior of :option:`sphinx-build -W` during

sphinx/ext/autodoc/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -2634,6 +2634,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
26342634

26352635
app.connect('config-inited', migrate_autodoc_member_order, priority=800)
26362636

2637+
app.setup_extension('sphinx.ext.autodoc.preserve_defaults')
26372638
app.setup_extension('sphinx.ext.autodoc.type_comment')
26382639
app.setup_extension('sphinx.ext.autodoc.typehints')
26392640

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""
2+
sphinx.ext.autodoc.preserve_defaults
3+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4+
5+
Preserve the default argument values of function signatures in source code
6+
and keep them not evaluated for readability.
7+
8+
:copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
9+
:license: BSD, see LICENSE for details.
10+
"""
11+
12+
import ast
13+
import inspect
14+
from typing import Any, Dict
15+
16+
from sphinx.application import Sphinx
17+
from sphinx.locale import __
18+
from sphinx.pycode.ast import parse as ast_parse
19+
from sphinx.pycode.ast import unparse as ast_unparse
20+
from sphinx.util import logging
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
class DefaultValue:
26+
def __init__(self, name: str) -> None:
27+
self.name = name
28+
29+
def __repr__(self) -> str:
30+
return self.name
31+
32+
33+
def get_function_def(obj: Any) -> ast.FunctionDef:
34+
"""Get FunctionDef object from living object.
35+
This tries to parse original code for living object and returns
36+
AST node for given *obj*.
37+
"""
38+
try:
39+
source = inspect.getsource(obj)
40+
if source.startswith((' ', r'\t')):
41+
# subject is placed inside class or block. To read its docstring,
42+
# this adds if-block before the declaration.
43+
module = ast_parse('if True:\n' + source)
44+
return module.body[0].body[0] # type: ignore
45+
else:
46+
module = ast_parse(source)
47+
return module.body[0] # type: ignore
48+
except (OSError, TypeError): # failed to load source code
49+
return None
50+
51+
52+
def update_defvalue(app: Sphinx, obj: Any, bound_method: bool) -> None:
53+
"""Update defvalue info of *obj* using type_comments."""
54+
try:
55+
function = get_function_def(obj)
56+
if function.args.defaults or function.args.kw_defaults:
57+
sig = inspect.signature(obj)
58+
defaults = list(function.args.defaults)
59+
kw_defaults = list(function.args.kw_defaults)
60+
parameters = list(sig.parameters.values())
61+
for i, param in enumerate(parameters):
62+
if param.default is not param.empty:
63+
if param.kind in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD):
64+
value = DefaultValue(ast_unparse(defaults.pop(0))) # type: ignore
65+
parameters[i] = param.replace(default=value)
66+
else:
67+
value = DefaultValue(ast_unparse(kw_defaults.pop(0))) # type: ignore
68+
parameters[i] = param.replace(default=value)
69+
sig = sig.replace(parameters=parameters)
70+
obj.__signature__ = sig
71+
except (AttributeError, TypeError):
72+
# failed to update signature (ex. built-in or extension types)
73+
pass
74+
except NotImplementedError as exc: # failed to ast.unparse()
75+
logger.warning(__("Failed to parse a default argument value for %r: %s"), obj, exc)
76+
77+
78+
def setup(app: Sphinx) -> Dict[str, Any]:
79+
app.add_config_value('autodoc_preserve_defaults', False, True)
80+
app.connect('autodoc-before-process-signature', update_defvalue)
81+
82+
return {
83+
'version': '1.0',
84+
'parallel_read_safe': True
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from datetime import datetime
2+
from typing import Any
3+
4+
CONSTANT = 'foo'
5+
SENTINEL = object()
6+
7+
8+
def foo(name: str = CONSTANT,
9+
sentinal: Any = SENTINEL,
10+
now: datetime = datetime.now()) -> None:
11+
"""docstring"""
12+
13+
14+
class Class:
15+
"""docstring"""
16+
17+
def meth(self, name: str = CONSTANT, sentinal: Any = SENTINEL,
18+
now: datetime = datetime.now()) -> None:
19+
"""docstring"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
test_ext_autodoc_preserve_defaults
3+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4+
5+
Test the autodoc extension.
6+
7+
:copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8+
:license: BSD, see LICENSE for details.
9+
"""
10+
11+
import pytest
12+
13+
from .test_ext_autodoc import do_autodoc
14+
15+
16+
@pytest.mark.sphinx('html', testroot='ext-autodoc',
17+
confoverrides={'autodoc_preserve_defaults': True})
18+
def test_preserve_defaults(app):
19+
options = {"members": None}
20+
actual = do_autodoc(app, 'module', 'target.preserve_defaults', options)
21+
assert list(actual) == [
22+
'',
23+
'.. py:module:: target.preserve_defaults',
24+
'',
25+
'',
26+
'.. py:class:: Class',
27+
' :module: target.preserve_defaults',
28+
'',
29+
' docstring',
30+
'',
31+
'',
32+
' .. py:method:: Class.meth(name: str = CONSTANT, sentinal: Any = SENTINEL, '
33+
'now: datetime.datetime = datetime.now()) -> None',
34+
' :module: target.preserve_defaults',
35+
'',
36+
' docstring',
37+
'',
38+
'',
39+
'.. py:function:: foo(name: str = CONSTANT, sentinal: Any = SENTINEL, now: '
40+
'datetime.datetime = datetime.now()) -> None',
41+
' :module: target.preserve_defaults',
42+
'',
43+
' docstring',
44+
'',
45+
]

0 commit comments

Comments
 (0)