summaryrefslogtreecommitdiffstats
path: root/sphinx/ext/autodoc/preserve_defaults.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/ext/autodoc/preserve_defaults.py')
-rw-r--r--sphinx/ext/autodoc/preserve_defaults.py199
1 files changed, 199 insertions, 0 deletions
diff --git a/sphinx/ext/autodoc/preserve_defaults.py b/sphinx/ext/autodoc/preserve_defaults.py
new file mode 100644
index 0000000..5f957ce
--- /dev/null
+++ b/sphinx/ext/autodoc/preserve_defaults.py
@@ -0,0 +1,199 @@
+"""Preserve function defaults.
+
+Preserve the default argument values of function signatures in source code
+and keep them not evaluated for readability.
+"""
+
+from __future__ import annotations
+
+import ast
+import inspect
+import types
+import warnings
+from typing import TYPE_CHECKING
+
+import sphinx
+from sphinx.deprecation import RemovedInSphinx90Warning
+from sphinx.locale import __
+from sphinx.pycode.ast import unparse as ast_unparse
+from sphinx.util import logging
+
+if TYPE_CHECKING:
+ from typing import Any
+
+ from sphinx.application import Sphinx
+
+logger = logging.getLogger(__name__)
+_LAMBDA_NAME = (lambda: None).__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 | None:
+ """Get FunctionDef object from living object.
+
+ This tries to parse original code for living object and returns
+ AST node for given *obj*.
+ """
+ warnings.warn('sphinx.ext.autodoc.preserve_defaults.get_function_def is'
+ ' deprecated and scheduled for removal in Sphinx 9.'
+ ' Use sphinx.ext.autodoc.preserve_defaults._get_arguments() to'
+ ' extract AST arguments objects from a lambda or regular'
+ ' function.', RemovedInSphinx90Warning, stacklevel=2)
+
+ try:
+ source = inspect.getsource(obj)
+ if source.startswith((' ', '\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[attr-defined]
+ else:
+ module = ast.parse(source)
+ return module.body[0] # type: ignore[return-value]
+ except (OSError, TypeError): # failed to load source code
+ return None
+
+
+def _get_arguments(obj: Any, /) -> ast.arguments | None:
+ """Parse 'ast.arguments' from an object.
+
+ This tries to parse the original code for an object and returns
+ an 'ast.arguments' node.
+ """
+ try:
+ source = inspect.getsource(obj)
+ if source.startswith((' ', '\t')):
+ # 'obj' is in some indented block.
+ module = ast.parse('if True:\n' + source)
+ subject = module.body[0].body[0] # type: ignore[attr-defined]
+ else:
+ module = ast.parse(source)
+ subject = module.body[0]
+ except (OSError, TypeError):
+ # bail; failed to load source for 'obj'.
+ return None
+ except SyntaxError:
+ if _is_lambda(obj):
+ # Most likely a multi-line arising from detecting a lambda, e.g.:
+ #
+ # class Egg:
+ # x = property(
+ # lambda self: 1, doc="...")
+ return None
+
+ # Other syntax errors that are not due to the fact that we are
+ # documenting a lambda function are propagated
+ # (in particular if a lambda is renamed by the user).
+ raise
+
+ return _get_arguments_inner(subject)
+
+
+def _is_lambda(x, /):
+ return isinstance(x, types.LambdaType) and x.__name__ == _LAMBDA_NAME
+
+
+def _get_arguments_inner(x: Any, /) -> ast.arguments | None:
+ if isinstance(x, (ast.AsyncFunctionDef, ast.FunctionDef, ast.Lambda)):
+ return x.args
+ if isinstance(x, (ast.Assign, ast.AnnAssign)):
+ return _get_arguments_inner(x.value)
+ return None
+
+
+def get_default_value(lines: list[str], position: ast.AST) -> str | None:
+ try:
+ if position.lineno == position.end_lineno:
+ line = lines[position.lineno - 1]
+ return line[position.col_offset:position.end_col_offset]
+ else:
+ # multiline value is not supported now
+ return None
+ except (AttributeError, IndexError):
+ return None
+
+
+def update_defvalue(app: Sphinx, obj: Any, bound_method: bool) -> None:
+ """Update defvalue info of *obj* using type_comments."""
+ if not app.config.autodoc_preserve_defaults:
+ return
+
+ try:
+ lines = inspect.getsource(obj).splitlines()
+ if lines[0].startswith((' ', '\t')):
+ # insert a dummy line to follow what _get_arguments() does.
+ lines.insert(0, '')
+ except (OSError, TypeError):
+ lines = []
+
+ try:
+ args = _get_arguments(obj)
+ except SyntaxError:
+ return
+ if args is None:
+ # If the object is a built-in, we won't be always able to recover
+ # the function definition and its arguments. This happens if *obj*
+ # is the `__init__` method generated automatically for dataclasses.
+ return
+
+ if not args.defaults and not args.kw_defaults:
+ return
+
+ try:
+ if bound_method and inspect.ismethod(obj) and hasattr(obj, '__func__'):
+ sig = inspect.signature(obj.__func__)
+ else:
+ sig = inspect.signature(obj)
+ defaults = list(args.defaults)
+ kw_defaults = list(args.kw_defaults)
+ parameters = list(sig.parameters.values())
+ for i, param in enumerate(parameters):
+ if param.default is param.empty:
+ if param.kind == param.KEYWORD_ONLY:
+ # Consume kw_defaults for kwonly args
+ kw_defaults.pop(0)
+ else:
+ if param.kind in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD):
+ default = defaults.pop(0)
+ value = get_default_value(lines, default)
+ if value is None:
+ value = ast_unparse(default)
+ parameters[i] = param.replace(default=DefaultValue(value))
+ else:
+ default = kw_defaults.pop(0) # type: ignore[assignment]
+ value = get_default_value(lines, default)
+ if value is None:
+ value = ast_unparse(default)
+ parameters[i] = param.replace(default=DefaultValue(value))
+
+ sig = sig.replace(parameters=parameters)
+ try:
+ obj.__signature__ = sig
+ except AttributeError:
+ # __signature__ can't be set directly on bound methods.
+ obj.__dict__['__signature__'] = sig
+ except (AttributeError, TypeError):
+ # Failed to update signature (e.g. built-in or extension types).
+ # For user-defined functions, "obj" may not have __dict__,
+ # e.g. when decorated with a class that defines __slots__.
+ # In this case, we can't set __signature__.
+ return
+ except NotImplementedError as exc: # failed to ast_unparse()
+ logger.warning(__("Failed to parse a default argument value for %r: %s"), obj, exc)
+
+
+def setup(app: Sphinx) -> dict[str, Any]:
+ app.add_config_value('autodoc_preserve_defaults', False, True)
+ app.connect('autodoc-before-process-signature', update_defvalue)
+
+ return {
+ 'version': sphinx.__display_version__,
+ 'parallel_read_safe': True,
+ }