Coverage for src/pytkdocs/parsers/docstrings/numpy.py: 88.09%

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

145 statements  

1"""This module defines functions and classes to parse docstrings into structured data.""" 

2import re 

3from typing import List, Optional, Pattern 

4 

5from docstring_parser import parse 

6from docstring_parser.common import Docstring, DocstringMeta 

7 

8from pytkdocs.parsers.docstrings.base import AnnotatedObject, Attribute, Parameter, Parser, Section, empty 

9 

10RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$") 

11"""Regular expression to match lines of the form `<BLANKLINE>`.""" 

12RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$") 

13"""Regular expression to match lines containing doctest flags of the form `# doctest: +FLAG`.""" 

14 

15 

16class Numpy(Parser): 

17 """A Numpy-style docstrings parser.""" 

18 

19 def __init__(self, trim_doctest_flags: bool = True) -> None: 

20 """ 

21 Initialize the objects. 

22 

23 Arguments: 

24 trim_doctest_flags: Whether to remove doctest flags. 

25 """ 

26 super().__init__() 

27 self.trim_doctest_flags = trim_doctest_flags 

28 self.section_reader = { 

29 Section.Type.PARAMETERS: self.read_parameters_section, 

30 Section.Type.EXCEPTIONS: self.read_exceptions_section, 

31 Section.Type.EXAMPLES: self.read_examples_section, 

32 Section.Type.ATTRIBUTES: self.read_attributes_section, 

33 Section.Type.RETURN: self.read_return_section, 

34 } 

35 

36 def parse_sections(self, docstring: str) -> List[Section]: # noqa: D102 

37 if "signature" not in self.context: 37 ↛ 38line 37 didn't jump to line 38, because the condition on line 37 was never true

38 self.context["signature"] = getattr(self.context["obj"], "signature", None) 

39 if "annotation" not in self.context: 39 ↛ 41line 39 didn't jump to line 41, because the condition on line 39 was never false

40 self.context["annotation"] = getattr(self.context["obj"], "type", empty) 

41 if "attributes" not in self.context: 41 ↛ 44line 41 didn't jump to line 44, because the condition on line 41 was never false

42 self.context["attributes"] = {} 

43 

44 docstring_obj = parse(docstring) 

45 description_all = ( 

46 none_str_cast(docstring_obj.short_description) + "\n\n" + none_str_cast(docstring_obj.long_description) 

47 ).strip() 

48 sections = [Section(Section.Type.MARKDOWN, description_all)] if description_all else [] 

49 sections_other = [ 

50 reader(docstring_obj) # type: ignore 

51 if sec == Section.Type.RETURN 

52 else reader(docstring, docstring_obj) # type: ignore 

53 for (sec, reader) in self.section_reader.items() 

54 ] 

55 sections.extend([sec for sec in sections_other if sec]) 

56 return sections 

57 

58 def read_parameters_section( 

59 self, 

60 docstring: str, 

61 docstring_obj: Docstring, 

62 ) -> Optional[Section]: 

63 """ 

64 Parse a "parameters" section. 

65 

66 Arguments: 

67 docstring: The raw docstring. 

68 docstring_obj: Docstring object parsed by docstring_parser. 

69 

70 Returns: 

71 A `Section` object (or `None` if section is empty). 

72 """ 

73 parameters = [] 

74 

75 docstring_params = [p for p in docstring_obj.params if p.args[0] == "param"] 

76 

77 for param in docstring_params: 

78 name = param.arg_name 

79 kind = None 

80 type_name = param.type_name 

81 default = param.default or empty 

82 try: 

83 signature_param = self.context["signature"].parameters[name.lstrip("*")] 

84 except (AttributeError, KeyError): 

85 self.error(f"No type annotation for parameter '{name}'") 

86 else: 

87 if signature_param.annotation is not empty: 

88 type_name = signature_param.annotation 

89 if signature_param.default is not empty: 

90 default = signature_param.default 

91 kind = signature_param.kind 

92 

93 description = param.description or "" 

94 if not description: 

95 self.error(f"No description for parameter '{name}'") 

96 

97 parameters.append( 

98 Parameter( 

99 name=param.arg_name, 

100 annotation=type_name, 

101 description=description, 

102 default=default, 

103 kind=kind, 

104 ) 

105 ) 

106 

107 if parameters: 

108 return Section(Section.Type.PARAMETERS, parameters) 

109 if re.search("Parameters\n", docstring): 109 ↛ 110line 109 didn't jump to line 110, because the condition on line 109 was never true

110 self.error("Empty parameter section") 

111 return None 

112 

113 def read_attributes_section( 

114 self, 

115 docstring: str, 

116 docstring_obj: Docstring, 

117 ) -> Optional[Section]: 

118 """ 

119 Parse an "attributes" section. 

120 

121 Arguments: 

122 docstring: The raw docstring. 

123 docstring_obj: Docstring object parsed by docstring_parser. 

124 

125 Returns: 

126 A `Section` object (or `None` if section is empty). 

127 """ 

128 attributes = [] 

129 docstring_attributes = [p for p in docstring_obj.params if p.args[0] == "attribute"] 

130 

131 for attr in docstring_attributes: 131 ↛ 132line 131 didn't jump to line 132, because the loop on line 131 never started

132 description = attr.description or "" 

133 if not description: 

134 self.error(f"No description for attribute '{attr.arg_name}'") 

135 attributes.append( 

136 Attribute( 

137 name=attr.arg_name, 

138 annotation=attr.type_name, 

139 description=attr.description, 

140 ) 

141 ) 

142 

143 if attributes: 143 ↛ 144line 143 didn't jump to line 144, because the condition on line 143 was never true

144 return Section(Section.Type.ATTRIBUTES, attributes) 

145 if re.search("Attributes\n", docstring): 145 ↛ 146line 145 didn't jump to line 146, because the condition on line 145 was never true

146 self.error("Empty attributes section") 

147 return None 

148 

149 def read_exceptions_section( 

150 self, 

151 docstring: str, 

152 docstring_obj: Docstring, 

153 ) -> Optional[Section]: 

154 """ 

155 Parse an "exceptions" section. 

156 

157 Arguments: 

158 docstring: The raw docstring. 

159 docstring_obj: Docstring object parsed by docstring_parser. 

160 

161 Returns: 

162 A `Section` object (or `None` if section is empty). 

163 """ 

164 exceptions = [] 

165 except_obj = docstring_obj.raises 

166 

167 for exception in except_obj: 

168 description = exception.description or "" 

169 if not description: 

170 self.error(f"No description for exception '{exception.type_name}'") 

171 exceptions.append(AnnotatedObject(exception.type_name, description)) 

172 

173 if exceptions: 

174 return Section(Section.Type.EXCEPTIONS, exceptions) 

175 if re.search("Raises\n", docstring): 175 ↛ 176line 175 didn't jump to line 176, because the condition on line 175 was never true

176 self.error("Empty exceptions section") 

177 return None 

178 

179 def read_return_section( 

180 self, 

181 docstring_obj: Docstring, 

182 ) -> Optional[Section]: 

183 """ 

184 Parse a "returns" section. 

185 

186 Arguments: 

187 docstring_obj: Docstring object parsed by docstring_parser. 

188 

189 Returns: 

190 A `Section` object (or `None` if section is empty). 

191 """ 

192 if docstring_obj.returns: 

193 return_obj = docstring_obj.returns 

194 

195 if return_obj.description: 

196 description = return_obj.description 

197 else: 

198 self.error("Empty return description") 

199 description = "" 

200 

201 if self.context["signature"]: 

202 annotation = self.context["signature"].return_annotation 

203 else: 

204 annotation = self.context["annotation"] 

205 

206 if annotation is empty and return_obj.type_name: 

207 annotation = return_obj.type_name 

208 

209 if not annotation: 209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true

210 self.error("No return type annotation") 

211 annotation = "" 

212 

213 if annotation or description: 213 ↛ 216line 213 didn't jump to line 216, because the condition on line 213 was never false

214 return Section(Section.Type.RETURN, AnnotatedObject(annotation, description)) 

215 

216 return None 

217 

218 def read_examples_section( 

219 self, 

220 docstring: str, 

221 docstring_obj: Docstring, 

222 ) -> Optional[Section]: 

223 """ 

224 Parse an "examples" section. 

225 

226 Arguments: 

227 docstring: The raw docstring. 

228 docstring_obj: Docstring object parsed by docstring_parser. 

229 

230 Returns: 

231 A `Section` object (or `None` if section is empty). 

232 """ 

233 text = next( 

234 ( 

235 meta.description 

236 for meta in docstring_obj.meta 

237 if isinstance(meta, DocstringMeta) and meta.args[0] == "examples" 

238 ), 

239 "", 

240 ) 

241 

242 sub_sections = [] 

243 in_code_example = False 

244 in_code_block = False 

245 current_text: List[str] = [] 

246 current_example: List[str] = [] 

247 

248 if text: 

249 for line in text.split("\n"): 

250 if is_empty_line(line): 

251 if in_code_example: 

252 if current_example: 252 ↛ 255line 252 didn't jump to line 255, because the condition on line 252 was never false

253 sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example))) 

254 current_example = [] 

255 in_code_example = False 

256 else: 

257 current_text.append(line) 

258 

259 elif in_code_example: 

260 if self.trim_doctest_flags: 

261 line = RE_DOCTEST_FLAGS.sub("", line) 

262 line = RE_DOCTEST_BLANKLINE.sub("", line) 

263 current_example.append(line) 

264 

265 elif line.startswith("```"): 

266 in_code_block = not in_code_block 

267 current_text.append(line) 

268 

269 elif in_code_block: 

270 current_text.append(line) 

271 

272 elif line.startswith(">>>"): 

273 if current_text: 

274 sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text))) 

275 current_text = [] 

276 in_code_example = True 

277 

278 if self.trim_doctest_flags: 

279 line = RE_DOCTEST_FLAGS.sub("", line) 

280 current_example.append(line) 

281 else: 

282 current_text.append(line) 

283 

284 if current_text: 284 ↛ 285line 284 didn't jump to line 285, because the condition on line 284 was never true

285 sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text))) 

286 elif current_example: 

287 sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example))) 

288 

289 if sub_sections: 

290 return Section(Section.Type.EXAMPLES, sub_sections) 

291 

292 if re.search("Examples\n", docstring): 292 ↛ 293line 292 didn't jump to line 293, because the condition on line 292 was never true

293 self.error("Empty examples section") 

294 return None 

295 

296 

297def is_empty_line(line: str) -> bool: 

298 """ 

299 Tell if a line is empty. 

300 

301 Arguments: 

302 line: The line to check. 

303 

304 Returns: 

305 True if the line is empty or composed of blanks only, False otherwise. 

306 """ 

307 return not line.strip() 

308 

309 

310def none_str_cast(string: Optional[str]): 

311 return string or ""