Skip to content

Commit c85d269

Browse files
authored
Merge pull request #8771 from tk0miya/759_preserve_defaults
Fix #759: autodoc: Add sphinx.ext.autodoc.preserve_defaults extension
2 parents 647510e + 1ea11b1 commit c85d269

File tree

6 files changed

+166
-0
lines changed

6 files changed

+166
-0
lines changed

CHANGES

+3
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ Features added
192192
* #8775: autodoc: Support type union operator (PEP-604) in Python 3.10 or above
193193
* #8297: autodoc: Allow to extend :confval:`autodoc_default_options` via
194194
directive options
195+
* #759: autodoc: Add a new configuration :confval:`autodoc_preserve_defaults` as
196+
an experimental feature. It preserves the default argument values of
197+
functions in source code and keep them not evaluated for readability.
195198
* #8619: html: kbd role generates customizable HTML tags for compound keys
196199
* #8634: html: Allow to change the order of JS/CSS via ``priority`` parameter
197200
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

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
if not app.config.autodoc_preserve_defaults:
55+
return
56+
57+
try:
58+
function = get_function_def(obj)
59+
if function.args.defaults or function.args.kw_defaults:
60+
sig = inspect.signature(obj)
61+
defaults = list(function.args.defaults)
62+
kw_defaults = list(function.args.kw_defaults)
63+
parameters = list(sig.parameters.values())
64+
for i, param in enumerate(parameters):
65+
if param.default is not param.empty:
66+
if param.kind in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD):
67+
value = DefaultValue(ast_unparse(defaults.pop(0))) # type: ignore
68+
parameters[i] = param.replace(default=value)
69+
else:
70+
value = DefaultValue(ast_unparse(kw_defaults.pop(0))) # type: ignore
71+
parameters[i] = param.replace(default=value)
72+
sig = sig.replace(parameters=parameters)
73+
obj.__signature__ = sig
74+
except (AttributeError, TypeError):
75+
# failed to update signature (ex. built-in or extension types)
76+
pass
77+
except NotImplementedError as exc: # failed to ast.unparse()
78+
logger.warning(__("Failed to parse a default argument value for %r: %s"), obj, exc)
79+
80+
81+
def setup(app: Sphinx) -> Dict[str, Any]:
82+
app.add_config_value('autodoc_preserve_defaults', False, True)
83+
app.connect('autodoc-before-process-signature', update_defvalue)
84+
85+
return {
86+
'version': '1.0',
87+
'parallel_read_safe': True
88+
}
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)