From cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 19:25:40 +0200 Subject: Adding upstream version 7.2.6. Signed-off-by: Daniel Baumann --- sphinx/ext/autodoc/preserve_defaults.py | 199 ++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 sphinx/ext/autodoc/preserve_defaults.py (limited to 'sphinx/ext/autodoc/preserve_defaults.py') 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, + } -- cgit v1.2.3