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

1"""This module defines function to serialize objects. 

2 

3These functions simply take objects as parameters and return dictionaries that can be dumped by `json.dumps`. 

4""" 

5 

6import inspect 

7import re 

8from re import Match, Pattern 

9from typing import Any, Optional 

10 

11from pytkdocs.objects import Object, Source 

12from pytkdocs.parsers.docstrings.base import AnnotatedObject, Attribute, Parameter, Section 

13 

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.""" 

20 

21 

22RE_OPTIONAL: Pattern = re.compile(r"Union\[(.+), NoneType\]") 

23"""Regular expression to match optional annotations of the form `Union[T, NoneType]`.""" 

24 

25RE_FORWARD_REF: Pattern = re.compile(r"_?ForwardRef\('([^']+)'\)") 

26"""Regular expression to match forward-reference annotations of the form `_ForwardRef('T')`.""" 

27 

28 

29def rebuild_optional(match: Match) -> str: 

30 """Rebuild `Union[T, None]` as `Optional[T]`. 

31 

32 Arguments: 

33 match: The match object when matching against a regular expression (by the parent caller). 

34 

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}]" 

48 

49 

50def annotation_to_string(annotation: Any) -> str: 

51 """Return an annotation as a string. 

52 

53 Arguments: 

54 annotation: The annotation to return as a string. 

55 

56 Returns: 

57 The annotation as a string. 

58 """ 

59 if annotation is inspect.Signature.empty: 

60 return "" 

61 

62 if inspect.isclass(annotation) and not isinstance(annotation, GenericMeta): 

63 string = annotation.__name__ 

64 else: 

65 string = str(annotation).replace("typing.", "") 

66 

67 string = RE_FORWARD_REF.sub(lambda match: match.group(1), string) 

68 return RE_OPTIONAL.sub(rebuild_optional, string) 

69 

70 

71def serialize_annotated_object(obj: AnnotatedObject) -> dict: 

72 """Serialize an instance of [`AnnotatedObject`][pytkdocs.parsers.docstrings.base.AnnotatedObject]. 

73 

74 Arguments: 

75 obj: The object to serialize. 

76 

77 Returns: 

78 A JSON-serializable dictionary. 

79 """ 

80 return {"description": obj.description, "annotation": annotation_to_string(obj.annotation)} 

81 

82 

83def serialize_attribute(attribute: Attribute) -> dict: 

84 """Serialize an instance of [`Attribute`][pytkdocs.parsers.docstrings.base.Attribute]. 

85 

86 Arguments: 

87 attribute: The attribute to serialize. 

88 

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 } 

97 

98 

99def serialize_parameter(parameter: Parameter) -> dict: 

100 """Serialize an instance of [`Parameter`][pytkdocs.parsers.docstrings.base.Parameter]. 

101 

102 Arguments: 

103 parameter: The parameter to serialize. 

104 

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 

121 

122 

123def serialize_signature_parameter(parameter: inspect.Parameter) -> dict: 

124 """Serialize an instance of `inspect.Parameter`. 

125 

126 Arguments: 

127 parameter: The parameter to serialize. 

128 

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 

138 

139 

140def serialize_signature(signature: inspect.Signature) -> dict: 

141 """Serialize an instance of `inspect.Signature`. 

142 

143 Arguments: 

144 signature: The signature to serialize. 

145 

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 

157 

158 

159def serialize_docstring_section(section: Section) -> dict: 

160 """Serialize an instance of `inspect.Signature`. 

161 

162 Arguments: 

163 section: The section to serialize. 

164 

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 

182 

183 

184def serialize_source(source: Optional[Source]) -> dict: 

185 """Serialize an instance of [`Source`][pytkdocs.objects.Source]. 

186 

187 Arguments: 

188 source: The source to serialize. 

189 

190 Returns: 

191 A JSON-serializable dictionary. 

192 """ 

193 if source: 

194 return {"code": source.code, "line_start": source.line_start} 

195 return {} 

196 

197 

198def serialize_object(obj: Object) -> dict: 

199 """Serialize an instance of a subclass of [`Object`][pytkdocs.objects.Object]. 

200 

201 Arguments: 

202 obj: The object to serialize. 

203 

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