diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:25:40 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:25:40 +0000 |
commit | cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a (patch) | |
tree | 18dcde1a8d1f5570a77cd0c361de3b490d02c789 /sphinx/ext/autodoc/type_comment.py | |
parent | Initial commit. (diff) | |
download | sphinx-cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a.tar.xz sphinx-cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a.zip |
Adding upstream version 7.2.6.upstream/7.2.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sphinx/ext/autodoc/type_comment.py')
-rw-r--r-- | sphinx/ext/autodoc/type_comment.py | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py new file mode 100644 index 0000000..e2c9ae2 --- /dev/null +++ b/sphinx/ext/autodoc/type_comment.py @@ -0,0 +1,140 @@ +"""Update annotations info of living objects using type_comments.""" + +from __future__ import annotations + +import ast +from inspect import Parameter, Signature, getsource +from typing import TYPE_CHECKING, Any, cast + +import sphinx +from sphinx.locale import __ +from sphinx.pycode.ast import unparse as ast_unparse +from sphinx.util import inspect, logging + +if TYPE_CHECKING: + from collections.abc import Sequence + + from sphinx.application import Sphinx + +logger = logging.getLogger(__name__) + + +def not_suppressed(argtypes: Sequence[ast.expr] = ()) -> bool: + """Check given *argtypes* is suppressed type_comment or not.""" + if len(argtypes) == 0: # no argtypees + return False + if len(argtypes) == 1: + arg = argtypes[0] + if isinstance(arg, ast.Constant) and arg.value is ...: # suppressed + return False + # not suppressed + return True + + +def signature_from_ast(node: ast.FunctionDef, bound_method: bool, + type_comment: ast.FunctionDef) -> Signature: + """Return a Signature object for the given *node*. + + :param bound_method: Specify *node* is a bound method or not + """ + params = [] + for arg in node.args.posonlyargs: + param = Parameter(arg.arg, Parameter.POSITIONAL_ONLY, annotation=arg.type_comment) + params.append(param) + + for arg in node.args.args: + param = Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD, + annotation=arg.type_comment or Parameter.empty) + params.append(param) + + if node.args.vararg: + param = Parameter(node.args.vararg.arg, Parameter.VAR_POSITIONAL, + annotation=node.args.vararg.type_comment or Parameter.empty) + params.append(param) + + for arg in node.args.kwonlyargs: + param = Parameter(arg.arg, Parameter.KEYWORD_ONLY, + annotation=arg.type_comment or Parameter.empty) + params.append(param) + + if node.args.kwarg: + param = Parameter(node.args.kwarg.arg, Parameter.VAR_KEYWORD, + annotation=node.args.kwarg.type_comment or Parameter.empty) + params.append(param) + + # Remove first parameter when *obj* is bound_method + if bound_method and params: + params.pop(0) + + # merge type_comment into signature + if not_suppressed(type_comment.argtypes): # type: ignore[attr-defined] + for i, param in enumerate(params): + params[i] = param.replace( + annotation=type_comment.argtypes[i]) # type: ignore[attr-defined] + + if node.returns: + return Signature(params, return_annotation=node.returns) + elif type_comment.returns: + return Signature(params, return_annotation=ast_unparse(type_comment.returns)) + else: + return Signature(params) + + +def get_type_comment(obj: Any, bound_method: bool = False) -> Signature | None: + """Get type_comment'ed FunctionDef object from living object. + + This tries to parse original code for living object and returns + Signature for given *obj*. + """ + try: + source = getsource(obj) + if source.startswith((' ', r'\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, type_comments=True) + subject = cast( + ast.FunctionDef, module.body[0].body[0], # type: ignore[attr-defined] + ) + else: + module = ast.parse(source, type_comments=True) + subject = cast(ast.FunctionDef, module.body[0]) + + type_comment = getattr(subject, "type_comment", None) + if type_comment: + function = ast.parse(type_comment, mode='func_type', type_comments=True) + return signature_from_ast( + subject, bound_method, function, # type: ignore[arg-type] + ) + else: + return None + except (OSError, TypeError): # failed to load source code + return None + except SyntaxError: # failed to parse type_comments + return None + + +def update_annotations_using_type_comments(app: Sphinx, obj: Any, bound_method: bool) -> None: + """Update annotations info of *obj* using type_comments.""" + try: + type_sig = get_type_comment(obj, bound_method) + if type_sig: + sig = inspect.signature(obj, bound_method) + for param in sig.parameters.values(): + if param.name not in obj.__annotations__: + annotation = type_sig.parameters[param.name].annotation + if annotation is not Parameter.empty: + obj.__annotations__[param.name] = ast_unparse(annotation) + + if 'return' not in obj.__annotations__: + obj.__annotations__['return'] = type_sig.return_annotation + except KeyError as exc: + logger.warning(__("Failed to update signature for %r: parameter not found: %s"), + obj, exc) + except NotImplementedError as exc: # failed to ast.unparse() + logger.warning(__("Failed to parse type_comment for %r: %s"), obj, exc) + + +def setup(app: Sphinx) -> dict[str, Any]: + app.connect('autodoc-before-process-signature', update_annotations_using_type_comments) + + return {'version': sphinx.__display_version__, 'parallel_read_safe': True} |