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
« 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.
4from __future__ import annotations
6import json
7from pathlib import Path, PosixPath, WindowsPath
8from typing import TYPE_CHECKING, Any
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)
29if TYPE_CHECKING:
30 from collections.abc import Callable
32_json_encoder_map: dict[type, Callable[[Any], Any]] = {
33 Path: str,
34 PosixPath: str,
35 WindowsPath: str,
36 set: sorted,
37}
40class JSONEncoder(json.JSONEncoder):
41 """JSON encoder.
43 JSON encoders can be used directly, or through
44 the [`json.dump`][] or [`json.dumps`][] methods.
46 Examples:
47 >>> from griffe import JSONEncoder
48 >>> JSONEncoder(full=True).encode(..., **kwargs)
50 >>> import json
51 >>> from griffe import JSONEncoder
52 >>> json.dumps(..., cls=JSONEncoder, full=True, **kwargs)
53 """
55 def __init__(
56 self,
57 *args: Any,
58 full: bool = False,
59 **kwargs: Any,
60 ) -> None:
61 """Initialize the encoder.
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."""
77 def default(self, obj: Any) -> Any: # ty:ignore[invalid-method-override]
78 """Return a serializable representation of the given object.
80 Parameters:
81 obj: The object to serialize.
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)
92def _load_docstring(obj_dict: dict) -> Docstring | None:
93 if "docstring" in obj_dict:
94 return Docstring(**obj_dict["docstring"])
95 return None
98def _load_decorators(obj_dict: dict) -> list[Decorator]:
99 return [Decorator(**dec) for dec in obj_dict.get("decorators", [])]
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)
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
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 )
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 )
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 )
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
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)
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()
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
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()
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_
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
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
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
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
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}
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.
344 The [`json.loads`][] method walks the tree from bottom to top.
346 Examples:
347 >>> import json
348 >>> from griffe import json_decoder
349 >>> json.loads(..., object_hook=json_decoder)
351 Parameters:
352 obj_dict: The dictionary to decode.
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)
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)
373 # Return dict as is.
374 return obj_dict