Coverage for src/pytkdocs/serializer.py: 92.68%
83 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-09 18:24 +0100
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-09 18:24 +0100
1"""This module defines function to serialize objects.
3These functions simply take objects as parameters and return dictionaries that can be dumped by `json.dumps`.
4"""
6import inspect
7import re
8from re import Match, Pattern
9from typing import Any, Optional
11from pytkdocs.objects import Object, Source
12from pytkdocs.parsers.docstrings.base import AnnotatedObject, Attribute, Parameter, Section
14try:
15 from typing import GenericMeta # type: ignore[attr-defined]
16except ImportError:
17 # in 3.7, GenericMeta doesn't exist but we don't need it
18 class GenericMeta(type): # type: ignore[no-redef]
19 """GenericMeta type."""
22RE_OPTIONAL: Pattern = re.compile(r"Union\[(.+), NoneType\]")
23"""Regular expression to match optional annotations of the form `Union[T, NoneType]`."""
25RE_FORWARD_REF: Pattern = re.compile(r"_?ForwardRef\('([^']+)'\)")
26"""Regular expression to match forward-reference annotations of the form `_ForwardRef('T')`."""
29def rebuild_optional(match: Match) -> str:
30 """Rebuild `Union[T, None]` as `Optional[T]`.
32 Arguments:
33 match: The match object when matching against a regular expression (by the parent caller).
35 Returns:
36 The rebuilt type string.
37 """
38 group = match.group(1)
39 brackets_level = 0
40 for char in group: 40 ↛ 47line 40 didn't jump to line 47 because the loop on line 40 didn't complete
41 if char == "," and brackets_level == 0:
42 return f"Union[{group}]"
43 if char == "[":
44 brackets_level += 1
45 elif char == "]":
46 brackets_level -= 1
47 return f"Optional[{group}]"
50def annotation_to_string(annotation: Any) -> str:
51 """Return an annotation as a string.
53 Arguments:
54 annotation: The annotation to return as a string.
56 Returns:
57 The annotation as a string.
58 """
59 if annotation is inspect.Signature.empty:
60 return ""
62 if inspect.isclass(annotation) and not isinstance(annotation, GenericMeta):
63 string = annotation.__name__
64 else:
65 string = str(annotation).replace("typing.", "")
67 string = RE_FORWARD_REF.sub(lambda match: match.group(1), string)
68 return RE_OPTIONAL.sub(rebuild_optional, string)
71def serialize_annotated_object(obj: AnnotatedObject) -> dict:
72 """Serialize an instance of [`AnnotatedObject`][pytkdocs.parsers.docstrings.base.AnnotatedObject].
74 Arguments:
75 obj: The object to serialize.
77 Returns:
78 A JSON-serializable dictionary.
79 """
80 return {"description": obj.description, "annotation": annotation_to_string(obj.annotation)}
83def serialize_attribute(attribute: Attribute) -> dict:
84 """Serialize an instance of [`Attribute`][pytkdocs.parsers.docstrings.base.Attribute].
86 Arguments:
87 attribute: The attribute to serialize.
89 Returns:
90 A JSON-serializable dictionary.
91 """
92 return {
93 "name": attribute.name,
94 "description": attribute.description,
95 "annotation": annotation_to_string(attribute.annotation),
96 }
99def serialize_parameter(parameter: Parameter) -> dict:
100 """Serialize an instance of [`Parameter`][pytkdocs.parsers.docstrings.base.Parameter].
102 Arguments:
103 parameter: The parameter to serialize.
105 Returns:
106 A JSON-serializable dictionary.
107 """
108 serialized = serialize_annotated_object(parameter)
109 serialized.update(
110 {
111 "name": parameter.name,
112 "kind": str(parameter.kind),
113 "default": parameter.default_string,
114 "is_optional": parameter.is_optional,
115 "is_required": parameter.is_required,
116 "is_args": parameter.is_args,
117 "is_kwargs": parameter.is_kwargs,
118 },
119 )
120 return serialized
123def serialize_signature_parameter(parameter: inspect.Parameter) -> dict:
124 """Serialize an instance of `inspect.Parameter`.
126 Arguments:
127 parameter: The parameter to serialize.
129 Returns:
130 A JSON-serializable dictionary.
131 """
132 serialized = {"kind": str(parameter.kind), "name": parameter.name}
133 if parameter.annotation is not parameter.empty:
134 serialized["annotation"] = annotation_to_string(parameter.annotation)
135 if parameter.default is not parameter.empty:
136 serialized["default"] = repr(parameter.default)
137 return serialized
140def serialize_signature(signature: inspect.Signature) -> dict:
141 """Serialize an instance of `inspect.Signature`.
143 Arguments:
144 signature: The signature to serialize.
146 Returns:
147 A JSON-serializable dictionary.
148 """
149 if signature is None: 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true
150 return {}
151 serialized: dict = {
152 "parameters": [serialize_signature_parameter(value) for name, value in signature.parameters.items()],
153 }
154 if signature.return_annotation is not inspect.Signature.empty:
155 serialized["return_annotation"] = annotation_to_string(signature.return_annotation)
156 return serialized
159def serialize_docstring_section(section: Section) -> dict:
160 """Serialize an instance of `inspect.Signature`.
162 Arguments:
163 section: The section to serialize.
165 Returns:
166 A JSON-serializable dictionary.
167 """
168 serialized = {"type": section.type}
169 if section.type == section.Type.MARKDOWN:
170 serialized.update({"value": section.value})
171 elif section.type == section.Type.RETURN or section.type == section.Type.YIELD: # noqa: PLR1714
172 serialized.update({"value": serialize_annotated_object(section.value)}) # type: ignore[dict-item]
173 elif section.type == section.Type.EXCEPTIONS:
174 serialized.update({"value": [serialize_annotated_object(exc) for exc in section.value]}) # type: ignore[dict-item]
175 elif section.type == section.Type.PARAMETERS or section.type == section.Type.KEYWORD_ARGS: # noqa: PLR1714
176 serialized.update({"value": [serialize_parameter(param) for param in section.value]}) # type: ignore[dict-item]
177 elif section.type == section.Type.ATTRIBUTES: 177 ↛ 179line 177 didn't jump to line 179 because the condition on line 177 was always true
178 serialized.update({"value": [serialize_attribute(attr) for attr in section.value]}) # type: ignore[dict-item]
179 elif section.type == section.Type.EXAMPLES:
180 serialized.update({"value": section.value})
181 return serialized
184def serialize_source(source: Optional[Source]) -> dict:
185 """Serialize an instance of [`Source`][pytkdocs.objects.Source].
187 Arguments:
188 source: The source to serialize.
190 Returns:
191 A JSON-serializable dictionary.
192 """
193 if source:
194 return {"code": source.code, "line_start": source.line_start}
195 return {}
198def serialize_object(obj: Object) -> dict:
199 """Serialize an instance of a subclass of [`Object`][pytkdocs.objects.Object].
201 Arguments:
202 obj: The object to serialize.
204 Returns:
205 A JSON-serializable dictionary.
206 """
207 serialized = {
208 "name": obj.name,
209 "path": obj.path,
210 "category": obj.category,
211 "file_path": obj.file_path,
212 "relative_file_path": obj.relative_file_path,
213 "properties": sorted(set(obj.properties + obj.name_properties)),
214 "parent_path": obj.parent_path,
215 "has_contents": obj.has_contents(),
216 "docstring": obj.docstring,
217 "docstring_sections": [serialize_docstring_section(sec) for sec in obj.docstring_sections],
218 "source": serialize_source(obj.source),
219 "children": {child.path: serialize_object(child) for child in obj.children},
220 "attributes": [attr.path for attr in obj.attributes],
221 "methods": [meth.path for meth in obj.methods],
222 "functions": [func.path for func in obj.functions],
223 "modules": [mod.path for mod in obj.modules],
224 "classes": [clas.path for clas in obj.classes],
225 }
226 if hasattr(obj, "type"):
227 serialized["type"] = annotation_to_string(obj.type)
228 if hasattr(obj, "signature"):
229 serialized["signature"] = serialize_signature(obj.signature)
230 if hasattr(obj, "bases"):
231 serialized["bases"] = obj.bases
232 return serialized