Coverage for src/_griffe/encoders.py: 56.49%

106 statements  

« prev     ^ index     » next       coverage.py v7.6.2, created at 2024-10-12 01:34 +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 import expressions 

11from _griffe.enumerations import Kind, ParameterKind 

12from _griffe.models import ( 

13 Alias, 

14 Attribute, 

15 Class, 

16 Decorator, 

17 Docstring, 

18 Function, 

19 Module, 

20 Object, 

21 Parameter, 

22 Parameters, 

23) 

24 

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

26 Path: str, 

27 PosixPath: str, 

28 WindowsPath: str, 

29 set: sorted, 

30} 

31 

32 

33class JSONEncoder(json.JSONEncoder): 

34 """JSON encoder. 

35 

36 JSON encoders can be used directly, or through 

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

38 

39 Examples: 

40 >>> from griffe import JSONEncoder 

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

42 

43 >>> import json 

44 >>> from griffe import JSONEncoder 

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

46 """ 

47 

48 def __init__( 

49 self, 

50 *args: Any, 

51 full: bool = False, 

52 **kwargs: Any, 

53 ) -> None: 

54 """Initialize the encoder. 

55 

56 Parameters: 

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

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

59 If you plan to reload the data in Python memory 

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

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

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

63 tool instead, dump the full data. 

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

65 """ 

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

67 self.full: bool = full 

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

69 

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

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

72 

73 Parameters: 

74 obj: The object to serialize. 

75 

76 Returns: 

77 A serializable representation. 

78 """ 

79 try: 

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

81 except AttributeError: 

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

83 

84 

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

86 if "docstring" in obj_dict: 

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

88 return None 

89 

90 

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

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

93 

94 

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

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

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

98 expr = cls(**expression) 

99 

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

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

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

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

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

105 if cls is expressions.ExprAttribute: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true

106 previous = None 

107 for value in expr.values: 

108 if previous is not None: 

109 value.parent = previous 

110 if isinstance(value, expressions.ExprName): 

111 previous = value 

112 return expr 

113 

114 

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

116 return Parameter( 

117 obj_dict["name"], 

118 annotation=obj_dict["annotation"], 

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

120 default=obj_dict["default"], 

121 docstring=_load_docstring(obj_dict), 

122 ) 

123 

124 

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

126 if not isinstance(expr, expressions.Expr): 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true

127 return 

128 for elem in expr: 

129 if isinstance(elem, expressions.ExprName): 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true

130 elem.parent = parent 

131 elif isinstance(elem, expressions.ExprAttribute) and isinstance(elem.first, expressions.ExprName): 131 ↛ 132line 131 didn't jump to line 132 because the condition on line 131 was never true

132 elem.first.parent = parent 

133 

134 

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

136 # Every name and attribute expression must be reattached 

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

138 # to allow resolving names. 

139 if isinstance(obj, Class): 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true

140 if obj.docstring: 

141 _attach_parent_to_expr(obj.docstring.value, parent) 

142 for decorator in obj.decorators: 

143 _attach_parent_to_expr(decorator.value, parent) 

144 elif isinstance(obj, Function): 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true

145 if obj.docstring: 

146 _attach_parent_to_expr(obj.docstring.value, parent) 

147 for decorator in obj.decorators: 

148 _attach_parent_to_expr(decorator.value, parent) 

149 for param in obj.parameters: 

150 _attach_parent_to_expr(param.annotation, parent) 

151 _attach_parent_to_expr(param.default, parent) 

152 _attach_parent_to_expr(obj.returns, parent) 

153 elif isinstance(obj, Attribute): 

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

155 _attach_parent_to_expr(obj.docstring.value, parent) 

156 _attach_parent_to_expr(obj.value, parent) 

157 

158 

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

160 module = Module(name=obj_dict["name"], filepath=Path(obj_dict["filepath"]), docstring=_load_docstring(obj_dict)) 

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

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

163 # YORE: Bump 2: Remove block. 

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

165 members = members.values() 

166 

167 for module_member in members: 

168 module.set_member(module_member.name, module_member) 

169 _attach_parent_to_exprs(module_member, module) 

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

171 return module 

172 

173 

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

175 class_ = Class( 

176 name=obj_dict["name"], 

177 lineno=obj_dict["lineno"], 

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

179 docstring=_load_docstring(obj_dict), 

180 decorators=_load_decorators(obj_dict), 

181 bases=obj_dict["bases"], 

182 ) 

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

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

185 # YORE: Bump 2: Remove block. 

186 if isinstance(members, dict): 

187 members = members.values() 

188 

189 for class_member in members: 

190 class_.set_member(class_member.name, class_member) 

191 _attach_parent_to_exprs(class_member, class_) 

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

193 _attach_parent_to_exprs(class_, class_) 

194 return class_ 

195 

196 

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

198 function = Function( 

199 name=obj_dict["name"], 

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

201 returns=obj_dict["returns"], 

202 decorators=_load_decorators(obj_dict), 

203 lineno=obj_dict["lineno"], 

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

205 docstring=_load_docstring(obj_dict), 

206 ) 

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

208 return function 

209 

210 

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

212 attribute = Attribute( 

213 name=obj_dict["name"], 

214 lineno=obj_dict["lineno"], 

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

216 docstring=_load_docstring(obj_dict), 

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

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

219 ) 

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

221 return attribute 

222 

223 

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

225 return Alias( 

226 name=obj_dict["name"], 

227 target=obj_dict["target_path"], 

228 lineno=obj_dict["lineno"], 

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

230 ) 

231 

232 

233_loader_map: dict[Kind, Callable[[dict[str, Any]], Module | Class | Function | Attribute | Alias]] = { 

234 Kind.MODULE: _load_module, 

235 Kind.CLASS: _load_class, 

236 Kind.FUNCTION: _load_function, 

237 Kind.ATTRIBUTE: _load_attribute, 

238 Kind.ALIAS: _load_alias, 

239} 

240 

241 

242def json_decoder(obj_dict: dict[str, Any]) -> dict[str, Any] | Object | Alias | Parameter | str | expressions.Expr: 

243 """Decode dictionaries as data classes. 

244 

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

246 

247 Examples: 

248 >>> import json 

249 >>> from griffe import json_decoder 

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

251 

252 Parameters: 

253 obj_dict: The dictionary to decode. 

254 

255 Returns: 

256 An instance of a data class. 

257 """ 

258 # Load expressions. 

259 if "cls" in obj_dict: 

260 return _load_expression(obj_dict) 

261 

262 # Load objects and parameters. 

263 if "kind" in obj_dict: 

264 try: 

265 kind = Kind(obj_dict["kind"]) 

266 except ValueError: 

267 return _load_parameter(obj_dict) 

268 return _loader_map[kind](obj_dict) 

269 

270 # Return dict as is. 

271 return obj_dict