Skip to content

Commit a6e9251

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 62dad2f commit a6e9251

File tree

4 files changed

+151
-0
lines changed

4 files changed

+151
-0
lines changed

CHANGES

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ Features added
3535
info-field-list
3636
* #8514: autodoc: Default values of overloaded functions are taken from actual
3737
implementation if they're ellipsis
38+
* #759: autodoc: Add a new extension ``sphinx.ext.autodoc.preserve_defaults``.
39+
It preserves the default argument values of function signatures in source code
40+
and keep them not evaluated for readability. This is an experimental
41+
extension and it will be integrated into autodoc core in Sphinx-4.0
3842
* #8619: html: kbd role generates customizable HTML tags for compound keys
3943
* #8634: html: Allow to change the order of JS/CSS via ``priority`` parameter
4044
for :meth:`Sphinx.add_js_file()` and :meth:`Sphinx.add_css_file()`
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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)))
65+
parameters[i] = param.replace(default=value)
66+
else:
67+
value = DefaultValue(ast_unparse(kw_defaults.pop(0)))
68+
parameters[i] = param.replace(default=value)
69+
sig = sig.replace(parameters=parameters)
70+
obj.__signature__ = sig
71+
except NotImplementedError as exc: # failed to ast.unparse()
72+
logger.warning(__("Failed to parse type_comment for %r: %s"), obj, exc)
73+
74+
75+
def setup(app: Sphinx) -> Dict[str, Any]:
76+
app.setup_extension('sphinx.ext.autodoc')
77+
app.connect('autodoc-before-process-signature', update_defvalue)
78+
79+
return {
80+
'version': '1.0',
81+
'parallel_read_safe': True
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from datetime import datetime
2+
from typing import Any
3+
4+
5+
CONSTANT = 'foo'
6+
SENTINEL = object()
7+
8+
9+
def foo(name: str = CONSTANT,
10+
sentinal: Any = SENTINEL,
11+
now: datetime = datetime.now()) -> None:
12+
"""docstring"""
13+
14+
15+
class Class:
16+
"""docstring"""
17+
18+
def meth(self, name: str = CONSTANT, sentinal: Any = SENTINEL,
19+
now: datetime = datetime.now()) -> None:
20+
"""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={'extensions': ['sphinx.ext.autodoc.preserve_defaults']})
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)