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.
 

200 lines
7.0 KiB

"""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,
}