Coverage for packages / griffelib / src / griffe / _internal / encoders.py: 95.30%

162 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-11 11:48 +0100

1# This module contains data encoders/serializers and decoders/deserializers. 

2# We only support JSON for now, but might want to add more formats in the future. 

3 

4from __future__ import annotations 

5 

6import json 

7from pathlib import Path, PosixPath, WindowsPath 

8from typing import TYPE_CHECKING, Any 

9 

10from griffe._internal import expressions 

11from griffe._internal.enumerations import Kind, ParameterKind, TypeParameterKind 

12from griffe._internal.git import GitInfo 

13from griffe._internal.models import ( 

14 Alias, 

15 Attribute, 

16 Class, 

17 Decorator, 

18 Docstring, 

19 Function, 

20 Module, 

21 Object, 

22 Parameter, 

23 Parameters, 

24 TypeAlias, 

25 TypeParameter, 

26 TypeParameters, 

27) 

28 

29if TYPE_CHECKING: 

30 from collections.abc import Callable 

31 

32_json_encoder_map: dict[type, Callable[[Any], Any]] = { 

33 Path: str, 

34 PosixPath: str, 

35 WindowsPath: str, 

36 set: sorted, 

37} 

38 

39 

40class JSONEncoder(json.JSONEncoder): 

41 """JSON encoder. 

42 

43 JSON encoders can be used directly, or through 

44 the [`json.dump`][] or [`json.dumps`][] methods. 

45 

46 Examples: 

47 >>> from griffe import JSONEncoder 

48 >>> JSONEncoder(full=True).encode(..., **kwargs) 

49 

50 >>> import json 

51 >>> from griffe import JSONEncoder 

52 >>> json.dumps(..., cls=JSONEncoder, full=True, **kwargs) 

53 """ 

54 

55 def __init__( 

56 self, 

57 *args: Any, 

58 full: bool = False, 

59 **kwargs: Any, 

60 ) -> None: 

61 """Initialize the encoder. 

62 

63 Parameters: 

64 *args: See [`json.JSONEncoder`][]. 

65 full: Whether to dump full data or base data. 

66 If you plan to reload the data in Python memory 

67 using the [`json_decoder`][griffe.json_decoder], 

68 you don't need the full data as it can be inferred again 

69 using the base data. If you want to feed a non-Python 

70 tool instead, dump the full data. 

71 **kwargs: See [`json.JSONEncoder`][]. 

72 """ 

73 super().__init__(*args, **kwargs) 

74 self.full: bool = full 

75 """Whether to dump full data or base data.""" 

76 

77 def default(self, obj: Any) -> Any: # ty:ignore[invalid-method-override] 

78 """Return a serializable representation of the given object. 

79 

80 Parameters: 

81 obj: The object to serialize. 

82 

83 Returns: 

84 A serializable representation. 

85 """ 

86 try: 

87 return obj.as_dict(full=self.full) 

88 except AttributeError: 

89 return _json_encoder_map.get(type(obj), super().default)(obj) 

90 

91 

92def _load_docstring(obj_dict: dict) -> Docstring | None: 

93 if "docstring" in obj_dict: 

94 return Docstring(**obj_dict["docstring"]) 

95 return None 

96 

97 

98def _load_decorators(obj_dict: dict) -> list[Decorator]: 

99 return [Decorator(**dec) for dec in obj_dict.get("decorators", [])] 

100 

101 

102def _load_expression(expression: dict) -> expressions.Expr: 

103 # The expression class name is stored in the `cls` key-value. 

104 cls = getattr(expressions, expression.pop("cls")) 

105 expr = cls(**expression) 

106 

107 # For attributes, we need to re-attach names (`values`) together, 

108 # as a single linked list, from right to left: 

109 # in `a.b.c`, `c` links to `b` which links to `a`. 

110 # In `(a or b).c` however, `c` does not link to `(a or b)`, 

111 # as `(a or b)` is not a name and wouldn't allow to resolve `c`. 

112 if cls is expressions.ExprAttribute: 

113 previous = None 

114 for value in expr.values: 

115 if previous is not None: 

116 value.parent = previous 

117 if isinstance(value, expressions.ExprName): 

118 previous = value 

119 return expr 

120 

121 

122def _load_parameter(obj_dict: dict[str, Any]) -> Parameter: 

123 return Parameter( 

124 obj_dict["name"], 

125 annotation=obj_dict["annotation"], 

126 kind=ParameterKind(obj_dict["kind"]), 

127 default=obj_dict["default"], 

128 docstring=_load_docstring(obj_dict), 

129 ) 

130 

131 

132def _load_type_parameter(obj_dict: dict[str, Any]) -> TypeParameter: 

133 return TypeParameter( 

134 obj_dict["name"], 

135 kind=TypeParameterKind(obj_dict["kind"]), 

136 bound=obj_dict["annotation"], 

137 default=obj_dict["default"], 

138 ) 

139 

140 

141def _load_git_info(obj_dict: dict[str, Any]) -> GitInfo: 

142 return GitInfo( 

143 repository=Path(obj_dict["repository"]), 

144 service=obj_dict["service"], 

145 remote_url=obj_dict["remote_url"], 

146 commit_hash=obj_dict["commit_hash"], 

147 ) 

148 

149 

150def _attach_parent_to_expr(expr: expressions.Expr | str | None, parent: Module | Class) -> None: 

151 if not isinstance(expr, expressions.Expr): 

152 return 

153 for elem in expr: 

154 if isinstance(elem, expressions.ExprName): 

155 elem.parent = parent 

156 elif isinstance(elem, expressions.ExprAttribute) and isinstance(elem.first, expressions.ExprName): 

157 elem.first.parent = parent 

158 

159 

160def _attach_parent_to_exprs(obj: Class | Function | Attribute | TypeAlias, parent: Module | Class) -> None: 

161 # Every name and attribute expression must be reattached 

162 # to its parent Griffe object (using its `parent` attribute), 

163 # to allow resolving names. 

164 if isinstance(obj, Class): 

165 if obj.docstring: 

166 _attach_parent_to_expr(obj.docstring.value, parent) 

167 for decorator in obj.decorators: 

168 _attach_parent_to_expr(decorator.value, parent) 

169 for type_parameter in obj.type_parameters: 

170 _attach_parent_to_expr(type_parameter.annotation, parent) 

171 _attach_parent_to_expr(type_parameter.default, parent) 

172 elif isinstance(obj, Function): 

173 if obj.docstring: 

174 _attach_parent_to_expr(obj.docstring.value, parent) 

175 for decorator in obj.decorators: 

176 _attach_parent_to_expr(decorator.value, parent) 

177 for type_parameter in obj.type_parameters: 

178 _attach_parent_to_expr(type_parameter.annotation, parent) 

179 _attach_parent_to_expr(type_parameter.default, parent) 

180 for param in obj.parameters: 

181 _attach_parent_to_expr(param.annotation, parent) 

182 _attach_parent_to_expr(param.default, parent) 

183 _attach_parent_to_expr(obj.returns, parent) 

184 elif isinstance(obj, Attribute): 

185 if obj.docstring: 

186 _attach_parent_to_expr(obj.docstring.value, parent) 

187 _attach_parent_to_expr(obj.value, parent) 

188 elif isinstance(obj, TypeAlias): 

189 if obj.docstring: 189 ↛ 190line 189 didn't jump to line 190 because the condition on line 189 was never true

190 _attach_parent_to_expr(obj.docstring.value, parent) 

191 for type_parameter in obj.type_parameters: 

192 _attach_parent_to_expr(type_parameter.annotation, parent) 

193 _attach_parent_to_expr(type_parameter.default, parent) 

194 _attach_parent_to_expr(obj.value, parent) 

195 

196 

197def _load_module(obj_dict: dict[str, Any]) -> Module: 

198 filepath = obj_dict.get("filepath") 

199 if filepath is not None: 199 ↛ 201line 199 didn't jump to line 201 because the condition on line 199 was always true

200 filepath = [*map(Path, filepath)] if isinstance(filepath, list) else Path(filepath) 

201 module = Module( 

202 name=obj_dict["name"], 

203 filepath=filepath, 

204 docstring=_load_docstring(obj_dict), 

205 runtime=obj_dict.get("runtime", True), 

206 analysis=obj_dict.get("analysis"), 

207 ) 

208 members = obj_dict.get("members", {}).values() 

209 

210 for module_member in members: 

211 module.set_member(module_member.name, module_member) 

212 _attach_parent_to_exprs(module_member, module) 

213 module.labels |= set(obj_dict.get("labels", ())) 

214 module.exports = obj_dict.get("exports") 

215 module.imports = obj_dict.get("imports", {}) 

216 module.deprecated = obj_dict.get("deprecated") 

217 module.public = obj_dict.get("public") 

218 module.source_link = obj_dict.get("source_link") 

219 if git_info := obj_dict.get("git_info"): 

220 module.git_info = _load_git_info(git_info) 

221 return module 

222 

223 

224def _load_class(obj_dict: dict[str, Any]) -> Class: 

225 class_ = Class( 

226 name=obj_dict["name"], 

227 lineno=obj_dict["lineno"], 

228 endlineno=obj_dict.get("endlineno"), 

229 docstring=_load_docstring(obj_dict), 

230 decorators=_load_decorators(obj_dict), 

231 type_parameters=TypeParameters(*obj_dict["type_parameters"]) if "type_parameters" in obj_dict else None, 

232 bases=obj_dict["bases"], 

233 runtime=obj_dict.get("runtime", True), 

234 analysis=obj_dict.get("analysis"), 

235 ) 

236 members = obj_dict.get("members", {}).values() 

237 

238 for class_member in members: 

239 class_.set_member(class_member.name, class_member) 

240 _attach_parent_to_exprs(class_member, class_) 

241 class_.labels |= set(obj_dict.get("labels", ())) 

242 class_.imports = obj_dict.get("imports", {}) 

243 class_.deprecated = obj_dict.get("deprecated") 

244 class_.public = obj_dict.get("public") 

245 class_.source_link = obj_dict.get("source_link") 

246 _attach_parent_to_exprs(class_, class_) 

247 if git_info := obj_dict.get("git_info"): 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true

248 class_.git_info = _load_git_info(git_info) 

249 return class_ 

250 

251 

252def _load_function(obj_dict: dict[str, Any]) -> Function: 

253 function = Function( 

254 name=obj_dict["name"], 

255 parameters=Parameters(*obj_dict["parameters"]), 

256 returns=obj_dict["returns"], 

257 decorators=_load_decorators(obj_dict), 

258 type_parameters=TypeParameters(*obj_dict["type_parameters"]) if "type_parameters" in obj_dict else None, 

259 lineno=obj_dict["lineno"], 

260 endlineno=obj_dict.get("endlineno"), 

261 docstring=_load_docstring(obj_dict), 

262 runtime=obj_dict.get("runtime", True), 

263 analysis=obj_dict.get("analysis"), 

264 ) 

265 function.labels |= set(obj_dict.get("labels", ())) 

266 function.deprecated = obj_dict.get("deprecated") 

267 function.public = obj_dict.get("public") 

268 function.source_link = obj_dict.get("source_link") 

269 if git_info := obj_dict.get("git_info"): 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true

270 function.git_info = _load_git_info(git_info) 

271 return function 

272 

273 

274def _load_attribute(obj_dict: dict[str, Any]) -> Attribute: 

275 attribute = Attribute( 

276 name=obj_dict["name"], 

277 lineno=obj_dict["lineno"], 

278 endlineno=obj_dict.get("endlineno"), 

279 docstring=_load_docstring(obj_dict), 

280 value=obj_dict.get("value"), 

281 annotation=obj_dict.get("annotation"), 

282 analysis=obj_dict.get("analysis"), 

283 ) 

284 attribute.labels |= set(obj_dict.get("labels", ())) 

285 attribute.runtime = obj_dict.get("runtime", True) 

286 attribute.deprecated = obj_dict.get("deprecated") 

287 attribute.public = obj_dict.get("public") 

288 attribute.source_link = obj_dict.get("source_link") 

289 if git_info := obj_dict.get("git_info"): 289 ↛ 290line 289 didn't jump to line 290 because the condition on line 289 was never true

290 attribute.git_info = _load_git_info(git_info) 

291 return attribute 

292 

293 

294def _load_alias(obj_dict: dict[str, Any]) -> Alias: 

295 alias = Alias( 

296 name=obj_dict["name"], 

297 target=obj_dict["target_path"], 

298 lineno=obj_dict["lineno"], 

299 endlineno=obj_dict.get("endlineno"), 

300 runtime=obj_dict.get("runtime", True), 

301 inherited=obj_dict.get("inherited", False), 

302 analysis=obj_dict.get("analysis"), 

303 ) 

304 alias.public = obj_dict.get("public") 

305 alias.deprecated = obj_dict.get("deprecated") 

306 return alias 

307 

308 

309def _load_type_alias(obj_dict: dict[str, Any]) -> TypeAlias: 

310 type_alias = TypeAlias( 

311 name=obj_dict["name"], 

312 value=obj_dict["value"], 

313 type_parameters=TypeParameters(*obj_dict["type_parameters"]), 

314 lineno=obj_dict["lineno"], 

315 endlineno=obj_dict.get("endlineno"), 

316 docstring=_load_docstring(obj_dict), 

317 analysis=obj_dict.get("analysis"), 

318 ) 

319 type_alias.labels |= set(obj_dict.get("labels", ())) 

320 type_alias.runtime = obj_dict.get("runtime", True) 

321 type_alias.deprecated = obj_dict.get("deprecated") 

322 type_alias.public = obj_dict.get("public") 

323 type_alias.source_link = obj_dict.get("source_link") 

324 if git_info := obj_dict.get("git_info"): 324 ↛ 325line 324 didn't jump to line 325 because the condition on line 324 was never true

325 type_alias.git_info = _load_git_info(git_info) 

326 return type_alias 

327 

328 

329_loader_map: dict[Kind, Callable[[dict[str, Any]], Object | Alias]] = { 

330 Kind.MODULE: _load_module, 

331 Kind.CLASS: _load_class, 

332 Kind.FUNCTION: _load_function, 

333 Kind.ATTRIBUTE: _load_attribute, 

334 Kind.ALIAS: _load_alias, 

335 Kind.TYPE_ALIAS: _load_type_alias, 

336} 

337 

338 

339def json_decoder( 

340 obj_dict: dict[str, Any], 

341) -> dict[str, Any] | Object | Alias | Parameter | TypeParameter | str | expressions.Expr: 

342 """Decode dictionaries as data classes. 

343 

344 The [`json.loads`][] method walks the tree from bottom to top. 

345 

346 Examples: 

347 >>> import json 

348 >>> from griffe import json_decoder 

349 >>> json.loads(..., object_hook=json_decoder) 

350 

351 Parameters: 

352 obj_dict: The dictionary to decode. 

353 

354 Returns: 

355 An instance of a data class. 

356 """ 

357 # Load expressions. 

358 if "cls" in obj_dict: 

359 return _load_expression(obj_dict) 

360 

361 # Load objects and parameters. 

362 if "kind" in obj_dict: 

363 kind = obj_dict["kind"] 

364 if kind in _loader_map: 

365 return _loader_map[kind](obj_dict) 

366 # YORE: EOL 3.11: Replace `.__members__.values()` with `` within line. 

367 if kind in ParameterKind.__members__.values(): 

368 return _load_parameter(obj_dict) 

369 # YORE: EOL 3.11: Replace `.__members__.values()` with `` within line. 

370 if kind in TypeParameterKind.__members__.values(): 

371 return _load_type_parameter(obj_dict) 

372 

373 # Return dict as is. 

374 return obj_dict