Skip to content

Commit 5fccd06

Browse files
committed
feat: Implement extension
1 parent 3b193c8 commit 5fccd06

5 files changed

Lines changed: 128 additions & 3 deletions

File tree

mkdocs.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,19 @@ plugins:
7474
nav_file: SUMMARY.txt
7575
- coverage
7676
- section-index
77-
- mkdocstrings
77+
- mkdocstrings:
78+
handlers:
79+
python:
80+
import:
81+
- https://docs.python.org/3/objects.inv
82+
- https://mkdocstrings.github.io/griffe/objects.inv
83+
options:
84+
separate_signature: true
85+
merge_init_into_class: true
86+
docstring_options:
87+
ignore_init_summary: true
88+
extensions:
89+
- griffe_typingdoc
7890

7991
extra:
8092
social:

src/griffe_typingdoc/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
Griffe extension for @tiangolo's `typing.doc` PEP.
55
"""
66

7-
from typing import List
7+
from __future__ import annotations
88

9-
__all__: List[str] = [] # noqa: WPS410 (the only __variable__ we use)
9+
from griffe_typingdoc.extension import TypingDocExtension as Extension
10+
11+
__all__: list[str] = ["Extension"] # noqa: WPS410

src/griffe_typingdoc/extension.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""This module defines the Griffe TypingDoc extension."""
2+
3+
from __future__ import annotations
4+
5+
import ast
6+
from collections import defaultdict
7+
from typing import Annotated, Any
8+
9+
from griffe.agents.extensions import VisitorExtension, When
10+
from griffe.agents.nodes import safe_get_annotation
11+
from griffe.dataclasses import Function
12+
from griffe.docstrings.dataclasses import DocstringParameter, DocstringSectionParameters
13+
14+
from griffe_typingdoc.typing_doc import __typing_doc__
15+
16+
17+
@__typing_doc__(description="Griffe extension parsing the `typing.doc` decorator.")
18+
class TypingDocExtension(VisitorExtension):
19+
"""Griffe extension parsing the `typing.doc` decorator."""
20+
21+
when = When.after_all
22+
23+
@__typing_doc__(description="Visit a function definition.")
24+
def visit_functiondef(
25+
self,
26+
node: Annotated[
27+
ast.FunctionDef,
28+
__typing_doc__(
29+
description="The AST node describing the function definition.",
30+
),
31+
],
32+
) -> None:
33+
"""Visit a function definition.
34+
35+
This function takes a function definition node and visits its contents,
36+
particularly its decorators, to build up the documentation metadata.
37+
"""
38+
function: Function = self.visitor.current.members[node.name] # type: ignore[assignment]
39+
40+
func_doc = {}
41+
for decorator_node in node.decorator_list:
42+
if isinstance(decorator_node, ast.Call):
43+
if decorator_node.func.id == "__typing_doc__": # type: ignore[attr-defined]
44+
func_doc.update({kw.arg: kw.value.value for kw in decorator_node.keywords}) # type: ignore[attr-defined]
45+
46+
params_doc: dict[str, dict[str, Any]] = defaultdict(dict)
47+
for arg in node.args.args:
48+
if isinstance(arg.annotation, ast.Subscript):
49+
if arg.annotation.value.id == "Annotated": # type: ignore[attr-defined]
50+
param_name = arg.arg
51+
params_doc[param_name]["annotation"] = safe_get_annotation(
52+
arg.annotation.slice.elts[0], # type: ignore[attr-defined]
53+
function.parent, # type: ignore[arg-type]
54+
)
55+
doc = arg.annotation.slice.elts[1] # type: ignore[attr-defined]
56+
if isinstance(doc, ast.Call):
57+
if 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]
59+
60+
if func_doc or params_doc:
61+
if function.docstring:
62+
sections = function.docstring.parsed
63+
if params_doc:
64+
docstring_params = []
65+
for param_name, param_doc in params_doc.items(): # noqa: WPS440
66+
docstring_params.append(
67+
DocstringParameter(
68+
name=param_name,
69+
description=param_doc["description"],
70+
annotation=param_doc["annotation"],
71+
value=function.parameters[param_name].default,
72+
),
73+
)
74+
sections.append(DocstringSectionParameters(docstring_params))

src/griffe_typingdoc/typing_doc.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""This module defines an alternate form of the `typing.doc` function.
2+
3+
See https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md#alternate-form.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from typing import Any, Callable, Mapping, Type, TypeVar
9+
10+
_Type = TypeVar("_Type")
11+
12+
13+
def __typing_doc__( # noqa: N807
14+
*,
15+
description: str | None = None,
16+
deprecated: bool = False,
17+
discouraged: bool = False,
18+
raises: Mapping[Type[BaseException], str | None] | None = None,
19+
extra: dict[Any, Any] | None = None,
20+
) -> Callable[[_Type], _Type]:
21+
return lambda _: _

tests/test_extension.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Tests for the Griffe extension."""
2+
3+
from griffe.agents.extensions import Extensions
4+
from griffe.loader import GriffeLoader
5+
from griffe.docstrings.dataclasses import DocstringSectionKind
6+
7+
from griffe_typingdoc.extension import TypingDocExtension
8+
9+
10+
def test_extension():
11+
"""Load our own package using the extension, assert a parameters section is added to the parsed docstring."""
12+
loader = GriffeLoader(extensions=Extensions(TypingDocExtension()))
13+
typingdoc = loader.load_module("griffe_typingdoc")
14+
sections = typingdoc["extension.TypingDocExtension.visit_functiondef"].docstring.parsed
15+
assert len(sections) == 2
16+
assert sections[1].kind is DocstringSectionKind.parameters

0 commit comments

Comments
 (0)