Coverage for src/griffe_typingdoc/_static.py: 91.83%
134 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-18 01:26 +0100
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-18 01:26 +0100
1"""Helpers to get documentation metadata statically."""
3from __future__ import annotations
5import inspect
6from ast import literal_eval
7from collections import defaultdict
8from typing import TYPE_CHECKING, Any
10from griffe import Expr, ExprCall, ExprSubscript, ExprTuple, ParameterKind
12from griffe_typingdoc._docstrings import (
13 _no_self_params,
14 _to_deprecated_section,
15 _to_other_parameters_section,
16 _to_parameters_section,
17 _to_raises_section,
18 _to_receives_section,
19 _to_returns_section,
20 _to_warns_section,
21 _to_yields_section,
22)
24if TYPE_CHECKING:
25 from collections.abc import Sequence
27 from griffe import Function
28 from griffe.dataclasses import Attribute
29 from griffe.docstrings.dataclasses import (
30 DocstringSectionAdmonition,
31 DocstringSectionParameters,
32 DocstringSectionRaises,
33 DocstringSectionReceives,
34 DocstringSectionReturns,
35 DocstringSectionWarns,
36 DocstringSectionYields,
37 )
40def _literal(value: str | Expr) -> str:
41 return inspect.cleandoc(literal_eval(str(value)))
44def _set_metadata_doc(metadata: dict[str, Any], data: ExprCall) -> None:
45 metadata["doc"] = _literal(data.arguments[0])
48def _set_metadata_deprecated(metadata: dict[str, Any], data: ExprCall) -> None:
49 metadata["deprecated"] = _literal(data.arguments[0])
52def _set_metadata_name(metadata: dict[str, Any], data: ExprCall) -> None:
53 metadata["name"] = _literal(data.arguments[0])
56def _set_metadata_raises(metadata: dict[str, Any], data: ExprCall) -> None:
57 metadata["raises"].append((data.arguments[0], _literal(data.arguments[1])))
60def _set_metadata_warns(metadata: dict[str, Any], data: ExprCall) -> None:
61 metadata["warns"].append((data.arguments[0], _literal(data.arguments[1])))
64_set_metadata_map = {
65 "typing.Doc": _set_metadata_doc,
66 "typing_extensions.Doc": _set_metadata_doc,
67 "typing.deprecated": _set_metadata_deprecated,
68 "typing_extensions.deprecated": _set_metadata_deprecated,
69 "typing.Name": _set_metadata_name,
70 "typing_extensions.Name": _set_metadata_name,
71 "typing.Raises": _set_metadata_raises,
72 "typing_extensions.Raises": _set_metadata_raises,
73 "typing.Warns": _set_metadata_warns,
74 "typing_extensions.Warns": _set_metadata_warns,
75}
78def _set_metadata(metadata: dict[str, Any], data: ExprCall) -> None:
79 if data.function.canonical_path in _set_metadata_map: 79 ↛ exitline 79 didn't return from function '_set_metadata' because the condition on line 79 was always true
80 _set_metadata_map[data.function.canonical_path](metadata, data)
83def _metadata(annotation: str | Expr | None) -> dict[str, Any]:
84 metadata: dict[str, Any] = {"raises": [], "warns": []}
85 if isinstance(annotation, ExprSubscript) and annotation.canonical_path in {
86 "typing.Annotated",
87 "typing_extensions.Annotated",
88 }:
89 annotated_data: Sequence[str | Expr]
90 if isinstance(annotation.slice, ExprTuple): 90 ↛ 93line 90 didn't jump to line 93 because the condition on line 90 was always true
91 annotation, *annotated_data = annotation.slice.elements
92 else:
93 annotation = annotation.slice
94 annotated_data = ()
95 for data in annotated_data:
96 if isinstance(data, ExprCall):
97 _set_metadata(metadata, data)
98 return metadata
101def _attribute_docs(attr: Attribute, **kwargs: Any) -> str: # noqa: ARG001
102 return _metadata(attr.annotation).get("doc", "")
105def _parameters_docs(func: Function, **kwargs: Any) -> DocstringSectionParameters | None: # noqa: ARG001
106 params_data: dict[str, dict[str, Any]] = defaultdict(dict)
107 for parameter in _no_self_params(func):
108 stars = {ParameterKind.var_positional: "*", ParameterKind.var_keyword: "**"}.get(parameter.kind, "") # type: ignore[arg-type]
109 param_name = f"{stars}{parameter.name}"
110 metadata = _metadata(parameter.annotation)
111 if "deprecated" in metadata or "doc" in metadata:
112 description = f"{metadata.get('deprecated', '')} {metadata.get('doc', '')}".lstrip()
113 params_data[param_name]["description"] = description
114 params_data[param_name]["annotation"] = parameter.annotation
115 if params_data:
116 return _to_parameters_section(params_data, func)
117 return None
120def _other_parameters_docs(func: Function, **kwargs: Any) -> DocstringSectionParameters | None: # noqa: ARG001
121 for parameter in func.parameters:
122 if parameter.kind is ParameterKind.var_keyword:
123 annotation = parameter.annotation
124 if isinstance(annotation, ExprSubscript) and annotation.canonical_path in {
125 "typing.Annotated",
126 "typing_extensions.Annotated",
127 }:
128 annotation = annotation.slice.elements[0] # type: ignore[union-attr]
129 if isinstance(annotation, ExprSubscript) and annotation.canonical_path in {
130 "typing.Unpack",
131 "typing_extensions.Unpack",
132 }:
133 slice_path = annotation.slice.canonical_path # type: ignore[union-attr]
134 typed_dict = func.modules_collection[slice_path]
135 params_data = {
136 attr.name: {"annotation": attr.annotation, "description": description}
137 for attr in typed_dict.members.values()
138 if (description := _metadata(attr.annotation).get("doc")) is not None
139 }
140 if params_data:
141 return _to_other_parameters_section(params_data)
142 break
143 return None
146def _yields_docs(func: Function, **kwargs: Any) -> DocstringSectionYields | None: # noqa: ARG001
147 yield_annotation = None
148 annotation = func.returns
150 if isinstance(annotation, ExprSubscript):
151 if annotation.canonical_path in {"typing.Generator", "typing_extensions.Generator"}:
152 yield_annotation = annotation.slice.elements[0] # type: ignore[union-attr]
153 elif annotation.canonical_path in {"typing.Iterator", "typing_extensions.Iterator"}:
154 yield_annotation = annotation.slice
156 if yield_annotation:
157 if isinstance(yield_annotation, ExprSubscript) and yield_annotation.is_tuple:
158 yield_elements = yield_annotation.slice.elements # type: ignore[union-attr]
159 else:
160 yield_elements = [yield_annotation]
161 yield_data = [
162 {"annotation": element, **metadata}
163 for element in yield_elements
164 if "doc" in (metadata := _metadata(element))
165 ]
166 if yield_data:
167 return _to_yields_section(yield_data)
169 return None
172def _receives_docs(func: Function, **kwargs: Any) -> DocstringSectionReceives | None: # noqa: ARG001
173 receive_annotation = None
174 annotation = func.returns
176 if isinstance(annotation, ExprSubscript) and annotation.canonical_path in {
177 "typing.Generator",
178 "typing_extensions.Generator",
179 }:
180 receive_annotation = annotation.slice.elements[1] # type: ignore[union-attr]
182 if receive_annotation:
183 if isinstance(receive_annotation, ExprSubscript) and receive_annotation.is_tuple:
184 receive_elements = receive_annotation.slice.elements # type: ignore[union-attr]
185 else:
186 receive_elements = [receive_annotation]
187 receive_data = [
188 {"annotation": element, **metadata}
189 for element in receive_elements
190 if "doc" in (metadata := _metadata(element))
191 ]
192 if receive_data:
193 return _to_receives_section(receive_data)
195 return None
198def _returns_docs(func: Function, **kwargs: Any) -> DocstringSectionReturns | None: # noqa: ARG001
199 return_annotation = None
200 annotation = func.returns
202 if isinstance(annotation, ExprSubscript) and annotation.canonical_path in {
203 "typing.Generator",
204 "typing_extensions.Generator",
205 }:
206 return_annotation = annotation.slice.elements[2] # type: ignore[union-attr]
207 elif isinstance(annotation, ExprSubscript) and annotation.canonical_path in {
208 "typing.Annotated",
209 "typing_extensions.Annotated",
210 }:
211 return_annotation = annotation
213 if return_annotation:
214 if isinstance(return_annotation, ExprSubscript) and return_annotation.is_tuple:
215 return_elements = return_annotation.slice.elements # type: ignore[union-attr]
216 else:
217 return_elements = [return_annotation]
218 return_data = [
219 {"annotation": element, **metadata}
220 for element in return_elements
221 if "doc" in (metadata := _metadata(element))
222 ]
223 if return_data:
224 return _to_returns_section(return_data)
226 return None
229def _warns_docs(attr_or_func: Attribute | Function, **kwargs: Any) -> DocstringSectionWarns | None: # noqa: ARG001
230 if attr_or_func.is_attribute:
231 annotation = attr_or_func.annotation
232 elif attr_or_func.is_function: 232 ↛ 234line 232 didn't jump to line 234 because the condition on line 232 was always true
233 annotation = attr_or_func.returns
234 metadata = _metadata(annotation)
235 if metadata["warns"]: 235 ↛ 236line 235 didn't jump to line 236 because the condition on line 235 was never true
236 return _to_warns_section({"annotation": warned[0], "description": warned[1]} for warned in metadata["warns"])
237 return None
240def _raises_docs(attr_or_func: Attribute | Function, **kwargs: Any) -> DocstringSectionRaises | None: # noqa: ARG001
241 if attr_or_func.is_attribute:
242 annotation = attr_or_func.annotation
243 elif attr_or_func.is_function: 243 ↛ 245line 243 didn't jump to line 245 because the condition on line 243 was always true
244 annotation = attr_or_func.returns
245 metadata = _metadata(annotation)
246 if metadata["raises"]: 246 ↛ 247line 246 didn't jump to line 247 because the condition on line 246 was never true
247 return _to_raises_section({"annotation": raised[0], "description": raised[1]} for raised in metadata["raises"])
248 return None
251def _deprecated_docs(
252 attr_or_func: Attribute | Function,
253 **kwargs: Any, # noqa: ARG001
254) -> DocstringSectionAdmonition | None:
255 if attr_or_func.is_attribute:
256 annotation = attr_or_func.annotation
257 elif attr_or_func.is_function: 257 ↛ 259line 257 didn't jump to line 259 because the condition on line 257 was always true
258 annotation = attr_or_func.returns
259 metadata = _metadata(annotation)
260 if "deprecated" in metadata: 260 ↛ 261line 260 didn't jump to line 261 because the condition on line 260 was never true
261 return _to_deprecated_section({"description": metadata["deprecated"]})
262 return None