This repository provides User Manual for setting up a Docker environment tailored for testing DGTD code.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
7.0 KiB

8 months ago
"""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
from sphinx.util.typing import ExtensionMetadata
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: Any, /) -> bool:
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) -> ExtensionMetadata:
app.add_config_value('autodoc_preserve_defaults', False, 'env')
app.connect('autodoc-before-process-signature', update_defvalue)
return {
'version': sphinx.__display_version__,
'parallel_read_safe': True,
}