Coverage for src/_griffe/docstrings/numpy.py: 85.23%
423 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:47 +0200
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:47 +0200
1# This module defines functions to parse Numpy-style docstrings into structured data.
2#
3# Based on https://numpydoc.readthedocs.io/en/latest/format.html,
4# it seems Numpydoc is a superset of RST.
5# Since fully parsing RST is a non-goal of this project,
6# some things are stripped from the Numpydoc specification.
7#
8# Rejected as non particularly Pythonic or useful as sections:
9#
10# - See also: this section feels too subjective (specially crafted as a standard for Numpy itself),
11# and there are may ways to reference related items in a docstring, depending on the chosen markup.
12#
13# Rejected as naturally handled by the user-chosen markup:
14#
15# - Warnings: this is just markup.
16# - Notes: again, just markup.
17# - References: again, just markup.
20from __future__ import annotations
22import re
23from contextlib import suppress
24from textwrap import dedent
25from typing import TYPE_CHECKING
27from _griffe.docstrings.models import (
28 DocstringAttribute,
29 DocstringClass,
30 DocstringFunction,
31 DocstringModule,
32 DocstringParameter,
33 DocstringRaise,
34 DocstringReceive,
35 DocstringReturn,
36 DocstringSection,
37 DocstringSectionAdmonition,
38 DocstringSectionAttributes,
39 DocstringSectionClasses,
40 DocstringSectionDeprecated,
41 DocstringSectionExamples,
42 DocstringSectionFunctions,
43 DocstringSectionModules,
44 DocstringSectionOtherParameters,
45 DocstringSectionParameters,
46 DocstringSectionRaises,
47 DocstringSectionReceives,
48 DocstringSectionReturns,
49 DocstringSectionText,
50 DocstringSectionWarns,
51 DocstringSectionYields,
52 DocstringWarn,
53 DocstringYield,
54)
55from _griffe.docstrings.utils import docstring_warning, parse_docstring_annotation
56from _griffe.enumerations import DocstringSectionKind, LogLevel
57from _griffe.expressions import ExprName
59if TYPE_CHECKING:
60 from typing import Any, Literal, Pattern
62 from _griffe.expressions import Expr
63 from _griffe.models import Docstring
66_section_kind = {
67 "deprecated": DocstringSectionKind.deprecated,
68 "parameters": DocstringSectionKind.parameters,
69 "other parameters": DocstringSectionKind.other_parameters,
70 "returns": DocstringSectionKind.returns,
71 "yields": DocstringSectionKind.yields,
72 "receives": DocstringSectionKind.receives,
73 "raises": DocstringSectionKind.raises,
74 "warns": DocstringSectionKind.warns,
75 "examples": DocstringSectionKind.examples,
76 "attributes": DocstringSectionKind.attributes,
77 "functions": DocstringSectionKind.functions,
78 "methods": DocstringSectionKind.functions,
79 "classes": DocstringSectionKind.classes,
80 "modules": DocstringSectionKind.modules,
81}
84def _is_empty_line(line: str) -> bool:
85 return not line.strip()
88def _is_dash_line(line: str) -> bool:
89 return not _is_empty_line(line) and _is_empty_line(line.replace("-", ""))
92def _read_block_items(
93 docstring: Docstring,
94 *,
95 offset: int,
96 **options: Any, # noqa: ARG001
97) -> tuple[list[list[str]], int]:
98 lines = docstring.lines
99 if offset >= len(lines): 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true
100 return [], offset
102 new_offset = offset
103 items: list[list[str]] = []
105 # skip first empty lines
106 while _is_empty_line(lines[new_offset]): 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true
107 new_offset += 1
109 # start processing first item
110 current_item = [lines[new_offset]]
111 new_offset += 1
113 # loop on next lines
114 while new_offset < len(lines):
115 line = lines[new_offset]
117 if _is_empty_line(line):
118 # empty line: preserve it in the current item
119 current_item.append("")
121 elif line.startswith(4 * " "):
122 # continuation line
123 current_item.append(line[4:])
125 elif line.startswith(" "): 125 ↛ 127line 125 didn't jump to line 127 because the condition on line 125 was never true
126 # indent between initial and continuation: append but warn
127 cont_indent = len(line) - len(line.lstrip())
128 current_item.append(line[cont_indent:])
129 docstring_warning(
130 docstring,
131 new_offset,
132 f"Confusing indentation for continuation line {new_offset+1} in docstring, "
133 f"should be 4 spaces, not {cont_indent}",
134 )
136 elif new_offset + 1 < len(lines) and _is_dash_line(lines[new_offset + 1]):
137 # detect the start of a new section
138 break
140 else:
141 items.append(current_item)
142 current_item = [line]
144 new_offset += 1
146 if current_item: 146 ↛ 149line 146 didn't jump to line 149 because the condition on line 146 was always true
147 items.append(current_item)
149 return items, new_offset - 1
152def _read_block(docstring: Docstring, *, offset: int, **options: Any) -> tuple[str, int]: # noqa: ARG001
153 lines = docstring.lines
154 if offset >= len(lines): 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true
155 return "", offset
157 new_offset = offset
158 block: list[str] = []
160 # skip first empty lines
161 while _is_empty_line(lines[new_offset]):
162 new_offset += 1
163 while new_offset < len(lines):
164 is_empty = _is_empty_line(lines[new_offset])
165 if is_empty and new_offset < len(lines) - 1 and _is_dash_line(lines[new_offset + 1]):
166 break # Break if a new unnamed section is reached.
168 if is_empty and new_offset < len(lines) - 2 and _is_dash_line(lines[new_offset + 2]):
169 break # Break if a new named section is reached.
171 block.append(lines[new_offset])
172 new_offset += 1
174 return "\n".join(block).rstrip("\n"), new_offset - 1
177_RE_OB: str = r"\{" # opening bracket
178_RE_CB: str = r"\}" # closing bracket
179_RE_NAME: str = r"\*{0,2}[_a-z][_a-z0-9]*"
180_RE_TYPE: str = r".+"
181_RE_RETURNS: Pattern = re.compile(
182 rf"""
183 (?:
184 (?P<nt_name>{_RE_NAME})\s*:\s*(?P<nt_type>{_RE_TYPE}) # name and type
185 | # or
186 (?P<name>{_RE_NAME})\s*:\s* # just name
187 | # or
188 \s*:\s*$ # no name, no type
189 | # or
190 (?::\s*)?(?P<type>{_RE_TYPE})\s* # just type
191 )
192 """,
193 re.IGNORECASE | re.VERBOSE,
194)
195_RE_YIELDS: Pattern = _RE_RETURNS
196_RE_RECEIVES: Pattern = _RE_RETURNS
197_RE_PARAMETER: Pattern = re.compile(
198 rf"""
199 (?P<names>{_RE_NAME}(?:,\s{_RE_NAME})*)
200 (?:
201 \s:\s
202 (?:
203 (?:{_RE_OB}(?P<choices>.+){_RE_CB})|
204 (?P<type>{_RE_TYPE})
205 )?
206 )?
207 """,
208 re.IGNORECASE | re.VERBOSE,
209)
210_RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$")
211_RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$")
214def _read_parameters(
215 docstring: Docstring,
216 *,
217 offset: int,
218 warn_unknown_params: bool = True,
219 **options: Any,
220) -> tuple[list[DocstringParameter], int]:
221 parameters = []
222 annotation: str | Expr | None
224 items, new_offset = _read_block_items(docstring, offset=offset, **options)
226 for item in items:
227 match = _RE_PARAMETER.match(item[0])
228 if not match:
229 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
230 continue
232 names = match.group("names").split(", ")
233 annotation = match.group("type") or None
234 choices = match.group("choices")
235 default = None
236 if choices:
237 annotation = choices
238 default = choices.split(", ", 1)[0]
239 elif annotation:
240 match = re.match(r"^(?P<annotation>.+),\s+default(?: |: |=)(?P<default>.+)$", annotation)
241 if match:
242 default = match.group("default")
243 annotation = match.group("annotation")
244 if annotation and annotation.endswith(", optional"):
245 annotation = annotation[:-10]
246 description = "\n".join(item[1:]).rstrip() if len(item) > 1 else ""
248 if annotation is None:
249 # try to use the annotation from the signature
250 for name in names:
251 with suppress(AttributeError, KeyError):
252 annotation = docstring.parent.parameters[name].annotation # type: ignore[union-attr]
253 break
254 else:
255 docstring_warning(docstring, new_offset, f"No types or annotations for parameters {names}")
256 else:
257 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
259 if default is None:
260 for name in names:
261 with suppress(AttributeError, KeyError):
262 default = docstring.parent.parameters[name].default # type: ignore[union-attr]
263 break
265 if warn_unknown_params:
266 with suppress(AttributeError): # for parameters sections in objects without parameters
267 params = docstring.parent.parameters # type: ignore[union-attr]
268 for name in names:
269 if name not in params:
270 message = f"Parameter '{name}' does not appear in the function signature"
271 for starred_name in (f"*{name}", f"**{name}"):
272 if starred_name in params: 272 ↛ 273line 272 didn't jump to line 273 because the condition on line 272 was never true
273 message += f". Did you mean '{starred_name}'?"
274 break
275 docstring_warning(docstring, new_offset, message)
277 for name in names:
278 parameters.append(DocstringParameter(name, value=default, annotation=annotation, description=description))
280 return parameters, new_offset
283def _read_parameters_section(
284 docstring: Docstring,
285 *,
286 offset: int,
287 **options: Any,
288) -> tuple[DocstringSectionParameters | None, int]:
289 parameters, new_offset = _read_parameters(docstring, offset=offset, **options)
291 if parameters: 291 ↛ 294line 291 didn't jump to line 294 because the condition on line 291 was always true
292 return DocstringSectionParameters(parameters), new_offset
294 docstring_warning(docstring, new_offset, f"Empty parameters section at line {offset}")
295 return None, new_offset
298def _read_other_parameters_section(
299 docstring: Docstring,
300 *,
301 offset: int,
302 warn_unknown_params: bool = True, # noqa: ARG001
303 **options: Any,
304) -> tuple[DocstringSectionOtherParameters | None, int]:
305 parameters, new_offset = _read_parameters(docstring, offset=offset, warn_unknown_params=False, **options)
307 if parameters: 307 ↛ 310line 307 didn't jump to line 310 because the condition on line 307 was always true
308 return DocstringSectionOtherParameters(parameters), new_offset
310 docstring_warning(docstring, new_offset, f"Empty other parameters section at line {offset}")
311 return None, new_offset
314def _read_deprecated_section(
315 docstring: Docstring,
316 *,
317 offset: int,
318 **options: Any,
319) -> tuple[DocstringSectionDeprecated | None, int]:
320 # deprecated
321 # SINCE_VERSION
322 # TEXT?
323 items, new_offset = _read_block_items(docstring, offset=offset, **options)
325 if not items: 325 ↛ 326line 325 didn't jump to line 326 because the condition on line 325 was never true
326 docstring_warning(docstring, new_offset, f"Empty deprecated section at line {offset}")
327 return None, new_offset
329 if len(items) > 1: 329 ↛ 330line 329 didn't jump to line 330 because the condition on line 329 was never true
330 docstring_warning(docstring, new_offset, f"Too many deprecated items at {offset}")
332 item = items[0]
333 version = item[0]
334 text = dedent("\n".join(item[1:]))
335 return DocstringSectionDeprecated(version=version, text=text), new_offset
338def _read_returns_section(
339 docstring: Docstring,
340 *,
341 offset: int,
342 **options: Any,
343) -> tuple[DocstringSectionReturns | None, int]:
344 # (NAME : )?TYPE
345 # TEXT?
346 items, new_offset = _read_block_items(docstring, offset=offset, **options)
348 if not items: 348 ↛ 349line 348 didn't jump to line 349 because the condition on line 348 was never true
349 docstring_warning(docstring, new_offset, f"Empty returns section at line {offset}")
350 return None, new_offset
352 returns = []
353 for index, item in enumerate(items):
354 match = _RE_RETURNS.match(item[0])
355 if not match: 355 ↛ 356line 355 didn't jump to line 356 because the condition on line 355 was never true
356 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
357 continue
359 groups = match.groupdict()
360 name = groups["nt_name"] or groups["name"]
361 annotation = groups["nt_type"] or groups["type"]
362 text = dedent("\n".join(item[1:]))
363 if annotation is None:
364 # try to retrieve the annotation from the docstring parent
365 with suppress(AttributeError, KeyError, ValueError):
366 if docstring.parent.is_function: # type: ignore[union-attr] 366 ↛ 368line 366 didn't jump to line 368 because the condition on line 366 was always true
367 annotation = docstring.parent.returns # type: ignore[union-attr]
368 elif docstring.parent.is_attribute: # type: ignore[union-attr]
369 annotation = docstring.parent.annotation # type: ignore[union-attr]
370 else:
371 raise ValueError
372 if len(items) > 1: 372 ↛ 365line 372 didn't jump to line 365
373 if annotation.is_tuple: 373 ↛ 374line 373 didn't jump to line 374 because the condition on line 373 was never true
374 annotation = annotation.slice.elements[index]
375 else:
376 if annotation.is_iterator: 376 ↛ 377line 376 didn't jump to line 377 because the condition on line 376 was never true
377 return_item = annotation.slice
378 elif annotation.is_generator: 378 ↛ 381line 378 didn't jump to line 381 because the condition on line 378 was always true
379 return_item = annotation.slice.elements[2]
380 else:
381 raise ValueError
382 if isinstance(return_item, ExprName): 382 ↛ 383line 382 didn't jump to line 383 because the condition on line 382 was never true
383 annotation = return_item
384 elif return_item.is_tuple: 384 ↛ 387line 384 didn't jump to line 387 because the condition on line 384 was always true
385 annotation = return_item.slice.elements[index]
386 else:
387 annotation = return_item
388 else:
389 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
390 returns.append(DocstringReturn(name=name or "", annotation=annotation, description=text))
391 return DocstringSectionReturns(returns), new_offset
394def _read_yields_section(
395 docstring: Docstring,
396 *,
397 offset: int,
398 **options: Any,
399) -> tuple[DocstringSectionYields | None, int]:
400 # yields
401 # (NAME : )?TYPE
402 # TEXT?
403 items, new_offset = _read_block_items(docstring, offset=offset, **options)
405 if not items: 405 ↛ 406line 405 didn't jump to line 406 because the condition on line 405 was never true
406 docstring_warning(docstring, new_offset, f"Empty yields section at line {offset}")
407 return None, new_offset
409 yields = []
410 for index, item in enumerate(items):
411 match = _RE_YIELDS.match(item[0])
412 if not match: 412 ↛ 413line 412 didn't jump to line 413 because the condition on line 412 was never true
413 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
414 continue
416 groups = match.groupdict()
417 name = groups["nt_name"] or groups["name"]
418 annotation = groups["nt_type"] or groups["type"]
419 text = dedent("\n".join(item[1:]))
420 if annotation is None:
421 # try to retrieve the annotation from the docstring parent
422 with suppress(AttributeError, IndexError, KeyError, ValueError):
423 annotation = docstring.parent.annotation # type: ignore[union-attr]
424 if annotation.is_iterator:
425 yield_item = annotation.slice
426 elif annotation.is_generator: 426 ↛ 429line 426 didn't jump to line 429 because the condition on line 426 was always true
427 yield_item = annotation.slice.elements[0]
428 else:
429 raise ValueError
430 if isinstance(yield_item, ExprName):
431 annotation = yield_item
432 elif yield_item.is_tuple: 432 ↛ 435line 432 didn't jump to line 435 because the condition on line 432 was always true
433 annotation = yield_item.slice.elements[index]
434 else:
435 annotation = yield_item
436 else:
437 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
438 yields.append(DocstringYield(name=name or "", annotation=annotation, description=text))
439 return DocstringSectionYields(yields), new_offset
442def _read_receives_section(
443 docstring: Docstring,
444 *,
445 offset: int,
446 **options: Any,
447) -> tuple[DocstringSectionReceives | None, int]:
448 # receives
449 # (NAME : )?TYPE
450 # TEXT?
451 items, new_offset = _read_block_items(docstring, offset=offset, **options)
453 if not items: 453 ↛ 454line 453 didn't jump to line 454 because the condition on line 453 was never true
454 docstring_warning(docstring, new_offset, f"Empty receives section at line {offset}")
455 return None, new_offset
457 receives = []
458 for index, item in enumerate(items):
459 match = _RE_RECEIVES.match(item[0])
460 if not match: 460 ↛ 461line 460 didn't jump to line 461 because the condition on line 460 was never true
461 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
462 continue
464 groups = match.groupdict()
465 name = groups["nt_name"] or groups["name"]
466 annotation = groups["nt_type"] or groups["type"]
467 text = dedent("\n".join(item[1:]))
468 if annotation is None:
469 # try to retrieve the annotation from the docstring parent
470 with suppress(AttributeError, KeyError):
471 annotation = docstring.parent.returns # type: ignore[union-attr]
472 if annotation.is_generator: 472 ↛ 470line 472 didn't jump to line 470
473 receives_item = annotation.slice.elements[1]
474 if isinstance(receives_item, ExprName):
475 annotation = receives_item
476 elif receives_item.is_tuple: 476 ↛ 479line 476 didn't jump to line 479 because the condition on line 476 was always true
477 annotation = receives_item.slice.elements[index]
478 else:
479 annotation = receives_item
480 else:
481 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
482 receives.append(DocstringReceive(name=name or "", annotation=annotation, description=text))
483 return DocstringSectionReceives(receives), new_offset
486def _read_raises_section(
487 docstring: Docstring,
488 *,
489 offset: int,
490 **options: Any,
491) -> tuple[DocstringSectionRaises | None, int]:
492 # raises
493 # EXCEPTION
494 # TEXT?
495 items, new_offset = _read_block_items(docstring, offset=offset, **options)
497 if not items: 497 ↛ 498line 497 didn't jump to line 498 because the condition on line 497 was never true
498 docstring_warning(docstring, new_offset, f"Empty raises section at line {offset}")
499 return None, new_offset
501 raises = []
502 for item in items:
503 annotation = parse_docstring_annotation(item[0], docstring)
504 text = dedent("\n".join(item[1:]))
505 raises.append(DocstringRaise(annotation=annotation, description=text))
506 return DocstringSectionRaises(raises), new_offset
509def _read_warns_section(
510 docstring: Docstring,
511 *,
512 offset: int,
513 **options: Any,
514) -> tuple[DocstringSectionWarns | None, int]:
515 # warns
516 # WARNING
517 # TEXT?
518 items, new_offset = _read_block_items(docstring, offset=offset, **options)
520 if not items: 520 ↛ 521line 520 didn't jump to line 521 because the condition on line 520 was never true
521 docstring_warning(docstring, new_offset, f"Empty warns section at line {offset}")
522 return None, new_offset
524 warns = []
525 for item in items:
526 annotation = parse_docstring_annotation(item[0], docstring)
527 text = dedent("\n".join(item[1:]))
528 warns.append(DocstringWarn(annotation=annotation, description=text))
529 return DocstringSectionWarns(warns), new_offset
532def _read_attributes_section(
533 docstring: Docstring,
534 *,
535 offset: int,
536 **options: Any,
537) -> tuple[DocstringSectionAttributes | None, int]:
538 # attributes (for classes)
539 # NAME( : TYPE)?
540 # TEXT?
541 items, new_offset = _read_block_items(docstring, offset=offset, **options)
543 if not items: 543 ↛ 544line 543 didn't jump to line 544 because the condition on line 543 was never true
544 docstring_warning(docstring, new_offset, f"Empty attributes section at line {offset}")
545 return None, new_offset
547 annotation: str | Expr | None
548 attributes = []
549 for item in items:
550 name_type = item[0]
551 if ":" in name_type:
552 name, annotation = name_type.split(":", 1)
553 name = name.strip()
554 annotation = annotation.strip() or None
555 else:
556 name = name_type
557 annotation = None
558 if annotation is None:
559 with suppress(AttributeError, KeyError):
560 annotation = docstring.parent.members[name].annotation # type: ignore[union-attr]
561 else:
562 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
563 text = dedent("\n".join(item[1:]))
564 attributes.append(DocstringAttribute(name=name, annotation=annotation, description=text))
565 return DocstringSectionAttributes(attributes), new_offset
568def _read_functions_section(
569 docstring: Docstring,
570 *,
571 offset: int,
572 **options: Any,
573) -> tuple[DocstringSectionFunctions | None, int]:
574 # SIGNATURE
575 # TEXT?
576 items, new_offset = _read_block_items(docstring, offset=offset, **options)
578 if not items: 578 ↛ 579line 578 didn't jump to line 579 because the condition on line 578 was never true
579 docstring_warning(docstring, new_offset, f"Empty functions/methods section at line {offset}")
580 return None, new_offset
582 functions = []
583 signature: str | Expr | None
584 for item in items:
585 name_signature = item[0]
586 if "(" in name_signature:
587 name = name_signature.split("(", 1)[0]
588 name = name.strip()
589 signature = name_signature.strip()
590 else:
591 name = name_signature
592 signature = None
593 text = dedent("\n".join(item[1:])).strip()
594 functions.append(DocstringFunction(name=name, annotation=signature, description=text))
595 return DocstringSectionFunctions(functions), new_offset
598def _read_classes_section(
599 docstring: Docstring,
600 *,
601 offset: int,
602 **options: Any,
603) -> tuple[DocstringSectionClasses | None, int]:
604 # SIGNATURE
605 # TEXT?
606 items, new_offset = _read_block_items(docstring, offset=offset, **options)
608 if not items: 608 ↛ 609line 608 didn't jump to line 609 because the condition on line 608 was never true
609 docstring_warning(docstring, new_offset, f"Empty classes section at line {offset}")
610 return None, new_offset
612 classes = []
613 signature: str | Expr | None
614 for item in items:
615 name_signature = item[0]
616 if "(" in name_signature:
617 name = name_signature.split("(", 1)[0]
618 name = name.strip()
619 signature = name_signature.strip()
620 else:
621 name = name_signature
622 signature = None
623 text = dedent("\n".join(item[1:])).strip()
624 classes.append(DocstringClass(name=name, annotation=signature, description=text))
625 return DocstringSectionClasses(classes), new_offset
628def _read_modules_section(
629 docstring: Docstring,
630 *,
631 offset: int,
632 **options: Any,
633) -> tuple[DocstringSectionModules | None, int]:
634 # NAME
635 # TEXT?
636 items, new_offset = _read_block_items(docstring, offset=offset, **options)
638 if not items: 638 ↛ 639line 638 didn't jump to line 639 because the condition on line 638 was never true
639 docstring_warning(docstring, new_offset, f"Empty modules section at line {offset}")
640 return None, new_offset
642 modules = []
643 signature: str | Expr | None
644 for item in items:
645 name_signature = item[0]
646 if "(" in name_signature: 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true
647 name = name_signature.split("(", 1)[0]
648 name = name.strip()
649 signature = name_signature.strip()
650 else:
651 name = name_signature
652 signature = None
653 text = dedent("\n".join(item[1:])).strip()
654 modules.append(DocstringModule(name=name, annotation=signature, description=text))
655 return DocstringSectionModules(modules), new_offset
658def _read_examples_section(
659 docstring: Docstring,
660 *,
661 offset: int,
662 trim_doctest_flags: bool = True,
663 **options: Any,
664) -> tuple[DocstringSectionExamples | None, int]:
665 text, new_offset = _read_block(docstring, offset=offset, **options)
667 sub_sections: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = []
668 in_code_example = False
669 in_code_block = False
670 current_text: list[str] = []
671 current_example: list[str] = []
673 for line in text.split("\n"):
674 if _is_empty_line(line):
675 if in_code_example:
676 if current_example: 676 ↛ 679line 676 didn't jump to line 679 because the condition on line 676 was always true
677 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
678 current_example = []
679 in_code_example = False
680 else:
681 current_text.append(line)
683 elif in_code_example:
684 if trim_doctest_flags:
685 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
686 line = _RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901
687 current_example.append(line)
689 elif line.startswith("```"):
690 in_code_block = not in_code_block
691 current_text.append(line)
693 elif in_code_block:
694 current_text.append(line)
696 elif line.startswith(">>>"):
697 if current_text: 697 ↛ 700line 697 didn't jump to line 700 because the condition on line 697 was always true
698 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
699 current_text = []
700 in_code_example = True
702 if trim_doctest_flags:
703 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
704 current_example.append(line)
706 else:
707 current_text.append(line)
709 if current_text:
710 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
711 elif current_example: 711 ↛ 714line 711 didn't jump to line 714 because the condition on line 711 was always true
712 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
714 if sub_sections: 714 ↛ 717line 714 didn't jump to line 717 because the condition on line 714 was always true
715 return DocstringSectionExamples(sub_sections), new_offset
717 docstring_warning(docstring, new_offset, f"Empty examples section at line {offset}")
718 return None, new_offset
721def _append_section(sections: list, current: list[str], admonition_title: str) -> None:
722 if admonition_title:
723 kind = admonition_title.lower().replace(" ", "-")
724 if kind in ("warnings", "notes"):
725 # NumpyDoc sections are pluralised but admonitions aren't.
726 # We can special-case these explicitly so that it renders
727 # as one would expect.
728 kind = kind[:-1]
729 sections.append(
730 DocstringSectionAdmonition(
731 kind=kind,
732 text="\n".join(current).rstrip("\n"),
733 title=admonition_title,
734 ),
735 )
736 elif current and any(current):
737 sections.append(DocstringSectionText("\n".join(current).rstrip("\n")))
740_section_reader = {
741 DocstringSectionKind.parameters: _read_parameters_section,
742 DocstringSectionKind.other_parameters: _read_other_parameters_section,
743 DocstringSectionKind.deprecated: _read_deprecated_section,
744 DocstringSectionKind.raises: _read_raises_section,
745 DocstringSectionKind.warns: _read_warns_section,
746 DocstringSectionKind.examples: _read_examples_section,
747 DocstringSectionKind.attributes: _read_attributes_section,
748 DocstringSectionKind.functions: _read_functions_section,
749 DocstringSectionKind.classes: _read_classes_section,
750 DocstringSectionKind.modules: _read_modules_section,
751 DocstringSectionKind.returns: _read_returns_section,
752 DocstringSectionKind.yields: _read_yields_section,
753 DocstringSectionKind.receives: _read_receives_section,
754}
757def parse_numpy(
758 docstring: Docstring,
759 *,
760 ignore_init_summary: bool = False,
761 trim_doctest_flags: bool = True,
762 warn_unknown_params: bool = True,
763 **options: Any,
764) -> list[DocstringSection]:
765 """Parse a Numpydoc-style docstring.
767 This function iterates on lines of a docstring to build sections.
768 It then returns this list of sections.
770 Parameters:
771 docstring: The docstring to parse.
772 ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings.
773 trim_doctest_flags: Whether to remove doctest flags from Python example blocks.
774 warn_unknown_params: Warn about documented parameters not appearing in the signature.
775 **options: Additional parsing options.
777 Returns:
778 A list of docstring sections.
779 """
780 sections: list[DocstringSection] = []
781 current_section = []
782 admonition_title = ""
784 in_code_block = False
785 lines = docstring.lines
787 options = {
788 "trim_doctest_flags": trim_doctest_flags,
789 "ignore_init_summary": ignore_init_summary,
790 "warn_unknown_params": warn_unknown_params,
791 **options,
792 }
794 ignore_summary = (
795 options["ignore_init_summary"]
796 and docstring.parent is not None
797 and docstring.parent.name == "__init__"
798 and docstring.parent.is_function
799 and docstring.parent.parent is not None
800 and docstring.parent.parent.is_class
801 )
803 offset = 2 if ignore_summary else 0
805 while offset < len(lines):
806 line_lower = lines[offset].lower()
808 # Code blocks can contain dash lines that we must not interpret.
809 if in_code_block:
810 # End of code block.
811 if line_lower.lstrip(" ").startswith("```"):
812 in_code_block = False
813 # Lines in code block must not be interpreted in any way.
814 current_section.append(lines[offset])
816 # Start of code block.
817 elif line_lower.lstrip(" ").startswith("```"):
818 in_code_block = True
819 current_section.append(lines[offset])
821 # Dash lines after empty lines lose their meaning.
822 elif _is_empty_line(lines[offset]):
823 current_section.append("")
825 # End of the docstring, wrap up.
826 elif offset == len(lines) - 1:
827 current_section.append(lines[offset])
828 _append_section(sections, current_section, admonition_title)
829 admonition_title = ""
830 current_section = []
832 # Dash line after regular, non-empty line.
833 elif _is_dash_line(lines[offset + 1]):
834 # Finish reading current section.
835 _append_section(sections, current_section, admonition_title)
836 current_section = []
838 # Start parsing new (known) section.
839 if line_lower in _section_kind:
840 admonition_title = ""
841 reader = _section_reader[_section_kind[line_lower]]
842 section, offset = reader(docstring, offset=offset + 2, **options) # type: ignore[operator]
843 if section: 843 ↛ 855line 843 didn't jump to line 855 because the condition on line 843 was always true
844 sections.append(section)
846 # Start parsing admonition.
847 else:
848 admonition_title = lines[offset]
849 offset += 1 # skip next dash line
851 # Regular line.
852 else:
853 current_section.append(lines[offset])
855 offset += 1
857 # Finish current section.
858 _append_section(sections, current_section, admonition_title)
860 return sections