Coverage for src/griffe/_internal/docstrings/google.py: 86.37%
461 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 Google-style docstrings into structured data.
3from __future__ import annotations
5import re
6from contextlib import suppress
7from typing import TYPE_CHECKING
9from griffe._internal.docstrings.models import (
10 DocstringAttribute,
11 DocstringClass,
12 DocstringFunction,
13 DocstringModule,
14 DocstringParameter,
15 DocstringRaise,
16 DocstringReceive,
17 DocstringReturn,
18 DocstringSection,
19 DocstringSectionAdmonition,
20 DocstringSectionAttributes,
21 DocstringSectionClasses,
22 DocstringSectionExamples,
23 DocstringSectionFunctions,
24 DocstringSectionModules,
25 DocstringSectionOtherParameters,
26 DocstringSectionParameters,
27 DocstringSectionRaises,
28 DocstringSectionReceives,
29 DocstringSectionReturns,
30 DocstringSectionText,
31 DocstringSectionTypeAliases,
32 DocstringSectionTypeParameters,
33 DocstringSectionWarns,
34 DocstringSectionYields,
35 DocstringTypeAlias,
36 DocstringTypeParameter,
37 DocstringWarn,
38 DocstringYield,
39)
40from griffe._internal.docstrings.utils import docstring_warning, parse_docstring_annotation
41from griffe._internal.enumerations import DocstringSectionKind, LogLevel
43if TYPE_CHECKING:
44 from re import Pattern
45 from typing import Any, Literal
47 from griffe._internal.expressions import Expr
48 from griffe._internal.models import Docstring
51_section_kind = {
52 "args": DocstringSectionKind.parameters,
53 "arguments": DocstringSectionKind.parameters,
54 "params": DocstringSectionKind.parameters,
55 "parameters": DocstringSectionKind.parameters,
56 "keyword args": DocstringSectionKind.other_parameters,
57 "keyword arguments": DocstringSectionKind.other_parameters,
58 "other args": DocstringSectionKind.other_parameters,
59 "other arguments": DocstringSectionKind.other_parameters,
60 "other params": DocstringSectionKind.other_parameters,
61 "other parameters": DocstringSectionKind.other_parameters,
62 "type args": DocstringSectionKind.type_parameters,
63 "type arguments": DocstringSectionKind.type_parameters,
64 "type params": DocstringSectionKind.type_parameters,
65 "type parameters": DocstringSectionKind.type_parameters,
66 "raises": DocstringSectionKind.raises,
67 "exceptions": DocstringSectionKind.raises,
68 "returns": DocstringSectionKind.returns,
69 "yields": DocstringSectionKind.yields,
70 "receives": DocstringSectionKind.receives,
71 "examples": DocstringSectionKind.examples,
72 "attributes": DocstringSectionKind.attributes,
73 "functions": DocstringSectionKind.functions,
74 "methods": DocstringSectionKind.functions,
75 "classes": DocstringSectionKind.classes,
76 "type aliases": DocstringSectionKind.type_aliases,
77 "modules": DocstringSectionKind.modules,
78 "warns": DocstringSectionKind.warns,
79 "warnings": DocstringSectionKind.warns,
80}
82_BlockItem = tuple[int, list[str]]
83_BlockItems = list[_BlockItem]
84_ItemsBlock = tuple[_BlockItems, int]
86_RE_ADMONITION: Pattern = re.compile(r"^(?P<type>[\w][\s\w-]*):(\s+(?P<title>[^\s].*))?\s*$", re.IGNORECASE)
87_RE_NAME_ANNOTATION_DESCRIPTION: Pattern = re.compile(r"^(?:(?P<name>\w+)?\s*(?:\((?P<type>.+)\))?:\s*)?(?P<desc>.*)$")
88_RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$")
89_RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$")
92def _read_block_items(docstring: Docstring, *, offset: int, warnings: bool = True, **options: Any) -> _ItemsBlock: # noqa: ARG001
93 lines = docstring.lines
94 if offset >= len(lines): 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 return [], offset
97 new_offset = offset
98 items: _BlockItems = []
100 # Skip first empty lines.
101 while _is_empty_line(lines[new_offset]): 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true
102 new_offset += 1
104 # Get initial indent.
105 indent = len(lines[new_offset]) - len(lines[new_offset].lstrip())
107 if indent == 0: 107 ↛ 109line 107 didn't jump to line 109 because the condition on line 107 was never true
108 # First non-empty line was not indented, abort.
109 return [], new_offset - 1
111 # Start processing first item.
112 current_item = (new_offset, [lines[new_offset][indent:]])
113 new_offset += 1
115 # Loop on next lines.
116 while new_offset < len(lines):
117 line = lines[new_offset]
119 if _is_empty_line(line):
120 # Empty line: preserve it in the current item.
121 current_item[1].append("")
123 elif line.startswith(indent * 2 * " "):
124 # Continuation line.
125 current_item[1].append(line[indent * 2 :])
127 elif line.startswith((indent + 1) * " "):
128 # Indent between initial and continuation: append but warn.
129 cont_indent = len(line) - len(line.lstrip())
130 current_item[1].append(line[cont_indent:])
131 if warnings: 131 ↛ 148line 131 didn't jump to line 148 because the condition on line 131 was always true
132 docstring_warning(
133 docstring,
134 new_offset,
135 f"Confusing indentation for continuation line {new_offset + 1} in docstring, "
136 f"should be {indent} * 2 = {indent * 2} spaces, not {cont_indent}",
137 )
139 elif line.startswith(indent * " "):
140 # Indent equal to initial one: new item.
141 items.append(current_item)
142 current_item = (new_offset, [line[indent:]])
144 else:
145 # Indent lower than initial one: end of section.
146 break
148 new_offset += 1
150 if current_item: 150 ↛ 153line 150 didn't jump to line 153 because the condition on line 150 was always true
151 items.append(current_item)
153 return items, new_offset - 1
156def _read_block(docstring: Docstring, *, offset: int, **options: Any) -> tuple[str, int]: # noqa: ARG001
157 lines = docstring.lines
158 if offset >= len(lines): 158 ↛ 159line 158 didn't jump to line 159 because the condition on line 158 was never true
159 return "", offset - 1
161 new_offset = offset
162 block: list[str] = []
164 # skip first empty lines.
165 while _is_empty_line(lines[new_offset]): 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 new_offset += 1
168 # Get initial indent.
169 indent = len(lines[new_offset]) - len(lines[new_offset].lstrip())
171 if indent == 0: 171 ↛ 173line 171 didn't jump to line 173 because the condition on line 171 was never true
172 # First non-empty line was not indented, abort.
173 return "", offset - 1
175 # Start processing first item.
176 block.append(lines[new_offset].lstrip())
177 new_offset += 1
179 # Loop on next lines.
180 while new_offset < len(lines) and (lines[new_offset].startswith(indent * " ") or _is_empty_line(lines[new_offset])):
181 block.append(lines[new_offset][indent:])
182 new_offset += 1
184 return "\n".join(block).rstrip("\n"), new_offset - 1
187def _read_parameters(
188 docstring: Docstring,
189 *,
190 offset: int,
191 warn_unknown_params: bool = True,
192 warn_missing_types: bool = True,
193 warnings: bool = True,
194 **options: Any,
195) -> tuple[list[DocstringParameter], int]:
196 parameters = []
197 annotation: str | Expr | None
199 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
201 for line_number, param_lines in block:
202 # Check the presence of a name and description, separated by a colon.
203 try:
204 name_with_type, description = param_lines[0].split(":", 1)
205 except ValueError:
206 if warnings: 206 ↛ 212line 206 didn't jump to line 212 because the condition on line 206 was always true
207 docstring_warning(
208 docstring,
209 line_number,
210 f"Failed to get 'name: description' pair from '{param_lines[0]}'",
211 )
212 continue
214 description = "\n".join([description.lstrip(), *param_lines[1:]]).rstrip("\n")
216 # Use the type given after the parameter name, if any.
217 if " " in name_with_type:
218 name, annotation = name_with_type.split(" ", 1)
219 annotation = annotation.strip("()")
220 annotation = annotation.removesuffix(", optional")
221 # Try to compile the annotation to transform it into an expression.
222 annotation = parse_docstring_annotation(annotation, docstring)
223 else:
224 name = name_with_type
225 # Try to use the annotation from the signature.
226 try:
227 annotation = docstring.parent.parameters[name].annotation # type: ignore[union-attr]
228 except (AttributeError, KeyError):
229 annotation = None
231 try:
232 default = docstring.parent.parameters[name].default # type: ignore[union-attr]
233 except (AttributeError, KeyError):
234 default = None
236 if warnings and warn_missing_types and annotation is None:
237 docstring_warning(docstring, line_number, f"No type or annotation for parameter '{name}'")
239 if warnings and warn_unknown_params:
240 with suppress(AttributeError): # For Parameters sections in objects without parameters.
241 params = docstring.parent.parameters # type: ignore[union-attr]
242 if name not in params:
243 message = f"Parameter '{name}' does not appear in the function signature"
244 for starred_name in (f"*{name}", f"**{name}"):
245 if starred_name in params: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 message += f". Did you mean '{starred_name}'?"
247 break
248 docstring_warning(docstring, line_number, message)
250 parameters.append(DocstringParameter(name=name, value=default, annotation=annotation, description=description))
252 return parameters, new_offset
255def _read_parameters_section(
256 docstring: Docstring,
257 *,
258 offset: int,
259 **options: Any,
260) -> tuple[DocstringSectionParameters | None, int]:
261 parameters, new_offset = _read_parameters(docstring, offset=offset, **options)
262 return DocstringSectionParameters(parameters), new_offset
265def _read_other_parameters_section(
266 docstring: Docstring,
267 *,
268 offset: int,
269 warn_unknown_params: bool = True, # noqa: ARG001
270 **options: Any,
271) -> tuple[DocstringSectionOtherParameters | None, int]:
272 parameters, new_offset = _read_parameters(docstring, offset=offset, warn_unknown_params=False, **options)
273 return DocstringSectionOtherParameters(parameters), new_offset
276def _read_type_parameters_section(
277 docstring: Docstring,
278 *,
279 offset: int,
280 warn_unknown_params: bool = True,
281 **options: Any,
282) -> tuple[DocstringSectionTypeParameters | None, int]:
283 type_parameters = []
284 bound: str | Expr | None
286 block, new_offset = _read_block_items(docstring, offset=offset, **options)
288 for line_number, type_param_lines in block:
289 # check the presence of a name and description, separated by a colon
290 try:
291 name_with_bound, description = type_param_lines[0].split(":", 1)
292 except ValueError:
293 docstring_warning(
294 docstring,
295 line_number,
296 f"Failed to get 'name: description' pair from '{type_param_lines[0]}'",
297 )
298 continue
300 description = "\n".join([description.lstrip(), *type_param_lines[1:]]).rstrip("\n")
302 # use the type given after the type parameter name, if any
303 if " " in name_with_bound:
304 name, bound = name_with_bound.split(" ", 1)
305 if bound.startswith("(") and bound.endswith(")"):
306 bound = bound[1:-1]
307 # try to compile the annotation to transform it into an expression
308 bound = parse_docstring_annotation(bound, docstring)
309 else:
310 name = name_with_bound
311 # try to use the annotation from the signature
312 try:
313 bound = docstring.parent.type_parameters[name].annotation # type: ignore[union-attr]
314 except (AttributeError, KeyError):
315 bound = None
317 try:
318 default = docstring.parent.type_parameters[name].default # type: ignore[union-attr]
319 except (AttributeError, KeyError):
320 default = None
322 if warn_unknown_params: 322 ↛ 333line 322 didn't jump to line 333 because the condition on line 322 was always true
323 with suppress(AttributeError): # for type parameters sections in objects without type parameters
324 type_params = docstring.parent.type_parameters # type: ignore[union-attr]
325 if name not in type_params:
326 message = f"Type parameter '{name}' does not appear in the {docstring.parent.kind.value} signature" # type: ignore[union-attr]
327 for starred_name in (f"*{name}", f"**{name}"):
328 if starred_name in type_params: 328 ↛ 329line 328 didn't jump to line 329 because the condition on line 328 was never true
329 message += f". Did you mean '{starred_name}'?"
330 break
331 docstring_warning(docstring, line_number, message)
333 type_parameters.append(
334 DocstringTypeParameter(
335 name=name,
336 value=default,
337 annotation=bound,
338 description=description,
339 ),
340 )
342 return DocstringSectionTypeParameters(type_parameters), new_offset
345def _read_attributes_section(
346 docstring: Docstring,
347 *,
348 offset: int,
349 warnings: bool = True,
350 **options: Any,
351) -> tuple[DocstringSectionAttributes | None, int]:
352 attributes = []
353 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
355 annotation: str | Expr | None = None
356 for line_number, attr_lines in block:
357 try:
358 name_with_type, description = attr_lines[0].split(":", 1)
359 except ValueError:
360 if warnings:
361 docstring_warning(
362 docstring,
363 line_number,
364 f"Failed to get 'name: description' pair from '{attr_lines[0]}'",
365 )
366 continue
368 description = "\n".join([description.lstrip(), *attr_lines[1:]]).rstrip("\n")
370 if " " in name_with_type: 370 ↛ 371line 370 didn't jump to line 371 because the condition on line 370 was never true
371 name, annotation = name_with_type.split(" ", 1)
372 annotation = annotation.strip("()")
373 annotation = annotation.removesuffix(", optional")
374 # Try to compile the annotation to transform it into an expression.
375 annotation = parse_docstring_annotation(annotation, docstring)
376 else:
377 name = name_with_type
378 with suppress(AttributeError, KeyError, TypeError):
379 # Use subscript syntax to fetch annotation from inherited members too.
380 annotation = docstring.parent[name].annotation # type: ignore[index]
382 attributes.append(DocstringAttribute(name=name, annotation=annotation, description=description))
384 return DocstringSectionAttributes(attributes), new_offset
387def _read_functions_section(
388 docstring: Docstring,
389 *,
390 offset: int,
391 warnings: bool = True,
392 **options: Any,
393) -> tuple[DocstringSectionFunctions | None, int]:
394 functions = []
395 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
397 signature: str | Expr | None = None
398 for line_number, func_lines in block:
399 try:
400 name_with_signature, description = func_lines[0].split(":", 1)
401 except ValueError:
402 if warnings:
403 docstring_warning(
404 docstring,
405 line_number,
406 f"Failed to get 'signature: description' pair from '{func_lines[0]}'",
407 )
408 continue
410 description = "\n".join([description.lstrip(), *func_lines[1:]]).rstrip("\n")
412 if "(" in name_with_signature:
413 name = name_with_signature.split("(", 1)[0]
414 signature = name_with_signature
415 else:
416 name = name_with_signature
417 signature = None
419 functions.append(DocstringFunction(name=name, annotation=signature, description=description))
421 return DocstringSectionFunctions(functions), new_offset
424def _read_classes_section(
425 docstring: Docstring,
426 *,
427 offset: int,
428 warnings: bool = True,
429 **options: Any,
430) -> tuple[DocstringSectionClasses | None, int]:
431 classes = []
432 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
434 signature: str | Expr | None = None
435 for line_number, class_lines in block:
436 try:
437 name_with_signature, description = class_lines[0].split(":", 1)
438 except ValueError:
439 if warnings:
440 docstring_warning(
441 docstring,
442 line_number,
443 f"Failed to get 'signature: description' pair from '{class_lines[0]}'",
444 )
445 continue
447 description = "\n".join([description.lstrip(), *class_lines[1:]]).rstrip("\n")
449 if "(" in name_with_signature:
450 name = name_with_signature.split("(", 1)[0]
451 signature = name_with_signature
452 else:
453 name = name_with_signature
454 signature = None
456 classes.append(DocstringClass(name=name, annotation=signature, description=description))
458 return DocstringSectionClasses(classes), new_offset
461def _read_type_aliases_section(
462 docstring: Docstring,
463 *,
464 offset: int,
465 **options: Any,
466) -> tuple[DocstringSectionTypeAliases | None, int]:
467 type_aliases = []
468 block, new_offset = _read_block_items(docstring, offset=offset, **options)
470 for line_number, type_alias_lines in block:
471 try:
472 name, description = type_alias_lines[0].split(":", 1)
473 except ValueError:
474 docstring_warning(
475 docstring,
476 line_number,
477 f"Failed to get 'name: description' pair from '{type_alias_lines[0]}'",
478 )
479 continue
480 description = "\n".join([description.lstrip(), *type_alias_lines[1:]]).rstrip("\n")
481 type_aliases.append(DocstringTypeAlias(name=name, description=description))
483 return DocstringSectionTypeAliases(type_aliases), new_offset
486def _read_modules_section(
487 docstring: Docstring,
488 *,
489 offset: int,
490 warnings: bool = True,
491 **options: Any,
492) -> tuple[DocstringSectionModules | None, int]:
493 modules = []
494 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
496 for line_number, module_lines in block:
497 try:
498 name, description = module_lines[0].split(":", 1)
499 except ValueError:
500 if warnings:
501 docstring_warning(
502 docstring,
503 line_number,
504 f"Failed to get 'name: description' pair from '{module_lines[0]}'",
505 )
506 continue
508 description = "\n".join([description.lstrip(), *module_lines[1:]]).rstrip("\n")
509 modules.append(DocstringModule(name=name, description=description))
511 return DocstringSectionModules(modules), new_offset
514def _read_raises_section(
515 docstring: Docstring,
516 *,
517 offset: int,
518 warnings: bool = True,
519 **options: Any,
520) -> tuple[DocstringSectionRaises | None, int]:
521 exceptions = []
522 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
524 annotation: str | Expr
525 for line_number, exception_lines in block:
526 try:
527 annotation, description = exception_lines[0].split(":", 1)
528 except ValueError:
529 if warnings:
530 docstring_warning(
531 docstring,
532 line_number,
533 f"Failed to get 'exception: description' pair from '{exception_lines[0]}'",
534 )
535 continue
537 description = "\n".join([description.lstrip(), *exception_lines[1:]]).rstrip("\n")
538 # Try to compile the annotation to transform it into an expression.
539 annotation = parse_docstring_annotation(annotation, docstring)
540 exceptions.append(DocstringRaise(annotation=annotation, description=description))
542 return DocstringSectionRaises(exceptions), new_offset
545def _read_warns_section(
546 docstring: Docstring,
547 *,
548 offset: int,
549 warnings: bool = True,
550 **options: Any,
551) -> tuple[DocstringSectionWarns | None, int]:
552 warns = []
553 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
555 for line_number, warning_lines in block:
556 try:
557 annotation, description = warning_lines[0].split(":", 1)
558 except ValueError:
559 if warnings:
560 docstring_warning(
561 docstring,
562 line_number,
563 f"Failed to get 'warning: description' pair from '{warning_lines[0]}'",
564 )
565 continue
567 description = "\n".join([description.lstrip(), *warning_lines[1:]]).rstrip("\n")
568 warns.append(DocstringWarn(annotation=annotation, description=description))
570 return DocstringSectionWarns(warns), new_offset
573def _read_block_items_maybe(
574 docstring: Docstring,
575 *,
576 offset: int,
577 multiple: bool = True,
578 **options: Any,
579) -> _ItemsBlock:
580 if multiple:
581 return _read_block_items(docstring, offset=offset, **options)
582 one_block, new_offset = _read_block(docstring, offset=offset, **options)
583 return [(new_offset, one_block.splitlines())], new_offset
586def _get_name_annotation_description(
587 docstring: Docstring,
588 line_number: int,
589 lines: list[str],
590 *,
591 named: bool = True,
592 warnings: bool = True,
593) -> tuple[str | None, Any, str]:
594 if named:
595 match = _RE_NAME_ANNOTATION_DESCRIPTION.match(lines[0])
596 if not match: 596 ↛ 597line 596 didn't jump to line 597 because the condition on line 596 was never true
597 if warnings:
598 docstring_warning(
599 docstring,
600 line_number,
601 f"Failed to get name, annotation or description from '{lines[0]}'",
602 )
603 raise ValueError
604 name, annotation, description = match.groups()
605 else:
606 name = None
607 if ":" in lines[0]:
608 annotation, description = lines[0].split(":", 1)
609 annotation = annotation.lstrip("(").rstrip(")")
610 else:
611 annotation = None
612 description = lines[0]
613 description = "\n".join([description.lstrip(), *lines[1:]]).rstrip("\n")
614 return name, annotation, description
617def _annotation_from_parent(
618 docstring: Docstring,
619 *,
620 gen_index: Literal[0, 1, 2],
621 multiple: bool = False,
622 index: int = 0,
623) -> str | Expr | None:
624 annotation = None
625 with suppress(Exception):
626 annotation = docstring.parent.annotation # type: ignore[union-attr]
627 if annotation.is_generator:
628 annotation = annotation.slice.elements[gen_index]
629 elif annotation.is_iterator and gen_index == 0:
630 annotation = annotation.slice
631 if multiple and annotation.is_tuple:
632 annotation = annotation.slice.elements[index]
633 return annotation
636def _read_returns_section(
637 docstring: Docstring,
638 *,
639 offset: int,
640 returns_multiple_items: bool = True,
641 returns_named_value: bool = True,
642 warn_missing_types: bool = True,
643 warnings: bool = True,
644 **options: Any,
645) -> tuple[DocstringSectionReturns | None, int]:
646 returns = []
648 block, new_offset = _read_block_items_maybe(
649 docstring,
650 offset=offset,
651 multiple=returns_multiple_items,
652 **options,
653 )
655 for index, (line_number, return_lines) in enumerate(block):
656 try:
657 name, annotation, description = _get_name_annotation_description(
658 docstring,
659 line_number,
660 return_lines,
661 named=returns_named_value,
662 )
663 except ValueError:
664 continue
666 if annotation:
667 # Try to compile the annotation to transform it into an expression.
668 annotation = parse_docstring_annotation(annotation, docstring)
669 else:
670 # Try to retrieve the annotation from the docstring parent.
671 annotation = _annotation_from_parent(docstring, gen_index=2, multiple=len(block) > 1, index=index)
673 if warnings and warn_missing_types and annotation is None:
674 returned_value = repr(name) if name else index + 1
675 docstring_warning(docstring, line_number, f"No type or annotation for returned value {returned_value}")
677 returns.append(DocstringReturn(name=name or "", annotation=annotation, description=description))
679 return DocstringSectionReturns(returns), new_offset
682def _read_yields_section(
683 docstring: Docstring,
684 *,
685 offset: int,
686 returns_multiple_items: bool = True,
687 returns_named_value: bool = True,
688 warn_missing_types: bool = True,
689 warnings: bool = True,
690 **options: Any,
691) -> tuple[DocstringSectionYields | None, int]:
692 yields = []
694 block, new_offset = _read_block_items_maybe(
695 docstring,
696 offset=offset,
697 multiple=returns_multiple_items,
698 **options,
699 )
701 for index, (line_number, yield_lines) in enumerate(block):
702 try:
703 name, annotation, description = _get_name_annotation_description(
704 docstring,
705 line_number,
706 yield_lines,
707 named=returns_named_value,
708 )
709 except ValueError:
710 continue
712 if annotation:
713 # Try to compile the annotation to transform it into an expression.
714 annotation = parse_docstring_annotation(annotation, docstring)
715 else:
716 # Try to retrieve the annotation from the docstring parent.
717 annotation = _annotation_from_parent(docstring, gen_index=0, multiple=len(block) > 1, index=index)
719 if warnings and warn_missing_types and annotation is None:
720 yielded_value = repr(name) if name else index + 1
721 docstring_warning(docstring, line_number, f"No type or annotation for yielded value {yielded_value}")
723 yields.append(DocstringYield(name=name or "", annotation=annotation, description=description))
725 return DocstringSectionYields(yields), new_offset
728def _read_receives_section(
729 docstring: Docstring,
730 *,
731 offset: int,
732 receives_multiple_items: bool = True,
733 receives_named_value: bool = True,
734 warn_missing_types: bool = True,
735 warnings: bool = True,
736 **options: Any,
737) -> tuple[DocstringSectionReceives | None, int]:
738 receives = []
740 block, new_offset = _read_block_items_maybe(
741 docstring,
742 offset=offset,
743 multiple=receives_multiple_items,
744 **options,
745 )
747 for index, (line_number, receive_lines) in enumerate(block):
748 try:
749 name, annotation, description = _get_name_annotation_description(
750 docstring,
751 line_number,
752 receive_lines,
753 named=receives_named_value,
754 )
755 except ValueError:
756 continue
758 if annotation:
759 # Try to compile the annotation to transform it into an expression.
760 annotation = parse_docstring_annotation(annotation, docstring)
761 else:
762 # Try to retrieve the annotation from the docstring parent.
763 annotation = _annotation_from_parent(docstring, gen_index=1, multiple=len(block) > 1, index=index)
765 if warnings and warn_missing_types and annotation is None:
766 received_value = repr(name) if name else index + 1
767 docstring_warning(docstring, line_number, f"No type or annotation for received value {received_value}")
769 receives.append(DocstringReceive(name=name or "", annotation=annotation, description=description))
771 return DocstringSectionReceives(receives), new_offset
774def _read_examples_section(
775 docstring: Docstring,
776 *,
777 offset: int,
778 trim_doctest_flags: bool = True,
779 **options: Any,
780) -> tuple[DocstringSectionExamples | None, int]:
781 text, new_offset = _read_block(docstring, offset=offset, **options)
783 sub_sections: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = []
784 in_code_example = False
785 in_code_block = False
786 current_text: list[str] = []
787 current_example: list[str] = []
789 for line in text.split("\n"):
790 if _is_empty_line(line):
791 if in_code_example:
792 if current_example: 792 ↛ 795line 792 didn't jump to line 795 because the condition on line 792 was always true
793 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
794 current_example = []
795 in_code_example = False
796 else:
797 current_text.append(line)
799 elif in_code_example:
800 if trim_doctest_flags:
801 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
802 line = _RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901
803 current_example.append(line)
805 elif line.startswith("```"):
806 in_code_block = not in_code_block
807 current_text.append(line)
809 elif in_code_block:
810 current_text.append(line)
812 elif line.startswith(">>>"):
813 if current_text:
814 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
815 current_text = []
816 in_code_example = True
818 if trim_doctest_flags:
819 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
820 current_example.append(line)
822 else:
823 current_text.append(line)
825 if current_text: 825 ↛ 826line 825 didn't jump to line 826 because the condition on line 825 was never true
826 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
827 elif current_example: 827 ↛ 830line 827 didn't jump to line 830 because the condition on line 827 was always true
828 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
830 return DocstringSectionExamples(sub_sections), new_offset
833def _is_empty_line(line: str) -> bool:
834 return not line.strip()
837_section_reader = {
838 DocstringSectionKind.parameters: _read_parameters_section,
839 DocstringSectionKind.other_parameters: _read_other_parameters_section,
840 DocstringSectionKind.type_parameters: _read_type_parameters_section,
841 DocstringSectionKind.raises: _read_raises_section,
842 DocstringSectionKind.warns: _read_warns_section,
843 DocstringSectionKind.examples: _read_examples_section,
844 DocstringSectionKind.attributes: _read_attributes_section,
845 DocstringSectionKind.functions: _read_functions_section,
846 DocstringSectionKind.classes: _read_classes_section,
847 DocstringSectionKind.type_aliases: _read_type_aliases_section,
848 DocstringSectionKind.modules: _read_modules_section,
849 DocstringSectionKind.returns: _read_returns_section,
850 DocstringSectionKind.yields: _read_yields_section,
851 DocstringSectionKind.receives: _read_receives_section,
852}
854_sentinel = object()
857def parse_google(
858 docstring: Docstring,
859 *,
860 ignore_init_summary: bool = False,
861 trim_doctest_flags: bool = True,
862 returns_multiple_items: bool = True,
863 returns_named_value: bool = True,
864 returns_type_in_property_summary: bool = False,
865 receives_multiple_items: bool = True,
866 receives_named_value: bool = True,
867 warn_unknown_params: bool = True,
868 warn_missing_types: bool = True,
869 warnings: bool = True,
870 **options: Any,
871) -> list[DocstringSection]:
872 """Parse a Google-style docstring.
874 This function iterates on lines of a docstring to build sections.
875 It then returns this list of sections.
877 Parameters:
878 docstring: The docstring to parse.
879 ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings.
880 trim_doctest_flags: Whether to remove doctest flags from Python example blocks.
881 returns_multiple_items: Whether to parse multiple items in `Yields` and `Returns` sections.
882 When true, each item's continuation lines must be indented.
883 When false (single item), no further indentation is required.
884 returns_named_value: Whether to parse `Yields` and `Returns` section items as name and description, rather than type and description.
885 When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`.
886 When false, parentheses are optional but the items cannot be named: `int: Description`.
887 receives_multiple_items: Whether to parse multiple items in `Receives` sections.
888 When true, each item's continuation lines must be indented.
889 When false (single item), no further indentation is required.
890 receives_named_value: Whether to parse `Receives` section items as name and description, rather than type and description.
891 When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`.
892 When false, parentheses are optional but the items cannot be named: `int: Description`.
893 returns_type_in_property_summary: Whether to parse the return type of properties
894 at the beginning of their summary: `str: Summary of the property`.
895 warn_unknown_params: Warn about documented parameters not appearing in the signature.
896 warn_missing_types: Warn about missing types/annotations for parameters, return values, etc.
897 warnings: Whether to log warnings at all.
898 **options: Additional parsing options.
900 Returns:
901 A list of docstring sections.
902 """
903 sections: list[DocstringSection] = []
904 current_section = []
906 in_code_block = False
907 lines = docstring.lines
909 options = {
910 "ignore_init_summary": ignore_init_summary,
911 "trim_doctest_flags": trim_doctest_flags,
912 "returns_multiple_items": returns_multiple_items,
913 "returns_named_value": returns_named_value,
914 "returns_type_in_property_summary": returns_type_in_property_summary,
915 "receives_multiple_items": receives_multiple_items,
916 "receives_named_value": receives_named_value,
917 "warn_unknown_params": warn_unknown_params,
918 "warn_missing_types": warn_missing_types,
919 "warnings": warnings,
920 **options,
921 }
923 ignore_summary = (
924 options["ignore_init_summary"]
925 and docstring.parent is not None
926 and docstring.parent.name == "__init__"
927 and docstring.parent.is_function
928 and docstring.parent.parent is not None
929 and docstring.parent.parent.is_class
930 )
932 offset = 2 if ignore_summary else 0
934 while offset < len(lines):
935 line_lower = lines[offset].lower()
937 if in_code_block:
938 if line_lower.lstrip(" ").startswith("```"):
939 in_code_block = False
940 current_section.append(lines[offset])
942 elif line_lower.lstrip(" ").startswith("```"):
943 in_code_block = True
944 current_section.append(lines[offset])
946 elif match := _RE_ADMONITION.match(lines[offset]):
947 groups = match.groupdict()
948 title = groups["title"]
949 admonition_type = groups["type"]
950 is_section = admonition_type.lower() in _section_kind
952 has_previous_line = offset > 0
953 blank_line_above = not has_previous_line or _is_empty_line(lines[offset - 1])
954 has_next_line = offset < len(lines) - 1
955 has_next_lines = offset < len(lines) - 2
956 blank_line_below = has_next_line and _is_empty_line(lines[offset + 1])
957 blank_lines_below = has_next_lines and _is_empty_line(lines[offset + 2])
958 indented_line_below = has_next_line and not blank_line_below and lines[offset + 1].startswith(" ")
959 indented_lines_below = has_next_lines and not blank_lines_below and lines[offset + 2].startswith(" ")
960 if not (indented_line_below or indented_lines_below):
961 # Do not warn when there are no contents,
962 # this is most probably not a section or admonition.
963 current_section.append(lines[offset])
964 offset += 1
965 continue
966 reasons = []
967 kind = "section" if is_section else "admonition"
968 if (indented_line_below or indented_lines_below) and not blank_line_above:
969 reasons.append(f"Missing blank line above {kind}")
970 if indented_lines_below and blank_line_below:
971 reasons.append(f"Extraneous blank line below {kind} title")
972 if reasons:
973 if warnings: 973 ↛ 981line 973 didn't jump to line 981 because the condition on line 973 was always true
974 reasons_string = "; ".join(reasons)
975 docstring_warning(
976 docstring,
977 offset,
978 f"Possible {kind} skipped, reasons: {reasons_string}",
979 LogLevel.debug,
980 )
981 current_section.append(lines[offset])
982 offset += 1
983 continue
985 if is_section:
986 if current_section:
987 if any(current_section): 987 ↛ 989line 987 didn't jump to line 989 because the condition on line 987 was always true
988 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
989 current_section = []
990 reader = _section_reader[_section_kind[admonition_type.lower()]]
991 section, offset = reader(docstring, offset=offset + 1, **options) # type: ignore[operator]
992 if section:
993 section.title = title
994 sections.append(section)
996 else:
997 contents, offset = _read_block(docstring, offset=offset + 1)
998 if contents: 998 ↛ 1008line 998 didn't jump to line 1008 because the condition on line 998 was always true
999 if current_section:
1000 if any(current_section): 1000 ↛ 1002line 1000 didn't jump to line 1002 because the condition on line 1000 was always true
1001 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
1002 current_section = []
1003 if title is None:
1004 title = admonition_type
1005 admonition_type = admonition_type.lower().replace(" ", "-")
1006 sections.append(DocstringSectionAdmonition(kind=admonition_type, text=contents, title=title))
1007 else:
1008 with suppress(IndexError):
1009 current_section.append(lines[offset])
1010 else:
1011 current_section.append(lines[offset])
1013 offset += 1
1015 if current_section:
1016 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
1018 if (
1019 returns_type_in_property_summary
1020 and sections
1021 and docstring.parent
1022 and docstring.parent.is_attribute
1023 and "property" in docstring.parent.labels
1024 ):
1025 lines = sections[0].value.lstrip().split("\n")
1026 if ":" in lines[0]: 1026 ↛ 1036line 1026 didn't jump to line 1036 because the condition on line 1026 was always true
1027 annotation, line = lines[0].split(":", 1)
1028 lines = [line, *lines[1:]]
1029 sections[0].value = "\n".join(lines)
1030 sections.append(
1031 DocstringSectionReturns(
1032 [DocstringReturn("", description="", annotation=parse_docstring_annotation(annotation, docstring))],
1033 ),
1034 )
1036 return sections