Coverage for src/_griffe/docstrings/numpy.py: 84.82%
422 statements
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +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 re import Pattern
61 from typing import Any, Literal
63 from _griffe.expressions import Expr
64 from _griffe.models import Docstring
67_section_kind = {
68 "deprecated": DocstringSectionKind.deprecated,
69 "parameters": DocstringSectionKind.parameters,
70 "other parameters": DocstringSectionKind.other_parameters,
71 "returns": DocstringSectionKind.returns,
72 "yields": DocstringSectionKind.yields,
73 "receives": DocstringSectionKind.receives,
74 "raises": DocstringSectionKind.raises,
75 "warns": DocstringSectionKind.warns,
76 "examples": DocstringSectionKind.examples,
77 "attributes": DocstringSectionKind.attributes,
78 "functions": DocstringSectionKind.functions,
79 "methods": DocstringSectionKind.functions,
80 "classes": DocstringSectionKind.classes,
81 "modules": DocstringSectionKind.modules,
82}
85def _is_empty_line(line: str) -> bool:
86 return not line.strip()
89def _is_dash_line(line: str) -> bool:
90 return not _is_empty_line(line) and _is_empty_line(line.replace("-", ""))
93def _read_block_items(
94 docstring: Docstring,
95 *,
96 offset: int,
97 **options: Any, # noqa: ARG001
98) -> tuple[list[list[str]], int]:
99 lines = docstring.lines
100 if offset >= len(lines): 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 return [], offset
103 new_offset = offset
104 items: list[list[str]] = []
106 # skip first empty lines
107 while _is_empty_line(lines[new_offset]): 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 new_offset += 1
110 # start processing first item
111 current_item = [lines[new_offset]]
112 new_offset += 1
114 # loop on next lines
115 while new_offset < len(lines):
116 line = lines[new_offset]
118 if _is_empty_line(line):
119 # empty line: preserve it in the current item
120 current_item.append("")
122 elif line.startswith(4 * " "):
123 # continuation line
124 current_item.append(line[4:])
126 elif line.startswith(" "): 126 ↛ 128line 126 didn't jump to line 128 because the condition on line 126 was never true
127 # indent between initial and continuation: append but warn
128 cont_indent = len(line) - len(line.lstrip())
129 current_item.append(line[cont_indent:])
130 docstring_warning(
131 docstring,
132 new_offset,
133 f"Confusing indentation for continuation line {new_offset+1} in docstring, "
134 f"should be 4 spaces, not {cont_indent}",
135 )
137 elif new_offset + 1 < len(lines) and _is_dash_line(lines[new_offset + 1]):
138 # detect the start of a new section
139 break
141 else:
142 items.append(current_item)
143 current_item = [line]
145 new_offset += 1
147 if current_item: 147 ↛ 150line 147 didn't jump to line 150 because the condition on line 147 was always true
148 items.append(current_item)
150 return items, new_offset - 1
153def _read_block(docstring: Docstring, *, offset: int, **options: Any) -> tuple[str, int]: # noqa: ARG001
154 lines = docstring.lines
155 if offset >= len(lines): 155 ↛ 156line 155 didn't jump to line 156 because the condition on line 155 was never true
156 return "", offset
158 new_offset = offset
159 block: list[str] = []
161 # skip first empty lines
162 while _is_empty_line(lines[new_offset]):
163 new_offset += 1
164 while new_offset < len(lines):
165 is_empty = _is_empty_line(lines[new_offset])
166 if is_empty and new_offset < len(lines) - 1 and _is_dash_line(lines[new_offset + 1]):
167 break # Break if a new unnamed section is reached.
169 if is_empty and new_offset < len(lines) - 2 and _is_dash_line(lines[new_offset + 2]):
170 break # Break if a new named section is reached.
172 block.append(lines[new_offset])
173 new_offset += 1
175 return "\n".join(block).rstrip("\n"), new_offset - 1
178_RE_OB: str = r"\{" # opening bracket
179_RE_CB: str = r"\}" # closing bracket
180_RE_NAME: str = r"\*{0,2}[_a-z][_a-z0-9]*"
181_RE_TYPE: str = r".+"
182_RE_RETURNS: Pattern = re.compile(
183 rf"""
184 (?:
185 (?P<nt_name>{_RE_NAME})\s*:\s*(?P<nt_type>{_RE_TYPE}) # name and type
186 | # or
187 (?P<name>{_RE_NAME})\s*:\s* # just name
188 | # or
189 \s*:\s*$ # no name, no type
190 | # or
191 (?::\s*)?(?P<type>{_RE_TYPE})\s* # just type
192 )
193 """,
194 re.IGNORECASE | re.VERBOSE,
195)
196_RE_YIELDS: Pattern = _RE_RETURNS
197_RE_RECEIVES: Pattern = _RE_RETURNS
198_RE_PARAMETER: Pattern = re.compile(
199 rf"""
200 (?P<names>{_RE_NAME}(?:,\s{_RE_NAME})*)
201 (?:
202 \s:\s
203 (?:
204 (?:{_RE_OB}(?P<choices>.+){_RE_CB})|
205 (?P<type>{_RE_TYPE})
206 )?
207 )?
208 """,
209 re.IGNORECASE | re.VERBOSE,
210)
211_RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$")
212_RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$")
215def _read_parameters(
216 docstring: Docstring,
217 *,
218 offset: int,
219 warn_unknown_params: bool = True,
220 **options: Any,
221) -> tuple[list[DocstringParameter], int]:
222 parameters: list[DocstringParameter] = []
223 annotation: str | Expr | None
225 items, new_offset = _read_block_items(docstring, offset=offset, **options)
227 for item in items:
228 match = _RE_PARAMETER.match(item[0])
229 if not match:
230 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
231 continue
233 names = match.group("names").split(", ")
234 annotation = match.group("type") or None
235 choices = match.group("choices")
236 default = None
237 if choices:
238 annotation = choices
239 default = choices.split(", ", 1)[0]
240 elif annotation:
241 match = re.match(r"^(?P<annotation>.+),\s+default(?: |: |=)(?P<default>.+)$", annotation)
242 if match:
243 default = match.group("default")
244 annotation = match.group("annotation")
245 if annotation and annotation.endswith(", optional"):
246 annotation = annotation[:-10]
247 description = "\n".join(item[1:]).rstrip() if len(item) > 1 else ""
249 if annotation is None:
250 # try to use the annotation from the signature
251 for name in names:
252 with suppress(AttributeError, KeyError):
253 annotation = docstring.parent.parameters[name].annotation # type: ignore[union-attr]
254 break
255 else:
256 docstring_warning(docstring, new_offset, f"No types or annotations for parameters {names}")
257 else:
258 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
260 if default is None:
261 for name in names:
262 with suppress(AttributeError, KeyError):
263 default = docstring.parent.parameters[name].default # type: ignore[union-attr]
264 break
266 if warn_unknown_params:
267 with suppress(AttributeError): # for parameters sections in objects without parameters
268 params = docstring.parent.parameters # type: ignore[union-attr]
269 for name in names:
270 if name not in params:
271 message = f"Parameter '{name}' does not appear in the function signature"
272 for starred_name in (f"*{name}", f"**{name}"):
273 if starred_name in params: 273 ↛ 274line 273 didn't jump to line 274 because the condition on line 273 was never true
274 message += f". Did you mean '{starred_name}'?"
275 break
276 docstring_warning(docstring, new_offset, message)
278 parameters.extend(
279 DocstringParameter(name, value=default, annotation=annotation, description=description) for name in names
280 )
282 return parameters, new_offset
285def _read_parameters_section(
286 docstring: Docstring,
287 *,
288 offset: int,
289 **options: Any,
290) -> tuple[DocstringSectionParameters | None, int]:
291 parameters, new_offset = _read_parameters(docstring, offset=offset, **options)
293 if parameters: 293 ↛ 296line 293 didn't jump to line 296 because the condition on line 293 was always true
294 return DocstringSectionParameters(parameters), new_offset
296 docstring_warning(docstring, new_offset, f"Empty parameters section at line {offset}")
297 return None, new_offset
300def _read_other_parameters_section(
301 docstring: Docstring,
302 *,
303 offset: int,
304 warn_unknown_params: bool = True, # noqa: ARG001
305 **options: Any,
306) -> tuple[DocstringSectionOtherParameters | None, int]:
307 parameters, new_offset = _read_parameters(docstring, offset=offset, warn_unknown_params=False, **options)
309 if parameters: 309 ↛ 312line 309 didn't jump to line 312 because the condition on line 309 was always true
310 return DocstringSectionOtherParameters(parameters), new_offset
312 docstring_warning(docstring, new_offset, f"Empty other parameters section at line {offset}")
313 return None, new_offset
316def _read_deprecated_section(
317 docstring: Docstring,
318 *,
319 offset: int,
320 **options: Any,
321) -> tuple[DocstringSectionDeprecated | None, int]:
322 # deprecated
323 # SINCE_VERSION
324 # TEXT?
325 items, new_offset = _read_block_items(docstring, offset=offset, **options)
327 if not items: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true
328 docstring_warning(docstring, new_offset, f"Empty deprecated section at line {offset}")
329 return None, new_offset
331 if len(items) > 1: 331 ↛ 332line 331 didn't jump to line 332 because the condition on line 331 was never true
332 docstring_warning(docstring, new_offset, f"Too many deprecated items at {offset}")
334 item = items[0]
335 version = item[0]
336 text = dedent("\n".join(item[1:]))
337 return DocstringSectionDeprecated(version=version, text=text), new_offset
340def _read_returns_section(
341 docstring: Docstring,
342 *,
343 offset: int,
344 **options: Any,
345) -> tuple[DocstringSectionReturns | None, int]:
346 # (NAME : )?TYPE
347 # TEXT?
348 items, new_offset = _read_block_items(docstring, offset=offset, **options)
350 if not items: 350 ↛ 351line 350 didn't jump to line 351 because the condition on line 350 was never true
351 docstring_warning(docstring, new_offset, f"Empty returns section at line {offset}")
352 return None, new_offset
354 returns = []
355 for index, item in enumerate(items):
356 match = _RE_RETURNS.match(item[0])
357 if not match: 357 ↛ 358line 357 didn't jump to line 358 because the condition on line 357 was never true
358 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
359 continue
361 groups = match.groupdict()
362 name = groups["nt_name"] or groups["name"]
363 annotation = groups["nt_type"] or groups["type"]
364 text = dedent("\n".join(item[1:]))
365 if annotation is None:
366 # try to retrieve the annotation from the docstring parent
367 with suppress(AttributeError, KeyError, ValueError):
368 if docstring.parent.is_function: # type: ignore[union-attr] 368 ↛ 370line 368 didn't jump to line 370 because the condition on line 368 was always true
369 annotation = docstring.parent.returns # type: ignore[union-attr]
370 elif docstring.parent.is_attribute: # type: ignore[union-attr]
371 annotation = docstring.parent.annotation # type: ignore[union-attr]
372 else:
373 raise ValueError
374 if len(items) > 1: 374 ↛ 392line 374 didn't jump to line 392
375 if annotation.is_tuple: 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true
376 annotation = annotation.slice.elements[index]
377 else:
378 if annotation.is_iterator: 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true
379 return_item = annotation.slice
380 elif annotation.is_generator: 380 ↛ 383line 380 didn't jump to line 383 because the condition on line 380 was always true
381 return_item = annotation.slice.elements[2]
382 else:
383 raise ValueError
384 if isinstance(return_item, ExprName): 384 ↛ 385line 384 didn't jump to line 385 because the condition on line 384 was never true
385 annotation = return_item
386 elif return_item.is_tuple: 386 ↛ 389line 386 didn't jump to line 389 because the condition on line 386 was always true
387 annotation = return_item.slice.elements[index]
388 else:
389 annotation = return_item
390 else:
391 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
392 returns.append(DocstringReturn(name=name or "", annotation=annotation, description=text))
393 return DocstringSectionReturns(returns), new_offset
396def _read_yields_section(
397 docstring: Docstring,
398 *,
399 offset: int,
400 **options: Any,
401) -> tuple[DocstringSectionYields | None, int]:
402 # yields
403 # (NAME : )?TYPE
404 # TEXT?
405 items, new_offset = _read_block_items(docstring, offset=offset, **options)
407 if not items: 407 ↛ 408line 407 didn't jump to line 408 because the condition on line 407 was never true
408 docstring_warning(docstring, new_offset, f"Empty yields section at line {offset}")
409 return None, new_offset
411 yields = []
412 for index, item in enumerate(items):
413 match = _RE_YIELDS.match(item[0])
414 if not match: 414 ↛ 415line 414 didn't jump to line 415 because the condition on line 414 was never true
415 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
416 continue
418 groups = match.groupdict()
419 name = groups["nt_name"] or groups["name"]
420 annotation = groups["nt_type"] or groups["type"]
421 text = dedent("\n".join(item[1:]))
422 if annotation is None:
423 # try to retrieve the annotation from the docstring parent
424 with suppress(AttributeError, IndexError, KeyError, ValueError):
425 annotation = docstring.parent.annotation # type: ignore[union-attr]
426 if annotation.is_iterator:
427 yield_item = annotation.slice
428 elif annotation.is_generator: 428 ↛ 431line 428 didn't jump to line 431 because the condition on line 428 was always true
429 yield_item = annotation.slice.elements[0]
430 else:
431 raise ValueError
432 if isinstance(yield_item, ExprName):
433 annotation = yield_item
434 elif yield_item.is_tuple: 434 ↛ 437line 434 didn't jump to line 437 because the condition on line 434 was always true
435 annotation = yield_item.slice.elements[index]
436 else:
437 annotation = yield_item
438 else:
439 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
440 yields.append(DocstringYield(name=name or "", annotation=annotation, description=text))
441 return DocstringSectionYields(yields), new_offset
444def _read_receives_section(
445 docstring: Docstring,
446 *,
447 offset: int,
448 **options: Any,
449) -> tuple[DocstringSectionReceives | None, int]:
450 # receives
451 # (NAME : )?TYPE
452 # TEXT?
453 items, new_offset = _read_block_items(docstring, offset=offset, **options)
455 if not items: 455 ↛ 456line 455 didn't jump to line 456 because the condition on line 455 was never true
456 docstring_warning(docstring, new_offset, f"Empty receives section at line {offset}")
457 return None, new_offset
459 receives = []
460 for index, item in enumerate(items):
461 match = _RE_RECEIVES.match(item[0])
462 if not match: 462 ↛ 463line 462 didn't jump to line 463 because the condition on line 462 was never true
463 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
464 continue
466 groups = match.groupdict()
467 name = groups["nt_name"] or groups["name"]
468 annotation = groups["nt_type"] or groups["type"]
469 text = dedent("\n".join(item[1:]))
470 if annotation is None:
471 # try to retrieve the annotation from the docstring parent
472 with suppress(AttributeError, KeyError):
473 annotation = docstring.parent.returns # type: ignore[union-attr]
474 if annotation.is_generator: 474 ↛ 484line 474 didn't jump to line 484
475 receives_item = annotation.slice.elements[1]
476 if isinstance(receives_item, ExprName):
477 annotation = receives_item
478 elif receives_item.is_tuple: 478 ↛ 481line 478 didn't jump to line 481 because the condition on line 478 was always true
479 annotation = receives_item.slice.elements[index]
480 else:
481 annotation = receives_item
482 else:
483 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
484 receives.append(DocstringReceive(name=name or "", annotation=annotation, description=text))
485 return DocstringSectionReceives(receives), new_offset
488def _read_raises_section(
489 docstring: Docstring,
490 *,
491 offset: int,
492 **options: Any,
493) -> tuple[DocstringSectionRaises | None, int]:
494 # raises
495 # EXCEPTION
496 # TEXT?
497 items, new_offset = _read_block_items(docstring, offset=offset, **options)
499 if not items: 499 ↛ 500line 499 didn't jump to line 500 because the condition on line 499 was never true
500 docstring_warning(docstring, new_offset, f"Empty raises section at line {offset}")
501 return None, new_offset
503 raises = []
504 for item in items:
505 annotation = parse_docstring_annotation(item[0], docstring)
506 text = dedent("\n".join(item[1:]))
507 raises.append(DocstringRaise(annotation=annotation, description=text))
508 return DocstringSectionRaises(raises), new_offset
511def _read_warns_section(
512 docstring: Docstring,
513 *,
514 offset: int,
515 **options: Any,
516) -> tuple[DocstringSectionWarns | None, int]:
517 # warns
518 # WARNING
519 # TEXT?
520 items, new_offset = _read_block_items(docstring, offset=offset, **options)
522 if not items: 522 ↛ 523line 522 didn't jump to line 523 because the condition on line 522 was never true
523 docstring_warning(docstring, new_offset, f"Empty warns section at line {offset}")
524 return None, new_offset
526 warns = []
527 for item in items:
528 annotation = parse_docstring_annotation(item[0], docstring)
529 text = dedent("\n".join(item[1:]))
530 warns.append(DocstringWarn(annotation=annotation, description=text))
531 return DocstringSectionWarns(warns), new_offset
534def _read_attributes_section(
535 docstring: Docstring,
536 *,
537 offset: int,
538 **options: Any,
539) -> tuple[DocstringSectionAttributes | None, int]:
540 # attributes (for classes)
541 # NAME( : TYPE)?
542 # TEXT?
543 items, new_offset = _read_block_items(docstring, offset=offset, **options)
545 if not items: 545 ↛ 546line 545 didn't jump to line 546 because the condition on line 545 was never true
546 docstring_warning(docstring, new_offset, f"Empty attributes section at line {offset}")
547 return None, new_offset
549 annotation: str | Expr | None
550 attributes = []
551 for item in items:
552 name_type = item[0]
553 if ":" in name_type:
554 name, annotation = name_type.split(":", 1)
555 name = name.strip()
556 annotation = annotation.strip() or None
557 else:
558 name = name_type
559 annotation = None
560 if annotation is None:
561 with suppress(AttributeError, KeyError):
562 annotation = docstring.parent.members[name].annotation # type: ignore[union-attr]
563 else:
564 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
565 text = dedent("\n".join(item[1:]))
566 attributes.append(DocstringAttribute(name=name, annotation=annotation, description=text))
567 return DocstringSectionAttributes(attributes), new_offset
570def _read_functions_section(
571 docstring: Docstring,
572 *,
573 offset: int,
574 **options: Any,
575) -> tuple[DocstringSectionFunctions | None, int]:
576 # SIGNATURE
577 # TEXT?
578 items, new_offset = _read_block_items(docstring, offset=offset, **options)
580 if not items: 580 ↛ 581line 580 didn't jump to line 581 because the condition on line 580 was never true
581 docstring_warning(docstring, new_offset, f"Empty functions/methods section at line {offset}")
582 return None, new_offset
584 functions = []
585 signature: str | Expr | None
586 for item in items:
587 name_signature = item[0]
588 if "(" in name_signature:
589 name = name_signature.split("(", 1)[0]
590 name = name.strip()
591 signature = name_signature.strip()
592 else:
593 name = name_signature
594 signature = None
595 text = dedent("\n".join(item[1:])).strip()
596 functions.append(DocstringFunction(name=name, annotation=signature, description=text))
597 return DocstringSectionFunctions(functions), new_offset
600def _read_classes_section(
601 docstring: Docstring,
602 *,
603 offset: int,
604 **options: Any,
605) -> tuple[DocstringSectionClasses | None, int]:
606 # SIGNATURE
607 # TEXT?
608 items, new_offset = _read_block_items(docstring, offset=offset, **options)
610 if not items: 610 ↛ 611line 610 didn't jump to line 611 because the condition on line 610 was never true
611 docstring_warning(docstring, new_offset, f"Empty classes section at line {offset}")
612 return None, new_offset
614 classes = []
615 signature: str | Expr | None
616 for item in items:
617 name_signature = item[0]
618 if "(" in name_signature:
619 name = name_signature.split("(", 1)[0]
620 name = name.strip()
621 signature = name_signature.strip()
622 else:
623 name = name_signature
624 signature = None
625 text = dedent("\n".join(item[1:])).strip()
626 classes.append(DocstringClass(name=name, annotation=signature, description=text))
627 return DocstringSectionClasses(classes), new_offset
630def _read_modules_section(
631 docstring: Docstring,
632 *,
633 offset: int,
634 **options: Any,
635) -> tuple[DocstringSectionModules | None, int]:
636 # NAME
637 # TEXT?
638 items, new_offset = _read_block_items(docstring, offset=offset, **options)
640 if not items: 640 ↛ 641line 640 didn't jump to line 641 because the condition on line 640 was never true
641 docstring_warning(docstring, new_offset, f"Empty modules section at line {offset}")
642 return None, new_offset
644 modules = []
645 signature: str | Expr | None
646 for item in items:
647 name_signature = item[0]
648 if "(" in name_signature: 648 ↛ 649line 648 didn't jump to line 649 because the condition on line 648 was never true
649 name = name_signature.split("(", 1)[0]
650 name = name.strip()
651 signature = name_signature.strip()
652 else:
653 name = name_signature
654 signature = None
655 text = dedent("\n".join(item[1:])).strip()
656 modules.append(DocstringModule(name=name, annotation=signature, description=text))
657 return DocstringSectionModules(modules), new_offset
660def _read_examples_section(
661 docstring: Docstring,
662 *,
663 offset: int,
664 trim_doctest_flags: bool = True,
665 **options: Any,
666) -> tuple[DocstringSectionExamples | None, int]:
667 text, new_offset = _read_block(docstring, offset=offset, **options)
669 sub_sections: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = []
670 in_code_example = False
671 in_code_block = False
672 current_text: list[str] = []
673 current_example: list[str] = []
675 for line in text.split("\n"):
676 if _is_empty_line(line):
677 if in_code_example:
678 if current_example: 678 ↛ 681line 678 didn't jump to line 681 because the condition on line 678 was always true
679 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
680 current_example = []
681 in_code_example = False
682 else:
683 current_text.append(line)
685 elif in_code_example:
686 if trim_doctest_flags:
687 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
688 line = _RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901
689 current_example.append(line)
691 elif line.startswith("```"):
692 in_code_block = not in_code_block
693 current_text.append(line)
695 elif in_code_block:
696 current_text.append(line)
698 elif line.startswith(">>>"):
699 if current_text: 699 ↛ 702line 699 didn't jump to line 702 because the condition on line 699 was always true
700 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
701 current_text = []
702 in_code_example = True
704 if trim_doctest_flags:
705 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
706 current_example.append(line)
708 else:
709 current_text.append(line)
711 if current_text:
712 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
713 elif current_example: 713 ↛ 716line 713 didn't jump to line 716 because the condition on line 713 was always true
714 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
716 if sub_sections: 716 ↛ 719line 716 didn't jump to line 719 because the condition on line 716 was always true
717 return DocstringSectionExamples(sub_sections), new_offset
719 docstring_warning(docstring, new_offset, f"Empty examples section at line {offset}")
720 return None, new_offset
723def _append_section(sections: list, current: list[str], admonition_title: str) -> None:
724 if admonition_title:
725 kind = admonition_title.lower().replace(" ", "-")
726 if kind in ("warnings", "notes"):
727 # NumpyDoc sections are pluralised but admonitions aren't.
728 # We can special-case these explicitly so that it renders
729 # as one would expect.
730 kind = kind[:-1]
731 sections.append(
732 DocstringSectionAdmonition(
733 kind=kind,
734 text="\n".join(current).rstrip("\n"),
735 title=admonition_title,
736 ),
737 )
738 elif current and any(current):
739 sections.append(DocstringSectionText("\n".join(current).rstrip("\n")))
742_section_reader = {
743 DocstringSectionKind.parameters: _read_parameters_section,
744 DocstringSectionKind.other_parameters: _read_other_parameters_section,
745 DocstringSectionKind.deprecated: _read_deprecated_section,
746 DocstringSectionKind.raises: _read_raises_section,
747 DocstringSectionKind.warns: _read_warns_section,
748 DocstringSectionKind.examples: _read_examples_section,
749 DocstringSectionKind.attributes: _read_attributes_section,
750 DocstringSectionKind.functions: _read_functions_section,
751 DocstringSectionKind.classes: _read_classes_section,
752 DocstringSectionKind.modules: _read_modules_section,
753 DocstringSectionKind.returns: _read_returns_section,
754 DocstringSectionKind.yields: _read_yields_section,
755 DocstringSectionKind.receives: _read_receives_section,
756}
759def parse_numpy(
760 docstring: Docstring,
761 *,
762 ignore_init_summary: bool = False,
763 trim_doctest_flags: bool = True,
764 warn_unknown_params: bool = True,
765 **options: Any,
766) -> list[DocstringSection]:
767 """Parse a Numpydoc-style docstring.
769 This function iterates on lines of a docstring to build sections.
770 It then returns this list of sections.
772 Parameters:
773 docstring: The docstring to parse.
774 ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings.
775 trim_doctest_flags: Whether to remove doctest flags from Python example blocks.
776 warn_unknown_params: Warn about documented parameters not appearing in the signature.
777 **options: Additional parsing options.
779 Returns:
780 A list of docstring sections.
781 """
782 sections: list[DocstringSection] = []
783 current_section = []
784 admonition_title = ""
786 in_code_block = False
787 lines = docstring.lines
789 options = {
790 "trim_doctest_flags": trim_doctest_flags,
791 "ignore_init_summary": ignore_init_summary,
792 "warn_unknown_params": warn_unknown_params,
793 **options,
794 }
796 ignore_summary = (
797 options["ignore_init_summary"]
798 and docstring.parent is not None
799 and docstring.parent.name == "__init__"
800 and docstring.parent.is_function
801 and docstring.parent.parent is not None
802 and docstring.parent.parent.is_class
803 )
805 offset = 2 if ignore_summary else 0
807 while offset < len(lines):
808 line_lower = lines[offset].lower()
810 # Code blocks can contain dash lines that we must not interpret.
811 if in_code_block:
812 # End of code block.
813 if line_lower.lstrip(" ").startswith("```"):
814 in_code_block = False
815 # Lines in code block must not be interpreted in any way.
816 current_section.append(lines[offset])
818 # Start of code block.
819 elif line_lower.lstrip(" ").startswith("```"):
820 in_code_block = True
821 current_section.append(lines[offset])
823 # Dash lines after empty lines lose their meaning.
824 elif _is_empty_line(lines[offset]):
825 current_section.append("")
827 # End of the docstring, wrap up.
828 elif offset == len(lines) - 1:
829 current_section.append(lines[offset])
830 _append_section(sections, current_section, admonition_title)
831 admonition_title = ""
832 current_section = []
834 # Dash line after regular, non-empty line.
835 elif _is_dash_line(lines[offset + 1]):
836 # Finish reading current section.
837 _append_section(sections, current_section, admonition_title)
838 current_section = []
840 # Start parsing new (known) section.
841 if line_lower in _section_kind:
842 admonition_title = ""
843 reader = _section_reader[_section_kind[line_lower]]
844 section, offset = reader(docstring, offset=offset + 2, **options) # type: ignore[operator]
845 if section: 845 ↛ 857line 845 didn't jump to line 857 because the condition on line 845 was always true
846 sections.append(section)
848 # Start parsing admonition.
849 else:
850 admonition_title = lines[offset]
851 offset += 1 # skip next dash line
853 # Regular line.
854 else:
855 current_section.append(lines[offset])
857 offset += 1
859 # Finish current section.
860 _append_section(sections, current_section, admonition_title)
862 return sections