Skip to content

Commit bd3eecd

Browse files
committed
refactor: Update to support new proposal (https://peps.python.org/pep-0727/)
1 parent 33d242e commit bd3eecd

4 files changed

Lines changed: 62 additions & 75 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/pawamoy/griffe-typingdoc)
77
[![gitter](https://badges.gitter.im/join%20chat.svg)](https://gitter.im/griffe-typingdoc/community)
88

9-
Griffe extension for @tiangolo's `typing.doc` PEP.
9+
Griffe extension for [PEP 727 – Documentation Metadata in Typing](https://peps.python.org/pep-0727/).
1010

1111
## Installation
1212

src/griffe_typingdoc/extension.py

Lines changed: 59 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,72 +2,80 @@
22

33
from __future__ import annotations
44

5-
import ast
6-
import sys
75
from collections import defaultdict
8-
from typing import TYPE_CHECKING, Any
6+
from typing import TYPE_CHECKING, Any, Sequence
97

10-
from griffe import Extension, safe_get_annotation
8+
from griffe import Docstring, Extension, Function, ObjectNode
119
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
2012

2113
if TYPE_CHECKING:
22-
from griffe import Function, ObjectNode
14+
import ast
15+
16+
from typing_extensions import Annotated, doc # type: ignore[attr-defined]
2317

2418

25-
@__typing_doc__(description="Griffe extension parsing the `typing.doc` decorator.")
2619
class TypingDocExtension(Extension):
27-
"""Griffe extension parsing the `typing.doc` decorator."""
20+
"""Griffe extension that reads documentation from `typing.doc`."""
2821

29-
@__typing_doc__(description="Visit a function definition.")
3022
def on_function_instance(
3123
self,
3224
node: Annotated[
3325
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."),
3531
],
36-
func: Annotated[Function, __typing_doc__(description="The Griffe function just instantiated.")],
3732
) -> 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
5965

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)
6169
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]
7277
)
73-
sections.append(DocstringSectionParameters(docstring_params))
78+
for param_name, param_doc in params_doc.items()
79+
],
80+
)
81+
sections.insert(1, param_section)

src/griffe_typingdoc/typing_doc.py

Lines changed: 0 additions & 21 deletions
This file was deleted.

tests/test_extension.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tests for the Griffe extension."""
22

3-
from griffe.agents.extensions import Extensions
43
from griffe.docstrings.dataclasses import DocstringSectionKind
4+
from griffe.extensions import Extensions
55
from griffe.loader import GriffeLoader
66

77
from griffe_typingdoc.extension import TypingDocExtension
@@ -11,6 +11,6 @@ def test_extension() -> None:
1111
"""Load our own package using the extension, assert a parameters section is added to the parsed docstring."""
1212
loader = GriffeLoader(extensions=Extensions(TypingDocExtension()))
1313
typingdoc = loader.load_module("griffe_typingdoc")
14-
sections = typingdoc["extension.TypingDocExtension.visit_functiondef"].docstring.parsed
14+
sections = typingdoc["extension.TypingDocExtension.on_function_instance"].docstring.parsed
1515
assert len(sections) == 2
1616
assert sections[1].kind is DocstringSectionKind.parameters

0 commit comments

Comments
 (0)