|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | | -import ast |
6 | | -import sys |
7 | 5 | from collections import defaultdict |
8 | | -from typing import TYPE_CHECKING, Any |
| 6 | +from typing import TYPE_CHECKING, Any, Sequence |
9 | 7 |
|
10 | | -from griffe import Extension, safe_get_annotation |
| 8 | +from griffe import Docstring, Extension, Function, ObjectNode |
11 | 9 | from griffe.docstrings.dataclasses import DocstringParameter, DocstringSectionParameters |
12 | | - |
13 | | -from griffe_typingdoc.typing_doc import __typing_doc__ |
14 | | - |
15 | | -# TODO: remove once support for Python 3.8 is dropped |
16 | | -if sys.version_info < (3, 9): |
17 | | - from typing_extensions import Annotated |
18 | | -else: |
19 | | - from typing import Annotated |
| 10 | +from griffe.expressions import Expr, ExprCall, ExprSubscript, ExprTuple |
| 11 | +from typing_extensions import get_type_hints |
20 | 12 |
|
21 | 13 | if TYPE_CHECKING: |
22 | | - from griffe import Function, ObjectNode |
| 14 | + import ast |
| 15 | + |
| 16 | + from typing_extensions import Annotated, doc # type: ignore[attr-defined] |
23 | 17 |
|
24 | 18 |
|
25 | | -@__typing_doc__(description="Griffe extension parsing the `typing.doc` decorator.") |
26 | 19 | class TypingDocExtension(Extension): |
27 | | - """Griffe extension parsing the `typing.doc` decorator.""" |
| 20 | + """Griffe extension that reads documentation from `typing.doc`.""" |
28 | 21 |
|
29 | | - @__typing_doc__(description="Visit a function definition.") |
30 | 22 | def on_function_instance( |
31 | 23 | self, |
32 | 24 | node: Annotated[ |
33 | 25 | ast.AST | ObjectNode, |
34 | | - __typing_doc__(description="The object/AST node describing the function or its definition."), |
| 26 | + doc("The object/AST node describing the function or its definition."), |
| 27 | + ], |
| 28 | + func: Annotated[ |
| 29 | + Function, |
| 30 | + doc("The Griffe function just instantiated."), |
35 | 31 | ], |
36 | | - func: Annotated[Function, __typing_doc__(description="The Griffe function just instantiated.")], |
37 | 32 | ) -> None: |
38 | | - """Visit a function definition. |
39 | | -
|
40 | | - This function takes a function definition node and visits its contents, |
41 | | - particularly its decorators, to build up the documentation metadata. |
42 | | - """ |
43 | | - func_doc = {} |
44 | | - for decorator_node in node.decorator_list: |
45 | | - if isinstance(decorator_node, ast.Call) and decorator_node.func.id == "__typing_doc__": # type: ignore[attr-defined] |
46 | | - func_doc.update({kw.arg: kw.value.value for kw in decorator_node.keywords}) # type: ignore[attr-defined] |
47 | | - |
48 | | - params_doc: dict[str, dict[str, Any]] = defaultdict(dict) |
49 | | - for arg in node.args.args: |
50 | | - if isinstance(arg.annotation, ast.Subscript) and arg.annotation.value.id == "Annotated": # type: ignore[attr-defined] |
51 | | - param_name = arg.arg |
52 | | - params_doc[param_name]["annotation"] = safe_get_annotation( |
53 | | - arg.annotation.slice.elts[0], # type: ignore[attr-defined] |
54 | | - func.parent, |
55 | | - ) |
56 | | - doc = arg.annotation.slice.elts[1] # type: ignore[attr-defined] |
57 | | - if isinstance(doc, ast.Call) and doc.func.id == "__typing_doc__": # type: ignore[attr-defined] |
58 | | - params_doc[param_name].update({kw.arg: kw.value.value for kw in doc.keywords}) # type: ignore[attr-defined,misc] |
| 33 | + """Post-process Griffe functions to add a parameters section.""" |
| 34 | + if isinstance(node, ObjectNode): |
| 35 | + hints = get_type_hints(node.obj, include_extras=True) |
| 36 | + params_doc: dict[str, dict[str, Any]] = { |
| 37 | + name: {"description": param.__metadata__[0].documentation} |
| 38 | + for name, param in hints.items() |
| 39 | + if name != "return" |
| 40 | + } |
| 41 | + else: |
| 42 | + params_doc = defaultdict(dict) |
| 43 | + for parameter in func.parameters: |
| 44 | + annotation = parameter.annotation |
| 45 | + if isinstance(annotation, ExprSubscript) and annotation.left.canonical_path in { |
| 46 | + "typing.Annotated", |
| 47 | + "typing_extensions.Annotated", |
| 48 | + }: |
| 49 | + metadata: Sequence[str | Expr] |
| 50 | + if isinstance(annotation.slice, ExprTuple): |
| 51 | + annotation, *metadata = annotation.slice.elements |
| 52 | + else: |
| 53 | + annotation = annotation.slice |
| 54 | + metadata = () |
| 55 | + doc = None |
| 56 | + for data in metadata: |
| 57 | + if isinstance(data, ExprCall) and data.function.canonical_path in { |
| 58 | + "typing.doc", |
| 59 | + "typing_extensions.doc", |
| 60 | + }: |
| 61 | + doc = eval(data.arguments[0]) |
| 62 | + params_doc[parameter.name]["annotation"] = annotation |
| 63 | + if doc: |
| 64 | + params_doc[parameter.name]["description"] = doc |
59 | 65 |
|
60 | | - if (func_doc or params_doc) and func.docstring: |
| 66 | + if params_doc: |
| 67 | + if not func.docstring: |
| 68 | + func.docstring = Docstring("", parent=func) |
61 | 69 | sections = func.docstring.parsed |
62 | | - if params_doc: |
63 | | - docstring_params = [] |
64 | | - for param_name, param_doc in params_doc.items(): |
65 | | - docstring_params.append( |
66 | | - DocstringParameter( |
67 | | - name=param_name, |
68 | | - description=param_doc["description"], |
69 | | - annotation=param_doc["annotation"], |
70 | | - value=func.parameters[param_name].default, # type: ignore[arg-type] |
71 | | - ), |
| 70 | + param_section = DocstringSectionParameters( |
| 71 | + [ |
| 72 | + DocstringParameter( |
| 73 | + name=param_name, |
| 74 | + description=param_doc["description"], |
| 75 | + annotation=param_doc["annotation"], |
| 76 | + value=func.parameters[param_name].default, # type: ignore[arg-type] |
72 | 77 | ) |
73 | | - sections.append(DocstringSectionParameters(docstring_params)) |
| 78 | + for param_name, param_doc in params_doc.items() |
| 79 | + ], |
| 80 | + ) |
| 81 | + sections.insert(1, param_section) |
0 commit comments