Skip to content

Commit 08be3d0

Browse files
authored
refactor: Run static analysis only after the whole package was loaded
Issue #7: #7 PR #8: #8
1 parent c64bb73 commit 08be3d0

File tree

7 files changed

+368
-101
lines changed

7 files changed

+368
-101
lines changed

docs/examples.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
---
2+
hide:
3+
- navigation
4+
---
5+
16
# Examples
27

38
## Simple
@@ -16,6 +21,11 @@
1621

1722
## Enhanced
1823

24+
> WARNING: **Non-standard features**
25+
The "enhanced" features are not part of PEP 727.
26+
They just serve as an example to show what would be possible
27+
if the PEP was enhanced to account for more use-cases.
28+
1929
/// details | `enhanced` Python module
2030
type: example
2131

docs/examples/simple.py

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,11 @@ def other_parameters(
4141

4242

4343
# Documenting yielded and received values, replacing Yields and Receives sections:
44-
def generator() -> (
45-
Generator[
46-
Annotated[int, Doc("Yielded integers.")],
47-
Annotated[int, Doc("Received integers.")],
48-
Annotated[int, Doc("Final returned value.")],
49-
]
50-
):
44+
def generator() -> Generator[
45+
Annotated[int, Doc("Yielded integers.")],
46+
Annotated[int, Doc("Received integers.")],
47+
Annotated[int, Doc("Final returned value.")],
48+
]:
5149
"""Showing off generators."""
5250

5351

@@ -57,20 +55,18 @@ def iterator() -> Iterator[Annotated[int, Doc("Yielded integers.")]]:
5755

5856

5957
# Advanced use-case: documenting multiple yielded/received/returned values:
60-
def return_tuple() -> (
61-
Generator[
62-
tuple[
63-
Annotated[int, Doc("First element of the yielded value.")],
64-
Annotated[float, Doc("Second element of the yielded value.")],
65-
],
66-
tuple[
67-
Annotated[int, Doc("First element of the received value.")],
68-
Annotated[float, Doc("Second element of the received value.")],
69-
],
70-
tuple[
71-
Annotated[int, Doc("First element of the returned value.")],
72-
Annotated[float, Doc("Second element of the returned value.")],
73-
],
74-
]
75-
):
58+
def return_tuple() -> Generator[
59+
tuple[
60+
Annotated[int, Doc("First element of the yielded value.")],
61+
Annotated[float, Doc("Second element of the yielded value.")],
62+
],
63+
tuple[
64+
Annotated[int, Doc("First element of the received value.")],
65+
Annotated[float, Doc("Second element of the received value.")],
66+
],
67+
tuple[
68+
Annotated[int, Doc("First element of the returned value.")],
69+
Annotated[float, Doc("Second element of the returned value.")],
70+
],
71+
]:
7672
"""Showing off tuples as yield/receive/return values."""

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ classifiers = [
2929
"Typing :: Typed",
3030
]
3131
dependencies = [
32-
"griffe>=0.35",
32+
"griffe>=0.38",
3333
"typing-extensions>=4.7",
3434
]
3535

src/griffe_typingdoc/_dynamic.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@
1010

1111
if TYPE_CHECKING:
1212
from griffe import Attribute, Function, ObjectNode
13-
from griffe.docstrings.dataclasses import DocstringSectionParameters
13+
from griffe.docstrings.dataclasses import (
14+
DocstringSectionAdmonition,
15+
DocstringSectionOtherParameters,
16+
DocstringSectionParameters,
17+
DocstringSectionRaises,
18+
DocstringSectionReceives,
19+
DocstringSectionReturns,
20+
DocstringSectionWarns,
21+
DocstringSectionYields,
22+
)
1423

1524

1625
def _hints(node: ObjectNode) -> dict[str, str]:
@@ -29,15 +38,90 @@ def _doc(name: str, hints: dict[str, Any]) -> str | None:
2938
return None
3039

3140

32-
def _attribute_docs(node: ObjectNode, attr: Attribute) -> str:
41+
def _attribute_docs(attr: Attribute, *, node: ObjectNode, **kwargs: Any) -> str: # noqa: ARG001
3342
return _doc(attr.name, _hints(node)) or ""
3443

3544

36-
def _parameters_docs(node: ObjectNode, func: Function) -> DocstringSectionParameters | None:
45+
def _parameters_docs(
46+
func: Function,
47+
*,
48+
node: ObjectNode,
49+
**kwargs: Any, # noqa: ARG001
50+
) -> DocstringSectionParameters | None:
3751
hints = _hints(node)
3852
params_doc: dict[str, dict[str, Any]] = {
3953
name: {"description": _doc(name, hints)} for name in hints if name != "return"
4054
}
4155
if params_doc:
4256
return _to_parameters_section(params_doc, func)
4357
return None
58+
59+
60+
# FIXME: Implement this function.
61+
def _other_parameters_docs(
62+
func: Function, # noqa: ARG001
63+
*,
64+
node: ObjectNode, # noqa: ARG001
65+
**kwargs: Any, # noqa: ARG001
66+
) -> DocstringSectionOtherParameters | None:
67+
return None
68+
69+
70+
# FIXME: Implement this function.
71+
def _deprecated_docs(
72+
attr_or_func: Attribute | Function, # noqa: ARG001
73+
*,
74+
node: ObjectNode, # noqa: ARG001
75+
**kwargs: Any, # noqa: ARG001
76+
) -> DocstringSectionAdmonition | None:
77+
return None
78+
79+
80+
# FIXME: Implement this function.
81+
def _raises_docs(
82+
attr_or_func: Attribute | Function, # noqa: ARG001
83+
*,
84+
node: ObjectNode, # noqa: ARG001
85+
**kwargs: Any, # noqa: ARG001
86+
) -> DocstringSectionRaises | None:
87+
return None
88+
89+
90+
# FIXME: Implement this function.
91+
def _warns_docs(
92+
attr_or_func: Attribute | Function, # noqa: ARG001
93+
*,
94+
node: ObjectNode, # noqa: ARG001
95+
**kwargs: Any, # noqa: ARG001
96+
) -> DocstringSectionWarns | None:
97+
return None
98+
99+
100+
# FIXME: Implement this function.
101+
def _yields_docs(
102+
func: Function, # noqa: ARG001
103+
*,
104+
node: ObjectNode, # noqa: ARG001
105+
**kwargs: Any, # noqa: ARG001
106+
) -> DocstringSectionYields | None:
107+
return None
108+
109+
110+
# FIXME: Implement this function.
111+
def _receives_docs(
112+
func: Function, # noqa: ARG001
113+
*,
114+
node: ObjectNode, # noqa: ARG001
115+
**kwargs: Any, # noqa: ARG001
116+
) -> DocstringSectionReceives | None:
117+
return None
118+
119+
120+
# FIXME: Implement this function.
121+
def _returns_docs(
122+
func: Function, # noqa: ARG001
123+
*,
124+
node: ObjectNode, # noqa: ARG001
125+
**kwargs: Any, # noqa: ARG001
126+
) -> DocstringSectionReturns | None:
127+
return None

src/griffe_typingdoc/_extension.py

Lines changed: 92 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,28 @@
1111
if TYPE_CHECKING:
1212
import ast
1313

14-
from griffe.dataclasses import Attribute
15-
from typing_extensions import Annotated, Doc # type: ignore[attr-defined]
14+
from griffe.dataclasses import Attribute, Module, Object
15+
from typing_extensions import Annotated, Doc
1616

1717

1818
class TypingDocExtension(Extension):
1919
"""Griffe extension that reads documentation from `typing.Doc`."""
2020

21-
def on_attribute_instance(
22-
self,
23-
*,
24-
node: Annotated[
25-
ast.AST | ObjectNode,
26-
Doc("The object/AST node describing the attribute or its definition."),
27-
],
28-
attr: Annotated[
29-
Attribute,
30-
Doc("The Griffe attribute just instantiated."),
31-
],
32-
) -> None:
33-
"""Post-process Griffe attributes to create their docstring."""
34-
module = _dynamic if isinstance(node, ObjectNode) else _static
21+
def __init__(self) -> None:
22+
self._handled: set[str] = set()
23+
24+
def _handle_attribute(self, attr: Attribute, /, *, node: ObjectNode | None = None) -> None:
25+
if attr.path in self._handled:
26+
return
27+
self._handled.add(attr.path)
28+
29+
module = _dynamic if node else _static
3530

3631
new_sections = (
37-
docstring := module._attribute_docs(node, attr),
38-
deprecated_section := module._deprecated_docs(node, attr),
39-
raises_section := module._raises_docs(node, attr),
40-
warns_section := module._warns_docs(node, attr),
32+
docstring := module._attribute_docs(attr, node=node),
33+
deprecated_section := module._deprecated_docs(attr, node=node),
34+
raises_section := module._raises_docs(attr, node=node),
35+
warns_section := module._warns_docs(attr, node=node),
4136
)
4237

4338
if not any(new_sections):
@@ -48,45 +43,31 @@ def on_attribute_instance(
4843

4944
sections = attr.docstring.parsed
5045

51-
if deprecated_section := module._deprecated_docs(node, attr):
46+
if deprecated_section:
5247
sections.insert(0, deprecated_section)
5348

54-
if raises_section := module._raises_docs(node, attr):
49+
if raises_section:
5550
sections.append(raises_section)
5651

57-
if warns_section := module._warns_docs(node, attr):
52+
if warns_section:
5853
sections.append(warns_section)
5954

60-
def on_function_instance(
61-
self,
62-
*,
63-
node: Annotated[
64-
ast.AST | ObjectNode,
65-
Doc("The object/AST node describing the function or its definition."),
66-
],
67-
func: Annotated[
68-
Function,
69-
Doc(
70-
# Multiline docstring to test de-indentation.
71-
"""
72-
The Griffe function just instantiated.
73-
""",
74-
),
75-
],
76-
) -> None:
77-
"""Post-process Griffe functions to add a parameters section."""
78-
module = _dynamic if isinstance(node, ObjectNode) else _static
55+
def _handle_function(self, func: Function, /, *, node: ObjectNode | None = None) -> None:
56+
if func.path in self._handled:
57+
return
58+
self._handled.add(func.path)
59+
60+
module = _dynamic if node else _static
7961

80-
yields_section, receives_section, returns_section = module._yrr_docs(node, func)
8162
new_sections = (
82-
deprecated_section := module._deprecated_docs(node, func),
83-
params_section := module._parameters_docs(node, func),
84-
other_params_section := module._other_parameters_docs(node, func),
85-
warns_section := module._warns_docs(node, func),
86-
raises_section := module._raises_docs(node, func),
87-
yields_section,
88-
receives_section,
89-
returns_section,
63+
deprecated_section := module._deprecated_docs(func, node=node),
64+
params_section := module._parameters_docs(func, node=node),
65+
other_params_section := module._other_parameters_docs(func, node=node),
66+
warns_section := module._warns_docs(func, node=node),
67+
raises_section := module._raises_docs(func, node=node),
68+
yields_section := module._yields_docs(func, node=node),
69+
receives_section := module._receives_docs(func, node=node),
70+
returns_section := module._returns_docs(func, node=node),
9071
)
9172

9273
if not any(new_sections):
@@ -120,3 +101,63 @@ def on_function_instance(
120101

121102
if returns_section:
122103
sections.append(returns_section)
104+
105+
def _handle_object(self, obj: Object) -> None:
106+
if obj.is_alias:
107+
return
108+
if obj.is_module or obj.is_class:
109+
for member in obj.members.values():
110+
self._handle_object(member) # type: ignore[arg-type]
111+
elif obj.is_function:
112+
self._handle_function(obj) # type: ignore[arg-type]
113+
elif obj.is_attribute:
114+
self._handle_attribute(obj) # type: ignore[arg-type]
115+
116+
def on_package_loaded(
117+
self,
118+
*,
119+
pkg: Annotated[
120+
Module,
121+
Doc("The top-level module representing a package."),
122+
],
123+
) -> None:
124+
"""Post-process Griffe packages recursively (non-yet handled objects only)."""
125+
self._handle_object(pkg)
126+
127+
def on_function_instance(
128+
self,
129+
*,
130+
node: Annotated[
131+
ast.AST | ObjectNode,
132+
Doc("The object/AST node describing the function or its definition."),
133+
],
134+
func: Annotated[
135+
Function,
136+
Doc("""The Griffe function just instantiated."""),
137+
],
138+
) -> None:
139+
"""Post-process Griffe functions to add a parameters section.
140+
141+
It applies only for dynamic analysis.
142+
"""
143+
if isinstance(node, ObjectNode):
144+
self._handle_function(func, node=node)
145+
146+
def on_attribute_instance(
147+
self,
148+
*,
149+
node: Annotated[
150+
ast.AST | ObjectNode,
151+
Doc("The object/AST node describing the attribute or its definition."),
152+
],
153+
attr: Annotated[
154+
Attribute,
155+
Doc("The Griffe attribute just instantiated."),
156+
],
157+
) -> None:
158+
"""Post-process Griffe attributes to create their docstring.
159+
160+
It applies only for dynamic analysis.
161+
"""
162+
if isinstance(node, ObjectNode):
163+
self._handle_attribute(attr, node=node)

0 commit comments

Comments
 (0)