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
« 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.
4from __future__ import annotations
6import json
7from pathlib import Path, PosixPath, WindowsPath
8from typing import Any, Callable
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)
28_json_encoder_map: dict[type, Callable[[Any], Any]] = {
29 Path: str,
30 PosixPath: str,
31 WindowsPath: str,
32 set: sorted,
33}
36class JSONEncoder(json.JSONEncoder):
37 """JSON encoder.
39 JSON encoders can be used directly, or through
40 the [`json.dump`][] or [`json.dumps`][] methods.
42 Examples:
43 >>> from griffe import JSONEncoder
44 >>> JSONEncoder(full=True).encode(..., **kwargs)
46 >>> import json
47 >>> from griffe import JSONEncoder
48 >>> json.dumps(..., cls=JSONEncoder, full=True, **kwargs)
49 """
51 def __init__(
52 self,
53 *args: Any,
54 full: bool = False,
55 **kwargs: Any,
56 ) -> None:
57 """Initialize the encoder.
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."""
73 def default(self, obj: Any) -> Any:
74 """Return a serializable representation of the given object.
76 Parameters:
77 obj: The object to serialize.
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)
88def _load_docstring(obj_dict: dict) -> Docstring | None:
89 if "docstring" in obj_dict:
90 return Docstring(**obj_dict["docstring"])
91 return None
94def _load_decorators(obj_dict: dict) -> list[Decorator]:
95 return [Decorator(**dec) for dec in obj_dict.get("decorators", [])]
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)
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
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 )
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 )
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
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)
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()
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
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()
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_
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
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
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
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 )
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}
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.
310 The [`json.loads`][] method walks the tree from bottom to top.
312 Examples:
313 >>> import json
314 >>> from griffe import json_decoder
315 >>> json.loads(..., object_hook=json_decoder)
317 Parameters:
318 obj_dict: The dictionary to decode.
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)
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)
339 # Return dict as is.
340 return obj_dict