Coverage for src/pytkdocs/parsers/docstrings/numpy.py: 74.11%
146 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 functions and classes to parse docstrings into structured data."""
3import re
4from re import Pattern
5from typing import Any, Optional
7from docstring_parser import parse
8from docstring_parser.common import Docstring, DocstringMeta
10from pytkdocs.parsers.docstrings.base import AnnotatedObject, Attribute, Parameter, Parser, Section, empty
12RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$")
13"""Regular expression to match lines of the form `<BLANKLINE>`."""
14RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$")
15"""Regular expression to match lines containing doctest flags of the form `# doctest: +FLAG`."""
18class Numpy(Parser):
19 """A Numpy-style docstrings parser."""
21 def __init__(self, trim_doctest_flags: bool = True, **kwargs: Any) -> None: # noqa: FBT001, FBT002, ARG002
22 """Initialize the objects.
24 Arguments:
25 trim_doctest_flags: Whether to remove doctest flags.
26 """
27 super().__init__()
28 self.trim_doctest_flags = trim_doctest_flags
29 self.section_reader = {
30 Section.Type.PARAMETERS: self.read_parameters_section,
31 Section.Type.EXCEPTIONS: self.read_exceptions_section,
32 Section.Type.EXAMPLES: self.read_examples_section,
33 Section.Type.ATTRIBUTES: self.read_attributes_section,
34 Section.Type.RETURN: self.read_return_section,
35 }
37 def parse_sections(self, docstring: str) -> list[Section]: # noqa: D102
38 if "signature" not in self.context: 38 ↛ 39line 38 didn't jump to line 39 because the condition on line 38 was never true
39 self.context["signature"] = getattr(self.context["obj"], "signature", None)
40 if "annotation" not in self.context: 40 ↛ 42line 40 didn't jump to line 42 because the condition on line 40 was always true
41 self.context["annotation"] = getattr(self.context["obj"], "type", empty)
42 if "attributes" not in self.context: 42 ↛ 45line 42 didn't jump to line 45 because the condition on line 42 was always true
43 self.context["attributes"] = {}
45 docstring_obj = parse(docstring)
46 description_all = (
47 none_str_cast(docstring_obj.short_description) + "\n\n" + none_str_cast(docstring_obj.long_description)
48 ).strip()
49 sections = [Section(Section.Type.MARKDOWN, description_all)] if description_all else []
50 sections_other = [
51 reader(docstring_obj) if sec == Section.Type.RETURN else reader(docstring, docstring_obj) # type: ignore[operator]
52 for (sec, reader) in self.section_reader.items()
53 ]
54 sections.extend([sec for sec in sections_other if sec])
55 return sections
57 def read_parameters_section(
58 self,
59 docstring: str,
60 docstring_obj: Docstring,
61 ) -> Optional[Section]:
62 """Parse a "parameters" section.
64 Arguments:
65 docstring: The raw docstring.
66 docstring_obj: Docstring object parsed by docstring_parser.
68 Returns:
69 A `Section` object (or `None` if section is empty).
70 """
71 parameters = []
73 docstring_params = [p for p in docstring_obj.params if p.args[0] == "param"]
75 for param in docstring_params:
76 name = param.arg_name
77 kind = None
78 type_name = param.type_name
79 default = param.default or empty
80 try:
81 signature_param = self.context["signature"].parameters[name.lstrip("*")]
82 except (AttributeError, KeyError):
83 self.error(f"No type annotation for parameter '{name}'")
84 else:
85 if signature_param.annotation is not empty:
86 type_name = signature_param.annotation
87 if signature_param.default is not empty:
88 default = signature_param.default
89 kind = signature_param.kind
91 description = param.description or ""
92 if not description:
93 self.error(f"No description for parameter '{name}'")
95 parameters.append(
96 Parameter(
97 name=param.arg_name,
98 annotation=type_name,
99 description=description,
100 default=default,
101 kind=kind,
102 ),
103 )
105 if parameters:
106 return Section(Section.Type.PARAMETERS, parameters)
107 if re.search("Parameters\n", docstring): 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 self.error("Empty parameter section")
109 return None
111 def read_attributes_section(
112 self,
113 docstring: str,
114 docstring_obj: Docstring,
115 ) -> Optional[Section]:
116 """Parse an "attributes" section.
118 Arguments:
119 docstring: The raw docstring.
120 docstring_obj: Docstring object parsed by docstring_parser.
122 Returns:
123 A `Section` object (or `None` if section is empty).
124 """
125 attributes = []
126 docstring_attributes = [p for p in docstring_obj.params if p.args[0] == "attribute"]
128 for attr in docstring_attributes: 128 ↛ 129line 128 didn't jump to line 129 because the loop on line 128 never started
129 description = attr.description or ""
130 if not description:
131 self.error(f"No description for attribute '{attr.arg_name}'")
132 attributes.append(
133 Attribute(
134 name=attr.arg_name,
135 annotation=attr.type_name,
136 description=description,
137 ),
138 )
140 if attributes: 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true
141 return Section(Section.Type.ATTRIBUTES, attributes)
142 if re.search("Attributes\n", docstring): 142 ↛ 143line 142 didn't jump to line 143 because the condition on line 142 was never true
143 self.error("Empty attributes section")
144 return None
146 def read_exceptions_section(
147 self,
148 docstring: str,
149 docstring_obj: Docstring,
150 ) -> Optional[Section]:
151 """Parse an "exceptions" section.
153 Arguments:
154 docstring: The raw docstring.
155 docstring_obj: Docstring object parsed by docstring_parser.
157 Returns:
158 A `Section` object (or `None` if section is empty).
159 """
160 exceptions = []
161 except_obj = docstring_obj.raises
163 for exception in except_obj:
164 description = exception.description or ""
165 if not description:
166 self.error(f"No description for exception '{exception.type_name}'")
167 exceptions.append(AnnotatedObject(exception.type_name, description))
169 if exceptions:
170 return Section(Section.Type.EXCEPTIONS, exceptions)
171 if re.search("Raises\n", docstring): 171 ↛ 172line 171 didn't jump to line 172 because the condition on line 171 was never true
172 self.error("Empty exceptions section")
173 return None
175 def read_return_section(
176 self,
177 docstring_obj: Docstring,
178 ) -> Optional[Section]:
179 """Parse a "returns" section.
181 Arguments:
182 docstring_obj: Docstring object parsed by docstring_parser.
184 Returns:
185 A `Section` object (or `None` if section is empty).
186 """
187 if docstring_obj.returns:
188 return_obj = docstring_obj.returns
190 if return_obj.description:
191 description = return_obj.description
192 else:
193 self.error("Empty return description")
194 description = ""
196 if self.context["signature"]:
197 annotation = self.context["signature"].return_annotation
198 else:
199 annotation = self.context["annotation"]
201 if annotation is empty and return_obj.type_name:
202 annotation = return_obj.type_name
204 if not annotation: 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true
205 self.error("No return type annotation")
206 annotation = ""
208 if annotation or description: 208 ↛ 211line 208 didn't jump to line 211 because the condition on line 208 was always true
209 return Section(Section.Type.RETURN, AnnotatedObject(annotation, description))
211 return None
213 def read_examples_section(
214 self,
215 docstring: str,
216 docstring_obj: Docstring,
217 ) -> Optional[Section]:
218 """Parse an "examples" section.
220 Arguments:
221 docstring: The raw docstring.
222 docstring_obj: Docstring object parsed by docstring_parser.
224 Returns:
225 A `Section` object (or `None` if section is empty).
226 """
227 text = next(
228 (
229 meta.description
230 for meta in docstring_obj.meta
231 if isinstance(meta, DocstringMeta) and meta.args[0] == "examples"
232 ),
233 "",
234 )
236 sub_sections = []
237 in_code_example = False
238 in_code_block = False
239 current_text: list[str] = []
240 current_example: list[str] = []
242 if text:
243 for line in text.split("\n"):
244 if is_empty_line(line):
245 if in_code_example: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 if current_example:
247 sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
248 current_example = []
249 in_code_example = False
250 else:
251 current_text.append(line)
253 elif in_code_example: 253 ↛ 254line 253 didn't jump to line 254 because the condition on line 253 was never true
254 if self.trim_doctest_flags:
255 line = RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
256 line = RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901
257 current_example.append(line)
259 elif line.startswith("```"): 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true
260 in_code_block = not in_code_block
261 current_text.append(line)
263 elif in_code_block: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true
264 current_text.append(line)
266 elif line.startswith(">>>"): 266 ↛ 267line 266 didn't jump to line 267 because the condition on line 266 was never true
267 if current_text:
268 sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
269 current_text = []
270 in_code_example = True
272 if self.trim_doctest_flags:
273 line = RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
274 current_example.append(line)
275 else:
276 current_text.append(line)
278 if current_text:
279 sub_sections.append((Section.Type.MARKDOWN, "\n".join(current_text)))
280 elif current_example: 280 ↛ 281line 280 didn't jump to line 281 because the condition on line 280 was never true
281 sub_sections.append((Section.Type.EXAMPLES, "\n".join(current_example)))
283 if sub_sections:
284 return Section(Section.Type.EXAMPLES, sub_sections)
286 if re.search("Examples\n", docstring): 286 ↛ 287line 286 didn't jump to line 287 because the condition on line 286 was never true
287 self.error("Empty examples section")
288 return None
291def is_empty_line(line: str) -> bool:
292 """Tell if a line is empty.
294 Arguments:
295 line: The line to check.
297 Returns:
298 True if the line is empty or composed of blanks only, False otherwise.
299 """
300 return not line.strip()
303def none_str_cast(string: Optional[str]) -> str: # noqa: D103
304 return string or ""