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
« 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.
4from __future__ import annotations
6import json
7from pathlib import Path, PosixPath, WindowsPath
8from typing import Any, Callable
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)
25_json_encoder_map: dict[type, Callable[[Any], Any]] = {
26 Path: str,
27 PosixPath: str,
28 WindowsPath: str,
29 set: sorted,
30}
33class JSONEncoder(json.JSONEncoder):
34 """JSON encoder.
36 JSON encoders can be used directly, or through
37 the [`json.dump`][] or [`json.dumps`][] methods.
39 Examples:
40 >>> from griffe import JSONEncoder
41 >>> JSONEncoder(full=True).encode(..., **kwargs)
43 >>> import json
44 >>> from griffe import JSONEncoder
45 >>> json.dumps(..., cls=JSONEncoder, full=True, **kwargs)
46 """
48 def __init__(
49 self,
50 *args: Any,
51 full: bool = False,
52 **kwargs: Any,
53 ) -> None:
54 """Initialize the encoder.
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."""
70 def default(self, obj: Any) -> Any:
71 """Return a serializable representation of the given object.
73 Parameters:
74 obj: The object to serialize.
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)
85def _load_docstring(obj_dict: dict) -> Docstring | None:
86 if "docstring" in obj_dict:
87 return Docstring(**obj_dict["docstring"])
88 return None
91def _load_decorators(obj_dict: dict) -> list[Decorator]:
92 return [Decorator(**dec) for dec in obj_dict.get("decorators", [])]
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)
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
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 )
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
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)
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()
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
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()
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_
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
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
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 )
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}
242def json_decoder(obj_dict: dict[str, Any]) -> dict[str, Any] | Object | Alias | Parameter | str | expressions.Expr:
243 """Decode dictionaries as data classes.
245 The [`json.loads`][] method walks the tree from bottom to top.
247 Examples:
248 >>> import json
249 >>> from griffe import json_decoder
250 >>> json.loads(..., object_hook=json_decoder)
252 Parameters:
253 obj_dict: The dictionary to decode.
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)
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)
270 # Return dict as is.
271 return obj_dict