|
| 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 | + } |
0 commit comments