Coverage for src/griffe/_internal/docstrings/numpy.py: 69.33%
500 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-14 23:10 +0200
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-14 23:10 +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._internal.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 DocstringSectionTypeAliases,
51 DocstringSectionTypeParameters,
52 DocstringSectionWarns,
53 DocstringSectionYields,
54 DocstringTypeAlias,
55 DocstringTypeParameter,
56 DocstringWarn,
57 DocstringYield,
58)
59from griffe._internal.docstrings.utils import docstring_warning, parse_docstring_annotation
60from griffe._internal.enumerations import DocstringSectionKind, LogLevel
61from griffe._internal.expressions import ExprName
63if TYPE_CHECKING:
64 from re import Pattern
65 from typing import Any, Literal
67 from griffe._internal.expressions import Expr
68 from griffe._internal.models import Docstring
71_section_kind = {
72 "deprecated": DocstringSectionKind.deprecated,
73 "parameters": DocstringSectionKind.parameters,
74 "other parameters": DocstringSectionKind.other_parameters,
75 "type parameters": DocstringSectionKind.type_parameters,
76 "returns": DocstringSectionKind.returns,
77 "yields": DocstringSectionKind.yields,
78 "receives": DocstringSectionKind.receives,
79 "raises": DocstringSectionKind.raises,
80 "warns": DocstringSectionKind.warns,
81 "examples": DocstringSectionKind.examples,
82 "attributes": DocstringSectionKind.attributes,
83 "functions": DocstringSectionKind.functions,
84 "methods": DocstringSectionKind.functions,
85 "classes": DocstringSectionKind.classes,
86 "type aliases": DocstringSectionKind.type_aliases,
87 "modules": DocstringSectionKind.modules,
88}
91def _is_empty_line(line: str) -> bool:
92 return not line.strip()
95def _is_dash_line(line: str) -> bool:
96 return not _is_empty_line(line) and _is_empty_line(line.replace("-", ""))
99def _read_block_items(
100 docstring: Docstring,
101 *,
102 offset: int,
103 warnings: bool = True,
104 **options: Any, # noqa: ARG001
105) -> tuple[list[list[str]], int]:
106 lines = docstring.lines
107 if offset >= len(lines): 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 return [], offset
110 new_offset = offset
111 items: list[list[str]] = []
113 # skip first empty lines
114 while _is_empty_line(lines[new_offset]): 114 ↛ 115line 114 didn't jump to line 115 because the condition on line 114 was never true
115 new_offset += 1
117 # start processing first item
118 current_item = [lines[new_offset]]
119 new_offset += 1
121 # loop on next lines
122 while new_offset < len(lines):
123 line = lines[new_offset]
125 if _is_empty_line(line):
126 # empty line: preserve it in the current item
127 current_item.append("")
129 elif line.startswith(4 * " "):
130 # continuation line
131 current_item.append(line[4:])
133 elif line.startswith(" "): 133 ↛ 135line 133 didn't jump to line 135 because the condition on line 133 was never true
134 # indent between initial and continuation: append but warn
135 cont_indent = len(line) - len(line.lstrip())
136 current_item.append(line[cont_indent:])
137 if warnings:
138 docstring_warning(
139 docstring,
140 new_offset,
141 f"Confusing indentation for continuation line {new_offset + 1} in docstring, "
142 f"should be 4 spaces, not {cont_indent}",
143 )
145 elif new_offset + 1 < len(lines) and _is_dash_line(lines[new_offset + 1]):
146 # detect the start of a new section
147 break
149 else:
150 items.append(current_item)
151 current_item = [line]
153 new_offset += 1
155 if current_item: 155 ↛ 158line 155 didn't jump to line 158 because the condition on line 155 was always true
156 items.append(current_item)
158 return items, new_offset - 1
161def _read_block(docstring: Docstring, *, offset: int, **options: Any) -> tuple[str, int]: # noqa: ARG001
162 lines = docstring.lines
163 if offset >= len(lines): 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 return "", offset
166 new_offset = offset
167 block: list[str] = []
169 # skip first empty lines
170 while _is_empty_line(lines[new_offset]):
171 new_offset += 1
172 while new_offset < len(lines):
173 is_empty = _is_empty_line(lines[new_offset])
174 if is_empty and new_offset < len(lines) - 1 and _is_dash_line(lines[new_offset + 1]):
175 break # Break if a new unnamed section is reached.
177 if is_empty and new_offset < len(lines) - 2 and _is_dash_line(lines[new_offset + 2]):
178 break # Break if a new named section is reached.
180 block.append(lines[new_offset])
181 new_offset += 1
183 return "\n".join(block).rstrip("\n"), new_offset - 1
186_RE_OB: str = r"\{" # Opening bracket.
187_RE_CB: str = r"\}" # Closing bracket.
188_RE_NAME: str = r"\*{0,2}[_a-z][_a-z0-9]*"
189_RE_TYPE: str = r".+"
190_RE_RETURNS: Pattern = re.compile(
191 rf"""
192 (?:
193 (?P<nt_name>{_RE_NAME})\s*:\s*(?P<nt_type>{_RE_TYPE}) # name and type
194 | # or
195 (?P<name>{_RE_NAME})\s*:\s* # just name
196 | # or
197 \s*:\s*$ # no name, no type
198 | # or
199 (?::\s*)?(?P<type>{_RE_TYPE})\s* # just type
200 )
201 """,
202 re.IGNORECASE | re.VERBOSE,
203)
204_RE_YIELDS: Pattern = _RE_RETURNS
205_RE_RECEIVES: Pattern = _RE_RETURNS
206_RE_PARAMETER: Pattern = re.compile(
207 rf"""
208 (?P<names>{_RE_NAME}(?:,\s{_RE_NAME})*)
209 (?:
210 \s:\s
211 (?:
212 (?:{_RE_OB}(?P<choices>.+){_RE_CB})|
213 (?P<type>{_RE_TYPE})
214 )?
215 )?
216 """,
217 re.IGNORECASE | re.VERBOSE,
218)
219_RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$")
220_RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$")
223def _read_parameters(
224 docstring: Docstring,
225 *,
226 offset: int,
227 warn_unknown_params: bool = True,
228 warn_missing_types: bool = True,
229 warnings: bool = True,
230 **options: Any,
231) -> tuple[list[DocstringParameter], int]:
232 parameters: list[DocstringParameter] = []
233 annotation: str | Expr | None
235 items, new_offset = _read_block_items(docstring, offset=offset, **options)
237 for item in items:
238 match = _RE_PARAMETER.match(item[0])
239 if not match:
240 if warnings: 240 ↛ 242line 240 didn't jump to line 242 because the condition on line 240 was always true
241 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
242 continue
244 names = match.group("names").split(", ")
245 annotation = match.group("type") or None
246 choices = match.group("choices")
247 default = None
248 if choices:
249 annotation = choices
250 default = choices.split(", ", 1)[0]
251 elif annotation:
252 match = re.match(r"^(?P<annotation>.+),\s+default(?: |: |=)(?P<default>.+)$", annotation)
253 if match:
254 default = match.group("default")
255 annotation = match.group("annotation")
256 if annotation and annotation.endswith(", optional"):
257 annotation = annotation[:-10]
258 description = "\n".join(item[1:]).rstrip() if len(item) > 1 else ""
260 if annotation is None:
261 # try to use the annotation from the signature
262 for name in names:
263 with suppress(AttributeError, KeyError):
264 annotation = docstring.parent.parameters[name].annotation # type: ignore[union-attr]
265 break
266 else:
267 if warnings and warn_missing_types:
268 docstring_warning(docstring, new_offset, f"No types or annotations for parameters {names}")
269 else:
270 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
272 if default is None:
273 for name in names:
274 with suppress(AttributeError, KeyError):
275 default = docstring.parent.parameters[name].default # type: ignore[union-attr]
276 break
278 if warnings and warn_unknown_params:
279 with suppress(AttributeError): # For Parameters sections in objects without parameters.
280 params = docstring.parent.parameters # type: ignore[union-attr]
281 for name in names:
282 if name not in params:
283 message = f"Parameter '{name}' does not appear in the function signature"
284 for starred_name in (f"*{name}", f"**{name}"):
285 if starred_name in params: 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true
286 message += f". Did you mean '{starred_name}'?"
287 break
288 docstring_warning(docstring, new_offset, message)
290 parameters.extend(
291 DocstringParameter(name, value=default, annotation=annotation, description=description) for name in names
292 )
294 return parameters, new_offset
297def _read_parameters_section(
298 docstring: Docstring,
299 *,
300 offset: int,
301 warnings: bool = True,
302 **options: Any,
303) -> tuple[DocstringSectionParameters | None, int]:
304 parameters, new_offset = _read_parameters(docstring, offset=offset, warnings=warnings, **options)
305 if parameters: 305 ↛ 307line 305 didn't jump to line 307 because the condition on line 305 was always true
306 return DocstringSectionParameters(parameters), new_offset
307 if warnings:
308 docstring_warning(docstring, new_offset, f"Empty parameters section at line {offset}")
309 return None, new_offset
312def _read_other_parameters_section(
313 docstring: Docstring,
314 *,
315 offset: int,
316 warn_unknown_params: bool = True, # noqa: ARG001
317 warnings: bool = True,
318 **options: Any,
319) -> tuple[DocstringSectionOtherParameters | None, int]:
320 parameters, new_offset = _read_parameters(
321 docstring,
322 offset=offset,
323 warn_unknown_params=False,
324 warnings=warnings,
325 **options,
326 )
327 if parameters: 327 ↛ 329line 327 didn't jump to line 329 because the condition on line 327 was always true
328 return DocstringSectionOtherParameters(parameters), new_offset
329 if warnings:
330 docstring_warning(docstring, new_offset, f"Empty other parameters section at line {offset}")
331 return None, new_offset
334def _read_type_parameters_section(
335 docstring: Docstring,
336 *,
337 offset: int,
338 warn_unknown_params: bool = True,
339 **options: Any,
340) -> tuple[DocstringSectionTypeParameters | None, int]:
341 type_parameters: list[DocstringTypeParameter] = []
342 bound: str | Expr | None
344 items, new_offset = _read_block_items(docstring, offset=offset, **options)
346 for item in items:
347 match = _RE_PARAMETER.match(item[0])
348 if not match:
349 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
350 continue
352 names = match.group("names").split(", ")
353 bound = match.group("type") or None
354 choices = match.group("choices")
355 default = None
356 if choices:
357 bound = choices
358 default = choices.split(", ", 1)[0]
359 elif bound:
360 match = re.match(r"^(?P<annotation>.+),\s+default(?: |: |=)(?P<default>.+)$", bound)
361 if match:
362 default = match.group("default")
363 bound = match.group("annotation")
364 description = "\n".join(item[1:]).rstrip() if len(item) > 1 else ""
366 if bound is None:
367 # try to use the bound from the signature
368 for name in names:
369 with suppress(AttributeError, KeyError):
370 bound = docstring.parent.type_parameters[name].annotation # type: ignore[union-attr]
371 break
372 else:
373 bound = parse_docstring_annotation(bound, docstring, log_level=LogLevel.debug)
375 if default is None:
376 for name in names:
377 with suppress(AttributeError, KeyError):
378 default = docstring.parent.type_parameters[name].default # type: ignore[union-attr]
379 break
381 if warn_unknown_params:
382 with suppress(AttributeError): # for parameters sections in objects without parameters
383 type_params = docstring.parent.type_parameters # type: ignore[union-attr]
384 for name in names:
385 if name not in type_params:
386 message = f"Type parameter '{name}' does not appear in the {docstring.parent.kind} signature" # type: ignore[union-attr]
387 for starred_name in (f"*{name}", f"**{name}"):
388 if starred_name in type_params:
389 message += f". Did you mean '{starred_name}'?"
390 break
391 docstring_warning(docstring, new_offset, message)
393 type_parameters.extend(
394 DocstringTypeParameter(name, value=default, annotation=bound, description=description) for name in names
395 )
397 if type_parameters:
398 return DocstringSectionTypeParameters(type_parameters), new_offset
400 docstring_warning(docstring, new_offset, f"Empty type parameters section at line {offset}")
401 return None, new_offset
404def _read_deprecated_section(
405 docstring: Docstring,
406 *,
407 offset: int,
408 warnings: bool = True,
409 **options: Any,
410) -> tuple[DocstringSectionDeprecated | None, int]:
411 # deprecated
412 # SINCE_VERSION
413 # TEXT?
414 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
416 if not items: 416 ↛ 417line 416 didn't jump to line 417 because the condition on line 416 was never true
417 if warnings:
418 docstring_warning(docstring, new_offset, f"Empty deprecated section at line {offset}")
419 return None, new_offset
421 if warnings and len(items) > 1: 421 ↛ 422line 421 didn't jump to line 422 because the condition on line 421 was never true
422 docstring_warning(docstring, new_offset, f"Too many deprecated items at {offset}")
424 item = items[0]
425 version = item[0]
426 text = dedent("\n".join(item[1:]))
427 return DocstringSectionDeprecated(version=version, text=text), new_offset
430def _read_returns_section(
431 docstring: Docstring,
432 *,
433 offset: int,
434 warnings: bool = True,
435 **options: Any,
436) -> tuple[DocstringSectionReturns | None, int]:
437 # (NAME : )?TYPE
438 # TEXT?
439 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
441 if not items: 441 ↛ 442line 441 didn't jump to line 442 because the condition on line 441 was never true
442 if warnings:
443 docstring_warning(docstring, new_offset, f"Empty returns section at line {offset}")
444 return None, new_offset
446 returns = []
447 for index, item in enumerate(items):
448 match = _RE_RETURNS.match(item[0])
449 if not match: 449 ↛ 450line 449 didn't jump to line 450 because the condition on line 449 was never true
450 if warnings:
451 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
452 continue
454 groups = match.groupdict()
455 name = groups["nt_name"] or groups["name"]
456 annotation = groups["nt_type"] or groups["type"]
457 text = dedent("\n".join(item[1:]))
458 if annotation is None:
459 # try to retrieve the annotation from the docstring parent
460 with suppress(AttributeError, KeyError, ValueError):
461 if docstring.parent.is_function: # type: ignore[union-attr] 461 ↛ 463line 461 didn't jump to line 463 because the condition on line 461 was always true
462 annotation = docstring.parent.returns # type: ignore[union-attr]
463 elif docstring.parent.is_attribute: # type: ignore[union-attr]
464 annotation = docstring.parent.annotation # type: ignore[union-attr]
465 else:
466 raise ValueError
467 if len(items) > 1: 467 ↛ 485line 467 didn't jump to line 485
468 if annotation.is_tuple: 468 ↛ 469line 468 didn't jump to line 469 because the condition on line 468 was never true
469 annotation = annotation.slice.elements[index]
470 else:
471 if annotation.is_iterator: 471 ↛ 472line 471 didn't jump to line 472 because the condition on line 471 was never true
472 return_item = annotation.slice
473 elif annotation.is_generator: 473 ↛ 476line 473 didn't jump to line 476 because the condition on line 473 was always true
474 return_item = annotation.slice.elements[2]
475 else:
476 raise ValueError
477 if isinstance(return_item, ExprName): 477 ↛ 478line 477 didn't jump to line 478 because the condition on line 477 was never true
478 annotation = return_item
479 elif return_item.is_tuple: 479 ↛ 482line 479 didn't jump to line 482 because the condition on line 479 was always true
480 annotation = return_item.slice.elements[index]
481 else:
482 annotation = return_item
483 else:
484 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
485 returns.append(DocstringReturn(name=name or "", annotation=annotation, description=text))
486 return DocstringSectionReturns(returns), new_offset
489def _read_yields_section(
490 docstring: Docstring,
491 *,
492 offset: int,
493 warnings: bool = True,
494 **options: Any,
495) -> tuple[DocstringSectionYields | None, int]:
496 # yields
497 # (NAME : )?TYPE
498 # TEXT?
499 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
501 if not items: 501 ↛ 502line 501 didn't jump to line 502 because the condition on line 501 was never true
502 if warnings:
503 docstring_warning(docstring, new_offset, f"Empty yields section at line {offset}")
504 return None, new_offset
506 yields = []
507 for index, item in enumerate(items):
508 match = _RE_YIELDS.match(item[0])
509 if not match: 509 ↛ 510line 509 didn't jump to line 510 because the condition on line 509 was never true
510 if warnings:
511 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
512 continue
514 groups = match.groupdict()
515 name = groups["nt_name"] or groups["name"]
516 annotation = groups["nt_type"] or groups["type"]
517 text = dedent("\n".join(item[1:]))
518 if annotation is None:
519 # try to retrieve the annotation from the docstring parent
520 with suppress(AttributeError, IndexError, KeyError, ValueError):
521 annotation = docstring.parent.annotation # type: ignore[union-attr]
522 if annotation.is_iterator:
523 yield_item = annotation.slice
524 elif annotation.is_generator: 524 ↛ 527line 524 didn't jump to line 527 because the condition on line 524 was always true
525 yield_item = annotation.slice.elements[0]
526 else:
527 raise ValueError
528 if isinstance(yield_item, ExprName):
529 annotation = yield_item
530 elif yield_item.is_tuple: 530 ↛ 533line 530 didn't jump to line 533 because the condition on line 530 was always true
531 annotation = yield_item.slice.elements[index]
532 else:
533 annotation = yield_item
534 else:
535 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
536 yields.append(DocstringYield(name=name or "", annotation=annotation, description=text))
537 return DocstringSectionYields(yields), new_offset
540def _read_receives_section(
541 docstring: Docstring,
542 *,
543 offset: int,
544 warnings: bool = True,
545 **options: Any,
546) -> tuple[DocstringSectionReceives | None, int]:
547 # receives
548 # (NAME : )?TYPE
549 # TEXT?
550 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
552 if not items: 552 ↛ 553line 552 didn't jump to line 553 because the condition on line 552 was never true
553 if warnings:
554 docstring_warning(docstring, new_offset, f"Empty receives section at line {offset}")
555 return None, new_offset
557 receives = []
558 for index, item in enumerate(items):
559 match = _RE_RECEIVES.match(item[0])
560 if not match: 560 ↛ 561line 560 didn't jump to line 561 because the condition on line 560 was never true
561 if warnings:
562 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
563 continue
565 groups = match.groupdict()
566 name = groups["nt_name"] or groups["name"]
567 annotation = groups["nt_type"] or groups["type"]
568 text = dedent("\n".join(item[1:]))
569 if annotation is None:
570 # try to retrieve the annotation from the docstring parent
571 with suppress(AttributeError, KeyError):
572 annotation = docstring.parent.returns # type: ignore[union-attr]
573 if annotation.is_generator: 573 ↛ 583line 573 didn't jump to line 583
574 receives_item = annotation.slice.elements[1]
575 if isinstance(receives_item, ExprName):
576 annotation = receives_item
577 elif receives_item.is_tuple: 577 ↛ 580line 577 didn't jump to line 580 because the condition on line 577 was always true
578 annotation = receives_item.slice.elements[index]
579 else:
580 annotation = receives_item
581 else:
582 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
583 receives.append(DocstringReceive(name=name or "", annotation=annotation, description=text))
584 return DocstringSectionReceives(receives), new_offset
587def _read_raises_section(
588 docstring: Docstring,
589 *,
590 offset: int,
591 warnings: bool = True,
592 **options: Any,
593) -> tuple[DocstringSectionRaises | None, int]:
594 # raises
595 # EXCEPTION
596 # TEXT?
597 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
599 if not items: 599 ↛ 600line 599 didn't jump to line 600 because the condition on line 599 was never true
600 if warnings:
601 docstring_warning(docstring, new_offset, f"Empty raises section at line {offset}")
602 return None, new_offset
604 raises = []
605 for item in items:
606 annotation = parse_docstring_annotation(item[0], docstring)
607 text = dedent("\n".join(item[1:]))
608 raises.append(DocstringRaise(annotation=annotation, description=text))
609 return DocstringSectionRaises(raises), new_offset
612def _read_warns_section(
613 docstring: Docstring,
614 *,
615 offset: int,
616 warnings: bool = True,
617 **options: Any,
618) -> tuple[DocstringSectionWarns | None, int]:
619 # warns
620 # WARNING
621 # TEXT?
622 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
624 if not items: 624 ↛ 625line 624 didn't jump to line 625 because the condition on line 624 was never true
625 if warnings:
626 docstring_warning(docstring, new_offset, f"Empty warns section at line {offset}")
627 return None, new_offset
629 warns = []
630 for item in items:
631 annotation = parse_docstring_annotation(item[0], docstring)
632 text = dedent("\n".join(item[1:]))
633 warns.append(DocstringWarn(annotation=annotation, description=text))
634 return DocstringSectionWarns(warns), new_offset
637def _read_attributes_section(
638 docstring: Docstring,
639 *,
640 offset: int,
641 warnings: bool = True,
642 **options: Any,
643) -> tuple[DocstringSectionAttributes | None, int]:
644 # attributes (for classes)
645 # NAME( : TYPE)?
646 # TEXT?
647 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
649 if not items: 649 ↛ 650line 649 didn't jump to line 650 because the condition on line 649 was never true
650 if warnings:
651 docstring_warning(docstring, new_offset, f"Empty attributes section at line {offset}")
652 return None, new_offset
654 annotation: str | Expr | None
655 attributes = []
656 for item in items:
657 name_type = item[0]
658 if ":" in name_type:
659 name, annotation = name_type.split(":", 1)
660 name = name.strip()
661 annotation = annotation.strip() or None
662 else:
663 name = name_type
664 annotation = None
665 if annotation is None:
666 with suppress(AttributeError, KeyError, TypeError):
667 # Use subscript syntax to fetch annotation from inherited members too.
668 annotation = docstring.parent[name].annotation # type: ignore[index]
669 else:
670 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug)
671 text = dedent("\n".join(item[1:]))
672 attributes.append(DocstringAttribute(name=name, annotation=annotation, description=text))
673 return DocstringSectionAttributes(attributes), new_offset
676def _read_functions_section(
677 docstring: Docstring,
678 *,
679 offset: int,
680 warnings: bool = True,
681 **options: Any,
682) -> tuple[DocstringSectionFunctions | None, int]:
683 # SIGNATURE
684 # TEXT?
685 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
687 if not items: 687 ↛ 688line 687 didn't jump to line 688 because the condition on line 687 was never true
688 if warnings:
689 docstring_warning(docstring, new_offset, f"Empty functions/methods section at line {offset}")
690 return None, new_offset
692 functions = []
693 signature: str | Expr | None
694 for item in items:
695 name_signature = item[0]
696 if "(" in name_signature:
697 name = name_signature.split("(", 1)[0]
698 name = name.strip()
699 signature = name_signature.strip()
700 else:
701 name = name_signature
702 signature = None
703 text = dedent("\n".join(item[1:])).strip()
704 functions.append(DocstringFunction(name=name, annotation=signature, description=text))
705 return DocstringSectionFunctions(functions), new_offset
708def _read_classes_section(
709 docstring: Docstring,
710 *,
711 offset: int,
712 warnings: bool = True,
713 **options: Any,
714) -> tuple[DocstringSectionClasses | None, int]:
715 # SIGNATURE
716 # TEXT?
717 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
719 if not items: 719 ↛ 720line 719 didn't jump to line 720 because the condition on line 719 was never true
720 if warnings:
721 docstring_warning(docstring, new_offset, f"Empty classes section at line {offset}")
722 return None, new_offset
724 classes = []
725 signature: str | Expr | None
726 for item in items:
727 name_signature = item[0]
728 if "(" in name_signature:
729 name = name_signature.split("(", 1)[0]
730 name = name.strip()
731 signature = name_signature.strip()
732 else:
733 name = name_signature
734 signature = None
735 text = dedent("\n".join(item[1:])).strip()
736 classes.append(DocstringClass(name=name, annotation=signature, description=text))
737 return DocstringSectionClasses(classes), new_offset
740def _read_type_aliases_section(
741 docstring: Docstring,
742 *,
743 offset: int,
744 **options: Any,
745) -> tuple[DocstringSectionTypeAliases | None, int]:
746 items, new_offset = _read_block_items(docstring, offset=offset, **options)
748 if not items:
749 docstring_warning(docstring, new_offset, f"Empty type aliases section at line {offset}")
750 return None, new_offset
752 type_aliases = []
753 for item in items:
754 name = item[0]
755 text = dedent("\n".join(item[1:])).strip()
756 type_aliases.append(DocstringTypeAlias(name=name, description=text))
757 return DocstringSectionTypeAliases(type_aliases), new_offset
760def _read_modules_section(
761 docstring: Docstring,
762 *,
763 offset: int,
764 warnings: bool = True,
765 **options: Any,
766) -> tuple[DocstringSectionModules | None, int]:
767 # NAME
768 # TEXT?
769 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
771 if not items: 771 ↛ 772line 771 didn't jump to line 772 because the condition on line 771 was never true
772 if warnings:
773 docstring_warning(docstring, new_offset, f"Empty modules section at line {offset}")
774 return None, new_offset
776 modules = []
777 signature: str | Expr | None
778 for item in items:
779 name_signature = item[0]
780 if "(" in name_signature: 780 ↛ 781line 780 didn't jump to line 781 because the condition on line 780 was never true
781 name = name_signature.split("(", 1)[0]
782 name = name.strip()
783 signature = name_signature.strip()
784 else:
785 name = name_signature
786 signature = None
787 text = dedent("\n".join(item[1:])).strip()
788 modules.append(DocstringModule(name=name, annotation=signature, description=text))
789 return DocstringSectionModules(modules), new_offset
792def _read_examples_section(
793 docstring: Docstring,
794 *,
795 offset: int,
796 trim_doctest_flags: bool = True,
797 warnings: bool = True,
798 **options: Any,
799) -> tuple[DocstringSectionExamples | None, int]:
800 text, new_offset = _read_block(docstring, offset=offset, warnings=warnings, **options)
802 sub_sections: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = []
803 in_code_example = False
804 in_code_block = False
805 current_text: list[str] = []
806 current_example: list[str] = []
808 for line in text.split("\n"):
809 if _is_empty_line(line):
810 if in_code_example:
811 if current_example: 811 ↛ 814line 811 didn't jump to line 814 because the condition on line 811 was always true
812 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
813 current_example = []
814 in_code_example = False
815 else:
816 current_text.append(line)
818 elif in_code_example:
819 if trim_doctest_flags:
820 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
821 line = _RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901
822 current_example.append(line)
824 elif line.startswith("```"):
825 in_code_block = not in_code_block
826 current_text.append(line)
828 elif in_code_block:
829 current_text.append(line)
831 elif line.startswith(">>>"):
832 if current_text: 832 ↛ 835line 832 didn't jump to line 835 because the condition on line 832 was always true
833 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
834 current_text = []
835 in_code_example = True
837 if trim_doctest_flags:
838 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
839 current_example.append(line)
841 else:
842 current_text.append(line)
844 if current_text:
845 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
846 elif current_example: 846 ↛ 849line 846 didn't jump to line 849 because the condition on line 846 was always true
847 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
849 if sub_sections: 849 ↛ 852line 849 didn't jump to line 852 because the condition on line 849 was always true
850 return DocstringSectionExamples(sub_sections), new_offset
852 if warnings:
853 docstring_warning(docstring, new_offset, f"Empty examples section at line {offset}")
854 return None, new_offset
857def _append_section(sections: list, current: list[str], admonition_title: str) -> None:
858 if admonition_title:
859 kind = admonition_title.lower().replace(" ", "-")
860 if kind in ("warnings", "notes"):
861 # NumpyDoc sections are pluralised but admonitions aren't.
862 # We can special-case these explicitly so that it renders
863 # as one would expect.
864 kind = kind[:-1]
865 sections.append(
866 DocstringSectionAdmonition(
867 kind=kind,
868 text="\n".join(current).rstrip("\n"),
869 title=admonition_title,
870 ),
871 )
872 elif current and any(current):
873 sections.append(DocstringSectionText("\n".join(current).rstrip("\n")))
876_section_reader = {
877 DocstringSectionKind.parameters: _read_parameters_section,
878 DocstringSectionKind.other_parameters: _read_other_parameters_section,
879 DocstringSectionKind.type_parameters: _read_type_parameters_section,
880 DocstringSectionKind.deprecated: _read_deprecated_section,
881 DocstringSectionKind.raises: _read_raises_section,
882 DocstringSectionKind.warns: _read_warns_section,
883 DocstringSectionKind.examples: _read_examples_section,
884 DocstringSectionKind.attributes: _read_attributes_section,
885 DocstringSectionKind.functions: _read_functions_section,
886 DocstringSectionKind.classes: _read_classes_section,
887 DocstringSectionKind.type_aliases: _read_type_aliases_section,
888 DocstringSectionKind.modules: _read_modules_section,
889 DocstringSectionKind.returns: _read_returns_section,
890 DocstringSectionKind.yields: _read_yields_section,
891 DocstringSectionKind.receives: _read_receives_section,
892}
895def parse_numpy(
896 docstring: Docstring,
897 *,
898 ignore_init_summary: bool = False,
899 trim_doctest_flags: bool = True,
900 warn_unknown_params: bool = True,
901 warn_missing_types: bool = True,
902 warnings: bool = True,
903 **options: Any,
904) -> list[DocstringSection]:
905 """Parse a Numpydoc-style docstring.
907 This function iterates on lines of a docstring to build sections.
908 It then returns this list of sections.
910 Parameters:
911 docstring: The docstring to parse.
912 ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings.
913 trim_doctest_flags: Whether to remove doctest flags from Python example blocks.
914 warn_unknown_params: Warn about documented parameters not appearing in the signature.
915 warn_missing_types: Warn about missing types/annotations for parameters, return values, etc.
916 warnings: Whether to log warnings at all.
917 **options: Additional parsing options.
919 Returns:
920 A list of docstring sections.
921 """
922 sections: list[DocstringSection] = []
923 current_section = []
924 admonition_title = ""
926 in_code_block = False
927 lines = docstring.lines
929 options = {
930 "trim_doctest_flags": trim_doctest_flags,
931 "ignore_init_summary": ignore_init_summary,
932 "warn_unknown_params": warn_unknown_params,
933 "warn_missing_types": warn_missing_types,
934 "warnings": warnings,
935 **options,
936 }
938 ignore_summary = (
939 options["ignore_init_summary"]
940 and docstring.parent is not None
941 and docstring.parent.name == "__init__"
942 and docstring.parent.is_function
943 and docstring.parent.parent is not None
944 and docstring.parent.parent.is_class
945 )
947 offset = 2 if ignore_summary else 0
949 while offset < len(lines):
950 line_lower = lines[offset].lower()
952 # Code blocks can contain dash lines that we must not interpret.
953 if in_code_block:
954 # End of code block.
955 if line_lower.lstrip(" ").startswith("```"):
956 in_code_block = False
957 # Lines in code block must not be interpreted in any way.
958 current_section.append(lines[offset])
960 # Start of code block.
961 elif line_lower.lstrip(" ").startswith("```"):
962 in_code_block = True
963 current_section.append(lines[offset])
965 # Dash lines after empty lines lose their meaning.
966 elif _is_empty_line(lines[offset]):
967 current_section.append("")
969 # End of the docstring, wrap up.
970 elif offset == len(lines) - 1:
971 current_section.append(lines[offset])
972 _append_section(sections, current_section, admonition_title)
973 admonition_title = ""
974 current_section = []
976 # Dash line after regular, non-empty line.
977 elif _is_dash_line(lines[offset + 1]):
978 # Finish reading current section.
979 _append_section(sections, current_section, admonition_title)
980 current_section = []
982 # Start parsing new (known) section.
983 if line_lower in _section_kind:
984 admonition_title = ""
985 reader = _section_reader[_section_kind[line_lower]]
986 section, offset = reader(docstring, offset=offset + 2, **options) # type: ignore[operator]
987 if section: 987 ↛ 999line 987 didn't jump to line 999 because the condition on line 987 was always true
988 sections.append(section)
990 # Start parsing admonition.
991 else:
992 admonition_title = lines[offset]
993 offset += 1 # Skip next dash line.
995 # Regular line.
996 else:
997 current_section.append(lines[offset])
999 offset += 1
1001 # Finish current section.
1002 _append_section(sections, current_section, admonition_title)
1004 return sections