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
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
1"""This module defines functions and classes to parse docstrings into structured data."""
2import re
3from typing import List, Optional, Pattern
5from docstring_parser import parse
6from docstring_parser.common import Docstring, DocstringMeta
8from pytkdocs.parsers.docstrings.base import AnnotatedObject, Attribute, Parameter, Parser, Section, empty
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`."""
16class Numpy(Parser):
17 """A Numpy-style docstrings parser."""
19 def __init__(self, trim_doctest_flags: bool = True) -> None:
20 """
21 Initialize the objects.
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 }
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"] = {}
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
58 def read_parameters_section(
59 self,
60 docstring: str,
61 docstring_obj: Docstring,
62 ) -> Optional[Section]:
63 """
64 Parse a "parameters" section.
66 Arguments:
67 docstring: The raw docstring.
68 docstring_obj: Docstring object parsed by docstring_parser.
70 Returns:
71 A `Section` object (or `None` if section is empty).
72 """
73 parameters = []
75 docstring_params = [p for p in docstring_obj.params if p.args[0] == "param"]
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
93 description = param.description or ""
94 if not description:
95 self.error(f"No description for parameter '{name}'")
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 )
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
113 def read_attributes_section(
114 self,
115 docstring: str,
116 docstring_obj: Docstring,
117 ) -> Optional[Section]:
118 """
119 Parse an "attributes" section.
121 Arguments:
122 docstring: The raw docstring.
123 docstring_obj: Docstring object parsed by docstring_parser.
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"]
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 )
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
149 def read_exceptions_section(
150 self,
151 docstring: str,
152 docstring_obj: Docstring,
153 ) -> Optional[Section]:
154 """
155 Parse an "exceptions" section.
157 Arguments:
158 docstring: The raw docstring.
159 docstring_obj: Docstring object parsed by docstring_parser.
161 Returns:
162 A `Section` object (or `None` if section is empty).
163 """
164 exceptions = []
165 except_obj = docstring_obj.raises
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))
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
179 def read_return_section(
180 self,
181 docstring_obj: Docstring,
182 ) -> Optional[Section]:
183 """
184 Parse a "returns" section.
186 Arguments:
187 docstring_obj: Docstring object parsed by docstring_parser.
189 Returns:
190 A `Section` object (or `None` if section is empty).
191 """
192 if docstring_obj.returns:
193 return_obj = docstring_obj.returns
195 if return_obj.description:
196 description = return_obj.description
197 else:
198 self.error("Empty return description")
199 description = ""
201 if self.context["signature"]:
202 annotation = self.context["signature"].return_annotation
203 else:
204 annotation = self.context["annotation"]
206 if annotation is empty and return_obj.type_name:
207 annotation = return_obj.type_name
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 = ""
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))
216 return None
218 def read_examples_section(
219 self,
220 docstring: str,
221 docstring_obj: Docstring,
222 ) -> Optional[Section]:
223 """
224 Parse an "examples" section.
226 Arguments:
227 docstring: The raw docstring.
228 docstring_obj: Docstring object parsed by docstring_parser.
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 )
242 sub_sections = []
243 in_code_example = False
244 in_code_block = False
245 current_text: List[str] = []
246 current_example: List[str] = []
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)
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)
265 elif line.startswith("```"):
266 in_code_block = not in_code_block
267 current_text.append(line)
269 elif in_code_block:
270 current_text.append(line)
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
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)
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)))
289 if sub_sections:
290 return Section(Section.Type.EXAMPLES, sub_sections)
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
297def is_empty_line(line: str) -> bool:
298 """
299 Tell if a line is empty.
301 Arguments:
302 line: The line to check.
304 Returns:
305 True if the line is empty or composed of blanks only, False otherwise.
306 """
307 return not line.strip()
310def none_str_cast(string: Optional[str]):
311 return string or ""