Coverage for src/pytkdocs/serializer.py: 92.52%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

87 statements  

1""" 

2This module defines function to serialize objects. 

3 

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

5""" 

6 

7import inspect 

8import re 

9from typing import Any, Match, Optional, Pattern 

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 

16except ImportError: 

17 # in 3.7, GenericMeta doesn't exist but we don't need it 

18 class GenericMeta(type): # type: ignore # noqa: WPS440 (variable overlap) 

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

31 Rebuild `Union[T, None]` as `Optional[T]`. 

32 

33 Arguments: 

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

35 

36 Returns: 

37 The rebuilt type string. 

38 """ 

39 group = match.group(1) 

40 brackets_level = 0 

41 for char in group: 

42 if char == "," and brackets_level == 0: 

43 return f"Union[{group}]" 

44 if char == "[": 

45 brackets_level += 1 

46 elif char == "]": 

47 brackets_level -= 1 

48 return f"Optional[{group}]" 

49 

50 

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

52 """ 

53 Return an annotation as a string. 

54 

55 Arguments: 

56 annotation: The annotation to return as a string. 

57 

58 Returns: 

59 The annotation as a string. 

60 """ 

61 if annotation is inspect.Signature.empty: 

62 return "" 

63 

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

65 string = annotation.__name__ 

66 else: 

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

68 

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

70 string = RE_OPTIONAL.sub(rebuild_optional, string) 

71 

72 return string # noqa: WPS331 (false-positive, string is not only used for the return) 

73 

74 

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

76 """ 

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

78 

79 Arguments: 

80 obj: The object to serialize. 

81 

82 Returns: 

83 A JSON-serializable dictionary. 

84 """ 

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

86 

87 

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

89 """ 

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

91 

92 Arguments: 

93 attribute: The attribute to serialize. 

94 

95 Returns: 

96 A JSON-serializable dictionary. 

97 """ 

98 return { 

99 "name": attribute.name, 

100 "description": attribute.description, 

101 "annotation": annotation_to_string(attribute.annotation), 

102 } 

103 

104 

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

106 """ 

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

108 

109 Arguments: 

110 parameter: The parameter to serialize. 

111 

112 Returns: 

113 A JSON-serializable dictionary. 

114 """ 

115 serialized = serialize_annotated_object(parameter) 

116 serialized.update( 

117 { 

118 "name": parameter.name, 

119 "kind": str(parameter.kind), 

120 "default": parameter.default_string, 

121 "is_optional": parameter.is_optional, 

122 "is_required": parameter.is_required, 

123 "is_args": parameter.is_args, 

124 "is_kwargs": parameter.is_kwargs, 

125 }, 

126 ) 

127 return serialized 

128 

129 

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

131 """ 

132 Serialize an instance of `inspect.Parameter`. 

133 

134 Arguments: 

135 parameter: The parameter to serialize. 

136 

137 Returns: 

138 A JSON-serializable dictionary. 

139 """ 

140 serialized = {"kind": str(parameter.kind), "name": parameter.name} 

141 if parameter.annotation is not parameter.empty: 

142 serialized["annotation"] = annotation_to_string(parameter.annotation) 

143 if parameter.default is not parameter.empty: 

144 serialized["default"] = repr(parameter.default) 

145 return serialized 

146 

147 

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

149 """ 

150 Serialize an instance of `inspect.Signature`. 

151 

152 Arguments: 

153 signature: The signature to serialize. 

154 

155 Returns: 

156 A JSON-serializable dictionary. 

157 """ 

158 if signature is None: 158 ↛ 159line 158 didn't jump to line 159, because the condition on line 158 was never true

159 return {} 

160 serialized: dict = { 

161 "parameters": [serialize_signature_parameter(value) for name, value in signature.parameters.items()], 

162 } 

163 if signature.return_annotation is not inspect.Signature.empty: 

164 serialized["return_annotation"] = annotation_to_string(signature.return_annotation) 

165 return serialized 

166 

167 

168def serialize_docstring_section(section: Section) -> dict: # noqa: WPS231 (not complex) 

169 """ 

170 Serialize an instance of `inspect.Signature`. 

171 

172 Arguments: 

173 section: The section to serialize. 

174 

175 Returns: 

176 A JSON-serializable dictionary. 

177 """ 

178 serialized = {"type": section.type} 

179 if section.type == section.Type.MARKDOWN: 

180 serialized.update({"value": section.value}) 

181 elif section.type == section.Type.RETURN: 

182 serialized.update({"value": serialize_annotated_object(section.value)}) # type: ignore 

183 elif section.type == section.Type.YIELD: 

184 serialized.update({"value": serialize_annotated_object(section.value)}) # type: ignore 

185 elif section.type == section.Type.EXCEPTIONS: 

186 serialized.update({"value": [serialize_annotated_object(exc) for exc in section.value]}) # type: ignore 

187 elif section.type == section.Type.PARAMETERS: 

188 serialized.update({"value": [serialize_parameter(param) for param in section.value]}) # type: ignore 

189 elif section.type == section.Type.KEYWORD_ARGS: 189 ↛ 190line 189 didn't jump to line 190, because the condition on line 189 was never true

190 serialized.update({"value": [serialize_parameter(param) for param in section.value]}) # type: ignore 

191 elif section.type == section.Type.ATTRIBUTES: 191 ↛ 193line 191 didn't jump to line 193, because the condition on line 191 was never false

192 serialized.update({"value": [serialize_attribute(attr) for attr in section.value]}) # type: ignore 

193 elif section.type == section.Type.EXAMPLES: 

194 serialized.update({"value": section.value}) 

195 return serialized 

196 

197 

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

199 """ 

200 Serialize an instance of [`Source`][pytkdocs.objects.Source]. 

201 

202 Arguments: 

203 source: The source to serialize. 

204 

205 Returns: 

206 A JSON-serializable dictionary. 

207 """ 

208 if source: 

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

210 return {} 

211 

212 

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

214 """ 

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

216 

217 Arguments: 

218 obj: The object to serialize. 

219 

220 Returns: 

221 A JSON-serializable dictionary. 

222 """ 

223 serialized = { 

224 "name": obj.name, 

225 "path": obj.path, 

226 "category": obj.category, 

227 "file_path": obj.file_path, 

228 "relative_file_path": obj.relative_file_path, 

229 "properties": sorted(set(obj.properties + obj.name_properties)), 

230 "parent_path": obj.parent_path, 

231 "has_contents": obj.has_contents(), 

232 "docstring": obj.docstring, 

233 "docstring_sections": [serialize_docstring_section(sec) for sec in obj.docstring_sections], 

234 "source": serialize_source(obj.source), 

235 "children": {child.path: serialize_object(child) for child in obj.children}, 

236 "attributes": [attr.path for attr in obj.attributes], 

237 "methods": [meth.path for meth in obj.methods], 

238 "functions": [func.path for func in obj.functions], 

239 "modules": [mod.path for mod in obj.modules], 

240 "classes": [clas.path for clas in obj.classes], 

241 } 

242 if hasattr(obj, "type"): # noqa: WPS421 (hasattr) 

243 serialized["type"] = annotation_to_string(obj.type) # type: ignore 

244 if hasattr(obj, "signature"): # noqa: WPS421 (hasattr) 

245 serialized["signature"] = serialize_signature(obj.signature) # type: ignore 

246 if hasattr(obj, "bases"): # noqa: WPS421 (hasattr) 

247 serialized["bases"] = obj.bases # type: ignore 

248 return serialized