Coverage for src/griffe/_internal/encoders.py: 98.53%

140 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-14 23:10 +0200

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 Any, Callable 

9 

10from griffe._internal import expressions 

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

12from griffe._internal.models import ( 

13 Alias, 

14 Attribute, 

15 Class, 

16 Decorator, 

17 Docstring, 

18 Function, 

19 Module, 

20 Object, 

21 Parameter, 

22 Parameters, 

23 TypeAlias, 

24 TypeParameter, 

25 TypeParameters, 

26) 

27 

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

29 Path: str, 

30 PosixPath: str, 

31 WindowsPath: str, 

32 set: sorted, 

33} 

34 

35 

36class JSONEncoder(json.JSONEncoder): 

37 """JSON encoder. 

38 

39 JSON encoders can be used directly, or through 

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

41 

42 Examples: 

43 >>> from griffe import JSONEncoder 

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

45 

46 >>> import json 

47 >>> from griffe import JSONEncoder 

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

49 """ 

50 

51 def __init__( 

52 self, 

53 *args: Any, 

54 full: bool = False, 

55 **kwargs: Any, 

56 ) -> None: 

57 """Initialize the encoder. 

58 

59 Parameters: 

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

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

62 If you plan to reload the data in Python memory 

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

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

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

66 tool instead, dump the full data. 

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

68 """ 

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

70 self.full: bool = full 

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

72 

73 def default(self, obj: Any) -> Any: 

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

75 

76 Parameters: 

77 obj: The object to serialize. 

78 

79 Returns: 

80 A serializable representation. 

81 """ 

82 try: 

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

84 except AttributeError: 

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

86 

87 

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

89 if "docstring" in obj_dict: 

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

91 return None 

92 

93 

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

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

96 

97 

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

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

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

101 expr = cls(**expression) 

102 

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

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

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

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

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

108 if cls is expressions.ExprAttribute: 

109 previous = None 

110 for value in expr.values: 

111 if previous is not None: 

112 value.parent = previous 

113 if isinstance(value, expressions.ExprName): 

114 previous = value 

115 return expr 

116 

117 

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

119 return Parameter( 

120 obj_dict["name"], 

121 annotation=obj_dict["annotation"], 

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

123 default=obj_dict["default"], 

124 docstring=_load_docstring(obj_dict), 

125 ) 

126 

127 

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

129 return TypeParameter( 

130 obj_dict["name"], 

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

132 bound=obj_dict["annotation"], 

133 default=obj_dict["default"], 

134 ) 

135 

136 

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

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

139 return 

140 for elem in expr: 

141 if isinstance(elem, expressions.ExprName): 

142 elem.parent = parent 

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

144 elem.first.parent = parent 

145 

146 

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

148 # Every name and attribute expression must be reattached 

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

150 # to allow resolving names. 

151 if isinstance(obj, Class): 

152 if obj.docstring: 

153 _attach_parent_to_expr(obj.docstring.value, parent) 

154 for decorator in obj.decorators: 

155 _attach_parent_to_expr(decorator.value, parent) 

156 for type_parameter in obj.type_parameters: 

157 _attach_parent_to_expr(type_parameter.annotation, parent) 

158 _attach_parent_to_expr(type_parameter.default, parent) 

159 elif isinstance(obj, Function): 

160 if obj.docstring: 

161 _attach_parent_to_expr(obj.docstring.value, parent) 

162 for decorator in obj.decorators: 

163 _attach_parent_to_expr(decorator.value, parent) 

164 for type_parameter in obj.type_parameters: 

165 _attach_parent_to_expr(type_parameter.annotation, parent) 

166 _attach_parent_to_expr(type_parameter.default, parent) 

167 for param in obj.parameters: 

168 _attach_parent_to_expr(param.annotation, parent) 

169 _attach_parent_to_expr(param.default, parent) 

170 _attach_parent_to_expr(obj.returns, parent) 

171 elif isinstance(obj, Attribute): 

172 if obj.docstring: 

173 _attach_parent_to_expr(obj.docstring.value, parent) 

174 _attach_parent_to_expr(obj.value, parent) 

175 elif isinstance(obj, TypeAlias): 

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

177 _attach_parent_to_expr(obj.docstring.value, parent) 

178 for type_parameter in obj.type_parameters: 

179 _attach_parent_to_expr(type_parameter.annotation, parent) 

180 _attach_parent_to_expr(type_parameter.default, parent) 

181 _attach_parent_to_expr(obj.value, parent) 

182 

183 

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

185 module = Module( 

186 name=obj_dict["name"], 

187 filepath=Path(obj_dict["filepath"]) if "filepath" in obj_dict else None, 

188 docstring=_load_docstring(obj_dict), 

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

190 ) 

191 # YORE: Bump 2: Replace line with `members = obj_dict.get("members", {}).values()`. 

192 members = obj_dict.get("members", []) 

193 # YORE: Bump 2: Remove block. 

194 if isinstance(members, dict): 194 ↛ 197line 194 didn't jump to line 197 because the condition on line 194 was always true

195 members = members.values() 

196 

197 for module_member in members: 

198 module.set_member(module_member.name, module_member) 

199 _attach_parent_to_exprs(module_member, module) 

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

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

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

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

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

205 return module 

206 

207 

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

209 class_ = Class( 

210 name=obj_dict["name"], 

211 lineno=obj_dict["lineno"], 

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

213 docstring=_load_docstring(obj_dict), 

214 decorators=_load_decorators(obj_dict), 

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

216 bases=obj_dict["bases"], 

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

218 ) 

219 # YORE: Bump 2: Replace line with `members = obj_dict.get("members", {}).values()`. 

220 members = obj_dict.get("members", []) 

221 # YORE: Bump 2: Remove block. 

222 if isinstance(members, dict): 

223 members = members.values() 

224 

225 for class_member in members: 

226 class_.set_member(class_member.name, class_member) 

227 _attach_parent_to_exprs(class_member, class_) 

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

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

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

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

232 _attach_parent_to_exprs(class_, class_) 

233 return class_ 

234 

235 

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

237 function = Function( 

238 name=obj_dict["name"], 

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

240 returns=obj_dict["returns"], 

241 decorators=_load_decorators(obj_dict), 

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

243 lineno=obj_dict["lineno"], 

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

245 docstring=_load_docstring(obj_dict), 

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

247 ) 

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

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

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

251 return function 

252 

253 

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

255 attribute = Attribute( 

256 name=obj_dict["name"], 

257 lineno=obj_dict["lineno"], 

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

259 docstring=_load_docstring(obj_dict), 

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

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

262 ) 

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

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

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

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

267 return attribute 

268 

269 

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

271 alias = Alias( 

272 name=obj_dict["name"], 

273 target=obj_dict["target_path"], 

274 lineno=obj_dict["lineno"], 

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

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

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

278 ) 

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

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

281 return alias 

282 

283 

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

285 return TypeAlias( 

286 name=obj_dict["name"], 

287 value=obj_dict["value"], 

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

289 lineno=obj_dict["lineno"], 

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

291 docstring=_load_docstring(obj_dict), 

292 ) 

293 

294 

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

296 Kind.MODULE: _load_module, 

297 Kind.CLASS: _load_class, 

298 Kind.FUNCTION: _load_function, 

299 Kind.ATTRIBUTE: _load_attribute, 

300 Kind.ALIAS: _load_alias, 

301 Kind.TYPE_ALIAS: _load_type_alias, 

302} 

303 

304 

305def json_decoder( 

306 obj_dict: dict[str, Any], 

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

308 """Decode dictionaries as data classes. 

309 

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

311 

312 Examples: 

313 >>> import json 

314 >>> from griffe import json_decoder 

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

316 

317 Parameters: 

318 obj_dict: The dictionary to decode. 

319 

320 Returns: 

321 An instance of a data class. 

322 """ 

323 # Load expressions. 

324 if "cls" in obj_dict: 

325 return _load_expression(obj_dict) 

326 

327 # Load objects and parameters. 

328 if "kind" in obj_dict: 

329 kind = obj_dict["kind"] 

330 if kind in _loader_map: 

331 return _loader_map[kind](obj_dict) 

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

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

334 return _load_parameter(obj_dict) 

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

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

337 return _load_type_parameter(obj_dict) 

338 

339 # Return dict as is. 

340 return obj_dict