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

1"""Helpers to get documentation metadata statically.""" 

2 

3from __future__ import annotations 

4 

5import inspect 

6from ast import literal_eval 

7from collections import defaultdict 

8from typing import TYPE_CHECKING, Any 

9 

10from griffe import Expr, ExprCall, ExprSubscript, ExprTuple, ParameterKind 

11 

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) 

23 

24if TYPE_CHECKING: 

25 from collections.abc import Sequence 

26 

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 ) 

38 

39 

40def _literal(value: str | Expr) -> str: 

41 return inspect.cleandoc(literal_eval(str(value))) 

42 

43 

44def _set_metadata_doc(metadata: dict[str, Any], data: ExprCall) -> None: 

45 metadata["doc"] = _literal(data.arguments[0]) 

46 

47 

48def _set_metadata_deprecated(metadata: dict[str, Any], data: ExprCall) -> None: 

49 metadata["deprecated"] = _literal(data.arguments[0]) 

50 

51 

52def _set_metadata_name(metadata: dict[str, Any], data: ExprCall) -> None: 

53 metadata["name"] = _literal(data.arguments[0]) 

54 

55 

56def _set_metadata_raises(metadata: dict[str, Any], data: ExprCall) -> None: 

57 metadata["raises"].append((data.arguments[0], _literal(data.arguments[1]))) 

58 

59 

60def _set_metadata_warns(metadata: dict[str, Any], data: ExprCall) -> None: 

61 metadata["warns"].append((data.arguments[0], _literal(data.arguments[1]))) 

62 

63 

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} 

76 

77 

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) 

81 

82 

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 

99 

100 

101def _attribute_docs(attr: Attribute, **kwargs: Any) -> str: # noqa: ARG001 

102 return _metadata(attr.annotation).get("doc", "") 

103 

104 

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 

118 

119 

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 

144 

145 

146def _yields_docs(func: Function, **kwargs: Any) -> DocstringSectionYields | None: # noqa: ARG001 

147 yield_annotation = None 

148 annotation = func.returns 

149 

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 

155 

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) 

168 

169 return None 

170 

171 

172def _receives_docs(func: Function, **kwargs: Any) -> DocstringSectionReceives | None: # noqa: ARG001 

173 receive_annotation = None 

174 annotation = func.returns 

175 

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] 

181 

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) 

194 

195 return None 

196 

197 

198def _returns_docs(func: Function, **kwargs: Any) -> DocstringSectionReturns | None: # noqa: ARG001 

199 return_annotation = None 

200 annotation = func.returns 

201 

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 

212 

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) 

225 

226 return None 

227 

228 

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 

238 

239 

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 

249 

250 

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