Coverage for packages / griffelib / src / griffe / _internal / docstrings / google.py: 86.78%
481 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-11 11:48 +0100
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-11 11:48 +0100
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, TypedDict
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 # TODO: Use `get_name_annotation_description` here too?
202 for line_number, param_lines in block:
203 # Check the presence of a name and description, separated by a colon.
204 try:
205 name_with_type, description = param_lines[0].split(":", 1)
206 except ValueError:
207 if warnings: 207 ↛ 213line 207 didn't jump to line 213 because the condition on line 207 was always true
208 docstring_warning(
209 docstring,
210 line_number,
211 f"Failed to get 'name: description' pair from '{param_lines[0]}'",
212 )
213 continue
215 description = "\n".join([description.lstrip(), *param_lines[1:]]).rstrip("\n")
217 # Use the type given after the parameter name, if any.
218 if "(" in name_with_type and name_with_type.endswith(")"):
219 name, annotation = name_with_type.split("(", 1)
220 name = name.strip()
221 annotation = annotation.removesuffix(")").removesuffix(", optional").strip()
222 # Try to compile the annotation to transform it into an expression.
223 annotation = parse_docstring_annotation(annotation, docstring)
224 else:
225 name = name_with_type
226 # Try to use the annotation from the signature.
227 try:
228 annotation = docstring.parent.parameters[name].annotation # ty:ignore[unresolved-attribute]
229 except (AttributeError, KeyError):
230 annotation = None
232 try:
233 default = docstring.parent.parameters[name].default # ty:ignore[unresolved-attribute]
234 except (AttributeError, KeyError):
235 default = None
237 if warnings and warn_missing_types and annotation is None:
238 docstring_warning(docstring, line_number, f"No type or annotation for parameter '{name}'")
240 if warnings and warn_unknown_params:
241 with suppress(AttributeError): # For Parameters sections in objects without parameters.
242 params = docstring.parent.parameters # ty:ignore[unresolved-attribute]
243 if name not in params:
244 message = f"Parameter '{name}' does not appear in the function signature"
245 for starred_name in (f"*{name}", f"**{name}"):
246 if starred_name in params: 246 ↛ 247line 246 didn't jump to line 247 because the condition on line 246 was never true
247 message += f". Did you mean '{starred_name}'?"
248 break
249 docstring_warning(docstring, line_number, message)
251 parameters.append(DocstringParameter(name=name, value=default, annotation=annotation, description=description))
253 return parameters, new_offset
256def _read_parameters_section(
257 docstring: Docstring,
258 *,
259 offset: int,
260 **options: Any,
261) -> tuple[DocstringSectionParameters | None, int]:
262 parameters, new_offset = _read_parameters(docstring, offset=offset, **options)
263 return DocstringSectionParameters(parameters), new_offset
266def _read_other_parameters_section(
267 docstring: Docstring,
268 *,
269 offset: int,
270 warn_unknown_params: bool = True, # noqa: ARG001
271 **options: Any,
272) -> tuple[DocstringSectionOtherParameters | None, int]:
273 parameters, new_offset = _read_parameters(docstring, offset=offset, warn_unknown_params=False, **options)
274 return DocstringSectionOtherParameters(parameters), new_offset
277def _read_type_parameters_section(
278 docstring: Docstring,
279 *,
280 offset: int,
281 warn_unknown_params: bool = True,
282 **options: Any,
283) -> tuple[DocstringSectionTypeParameters | None, int]:
284 type_parameters = []
285 bound: str | Expr | None
287 block, new_offset = _read_block_items(docstring, offset=offset, **options)
289 for line_number, type_param_lines in block:
290 # check the presence of a name and description, separated by a colon
291 try:
292 name_with_bound, description = type_param_lines[0].split(":", 1)
293 except ValueError:
294 docstring_warning(
295 docstring,
296 line_number,
297 f"Failed to get 'name: description' pair from '{type_param_lines[0]}'",
298 )
299 continue
301 description = "\n".join([description.lstrip(), *type_param_lines[1:]]).rstrip("\n")
303 # use the type given after the type parameter name, if any
304 if " " in name_with_bound:
305 name, bound = name_with_bound.split(" ", 1)
306 if bound.startswith("(") and bound.endswith(")"):
307 bound = bound[1:-1]
308 # try to compile the annotation to transform it into an expression
309 bound = parse_docstring_annotation(bound, docstring)
310 else:
311 name = name_with_bound
312 # try to use the annotation from the signature
313 try:
314 bound = docstring.parent.type_parameters[name].annotation # ty:ignore[possibly-missing-attribute]
315 except (AttributeError, KeyError):
316 bound = None
318 try:
319 default = docstring.parent.type_parameters[name].default # ty:ignore[possibly-missing-attribute]
320 except (AttributeError, KeyError):
321 default = None
323 if warn_unknown_params: 323 ↛ 334line 323 didn't jump to line 334 because the condition on line 323 was always true
324 with suppress(AttributeError): # for type parameters sections in objects without type parameters
325 type_params = docstring.parent.type_parameters # ty:ignore[possibly-missing-attribute]
326 if name not in type_params:
327 message = f"Type parameter '{name}' does not appear in the {docstring.parent.kind.value} signature" # ty:ignore[possibly-missing-attribute]
328 for starred_name in (f"*{name}", f"**{name}"):
329 if starred_name in type_params: 329 ↛ 330line 329 didn't jump to line 330 because the condition on line 329 was never true
330 message += f". Did you mean '{starred_name}'?"
331 break
332 docstring_warning(docstring, line_number, message)
334 type_parameters.append(
335 DocstringTypeParameter(
336 name=name,
337 value=default,
338 annotation=bound,
339 description=description,
340 ),
341 )
343 return DocstringSectionTypeParameters(type_parameters), new_offset
346def _read_attributes_section(
347 docstring: Docstring,
348 *,
349 offset: int,
350 warnings: bool = True,
351 **options: Any,
352) -> tuple[DocstringSectionAttributes | None, int]:
353 attributes = []
354 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
356 annotation: str | Expr | None = None
357 for line_number, attr_lines in block:
358 try:
359 name_with_type, description = attr_lines[0].split(":", 1)
360 except ValueError:
361 if warnings:
362 docstring_warning(
363 docstring,
364 line_number,
365 f"Failed to get 'name: description' pair from '{attr_lines[0]}'",
366 )
367 continue
369 description = "\n".join([description.lstrip(), *attr_lines[1:]]).rstrip("\n")
371 if " " in name_with_type: 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true
372 name, annotation = name_with_type.split(" ", 1)
373 annotation = annotation.strip("()")
374 annotation = annotation.removesuffix(", optional")
375 # Try to compile the annotation to transform it into an expression.
376 annotation = parse_docstring_annotation(annotation, docstring)
377 else:
378 name = name_with_type
379 with suppress(AttributeError, KeyError, TypeError):
380 # Use subscript syntax to fetch annotation from inherited members too.
381 annotation = docstring.parent[name].annotation # ty:ignore[not-subscriptable]
383 attributes.append(DocstringAttribute(name=name, annotation=annotation, description=description))
385 return DocstringSectionAttributes(attributes), new_offset
388def _read_functions_section(
389 docstring: Docstring,
390 *,
391 offset: int,
392 warnings: bool = True,
393 **options: Any,
394) -> tuple[DocstringSectionFunctions | None, int]:
395 functions = []
396 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
398 signature: str | Expr | None = None
399 for line_number, func_lines in block:
400 try:
401 name_with_signature, description = func_lines[0].split(":", 1)
402 except ValueError:
403 if warnings:
404 docstring_warning(
405 docstring,
406 line_number,
407 f"Failed to get 'signature: description' pair from '{func_lines[0]}'",
408 )
409 continue
411 description = "\n".join([description.lstrip(), *func_lines[1:]]).rstrip("\n")
413 if "(" in name_with_signature:
414 name = name_with_signature.split("(", 1)[0]
415 signature = name_with_signature
416 else:
417 name = name_with_signature
418 signature = None
420 functions.append(DocstringFunction(name=name, annotation=signature, description=description))
422 return DocstringSectionFunctions(functions), new_offset
425def _read_classes_section(
426 docstring: Docstring,
427 *,
428 offset: int,
429 warnings: bool = True,
430 **options: Any,
431) -> tuple[DocstringSectionClasses | None, int]:
432 classes = []
433 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
435 signature: str | Expr | None = None
436 for line_number, class_lines in block:
437 try:
438 name_with_signature, description = class_lines[0].split(":", 1)
439 except ValueError:
440 if warnings:
441 docstring_warning(
442 docstring,
443 line_number,
444 f"Failed to get 'signature: description' pair from '{class_lines[0]}'",
445 )
446 continue
448 description = "\n".join([description.lstrip(), *class_lines[1:]]).rstrip("\n")
450 if "(" in name_with_signature:
451 name = name_with_signature.split("(", 1)[0]
452 signature = name_with_signature
453 else:
454 name = name_with_signature
455 signature = None
457 classes.append(DocstringClass(name=name, annotation=signature, description=description))
459 return DocstringSectionClasses(classes), new_offset
462def _read_type_aliases_section(
463 docstring: Docstring,
464 *,
465 offset: int,
466 **options: Any,
467) -> tuple[DocstringSectionTypeAliases | None, int]:
468 type_aliases = []
469 block, new_offset = _read_block_items(docstring, offset=offset, **options)
471 for line_number, type_alias_lines in block:
472 try:
473 name, description = type_alias_lines[0].split(":", 1)
474 except ValueError:
475 docstring_warning(
476 docstring,
477 line_number,
478 f"Failed to get 'name: description' pair from '{type_alias_lines[0]}'",
479 )
480 continue
481 description = "\n".join([description.lstrip(), *type_alias_lines[1:]]).rstrip("\n")
482 type_aliases.append(DocstringTypeAlias(name=name, description=description))
484 return DocstringSectionTypeAliases(type_aliases), new_offset
487def _read_modules_section(
488 docstring: Docstring,
489 *,
490 offset: int,
491 warnings: bool = True,
492 **options: Any,
493) -> tuple[DocstringSectionModules | None, int]:
494 modules = []
495 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
497 for line_number, module_lines in block:
498 try:
499 name, description = module_lines[0].split(":", 1)
500 except ValueError:
501 if warnings:
502 docstring_warning(
503 docstring,
504 line_number,
505 f"Failed to get 'name: description' pair from '{module_lines[0]}'",
506 )
507 continue
509 description = "\n".join([description.lstrip(), *module_lines[1:]]).rstrip("\n")
510 modules.append(DocstringModule(name=name, description=description))
512 return DocstringSectionModules(modules), new_offset
515def _read_raises_section(
516 docstring: Docstring,
517 *,
518 offset: int,
519 warnings: bool = True,
520 **options: Any,
521) -> tuple[DocstringSectionRaises | None, int]:
522 exceptions = []
523 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
525 annotation: str | Expr
526 for line_number, exception_lines in block:
527 try:
528 annotation, description = exception_lines[0].split(":", 1)
529 except ValueError:
530 if warnings:
531 docstring_warning(
532 docstring,
533 line_number,
534 f"Failed to get 'exception: description' pair from '{exception_lines[0]}'",
535 )
536 continue
538 description = "\n".join([description.lstrip(), *exception_lines[1:]]).rstrip("\n")
539 # Try to compile the annotation to transform it into an expression.
540 annotation = parse_docstring_annotation(annotation, docstring)
541 exceptions.append(DocstringRaise(annotation=annotation, description=description))
543 return DocstringSectionRaises(exceptions), new_offset
546def _read_warns_section(
547 docstring: Docstring,
548 *,
549 offset: int,
550 warnings: bool = True,
551 **options: Any,
552) -> tuple[DocstringSectionWarns | None, int]:
553 warns = []
554 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options)
556 for line_number, warning_lines in block:
557 try:
558 annotation, description = warning_lines[0].split(":", 1)
559 except ValueError:
560 if warnings:
561 docstring_warning(
562 docstring,
563 line_number,
564 f"Failed to get 'warning: description' pair from '{warning_lines[0]}'",
565 )
566 continue
568 description = "\n".join([description.lstrip(), *warning_lines[1:]]).rstrip("\n")
569 warns.append(DocstringWarn(annotation=annotation, description=description))
571 return DocstringSectionWarns(warns), new_offset
574def _read_block_items_maybe(
575 docstring: Docstring,
576 *,
577 offset: int,
578 multiple: bool = True,
579 **options: Any,
580) -> _ItemsBlock:
581 if multiple:
582 return _read_block_items(docstring, offset=offset, **options)
583 one_block, new_offset = _read_block(docstring, offset=offset, **options)
584 return [(new_offset, one_block.splitlines())], new_offset
587def _get_name_annotation_description(
588 docstring: Docstring,
589 line_number: int,
590 lines: list[str],
591 *,
592 named: bool = True,
593 warnings: bool = True,
594) -> tuple[str | None, Any, str]:
595 if named:
596 match = _RE_NAME_ANNOTATION_DESCRIPTION.match(lines[0])
597 if not match: 597 ↛ 598line 597 didn't jump to line 598 because the condition on line 597 was never true
598 if warnings:
599 docstring_warning(
600 docstring,
601 line_number,
602 f"Failed to get name, annotation or description from '{lines[0]}'",
603 )
604 raise ValueError
605 name, annotation, description = match.groups()
606 else:
607 name = None
608 if ":" in lines[0]:
609 annotation, description = lines[0].split(":", 1)
610 annotation = annotation.lstrip("(").rstrip(")")
611 else:
612 annotation = None
613 description = lines[0]
614 description = "\n".join([description.lstrip(), *lines[1:]]).rstrip("\n")
615 return name, annotation, description
618def _annotation_from_parent(
619 docstring: Docstring,
620 *,
621 gen_index: Literal[0, 1, 2],
622 multiple: bool = False,
623 index: int = 0,
624) -> str | Expr | None:
625 annotation = None
626 with suppress(Exception):
627 annotation = docstring.parent.annotation # ty:ignore[unresolved-attribute]
628 if annotation.is_generator:
629 annotation = annotation.slice.elements[gen_index]
630 elif annotation.is_iterator and gen_index == 0:
631 annotation = annotation.slice
632 if multiple and annotation.is_tuple:
633 annotation = annotation.slice.elements[index]
634 return annotation
637def _read_returns_section(
638 docstring: Docstring,
639 *,
640 offset: int,
641 returns_multiple_items: bool = True,
642 returns_named_value: bool = True,
643 warn_missing_types: bool = True,
644 warnings: bool = True,
645 **options: Any,
646) -> tuple[DocstringSectionReturns | None, int]:
647 returns = []
649 block, new_offset = _read_block_items_maybe(
650 docstring,
651 offset=offset,
652 multiple=returns_multiple_items,
653 **options,
654 )
656 for index, (line_number, return_lines) in enumerate(block):
657 try:
658 name, annotation, description = _get_name_annotation_description(
659 docstring,
660 line_number,
661 return_lines,
662 named=returns_named_value,
663 )
664 except ValueError:
665 continue
667 if annotation:
668 # Try to compile the annotation to transform it into an expression.
669 annotation = parse_docstring_annotation(annotation, docstring)
670 else:
671 # Try to retrieve the annotation from the docstring parent.
672 annotation = _annotation_from_parent(docstring, gen_index=2, multiple=len(block) > 1, index=index)
674 if warnings and warn_missing_types and annotation is None:
675 returned_value = repr(name) if name else index + 1
676 docstring_warning(docstring, line_number, f"No type or annotation for returned value {returned_value}")
678 returns.append(DocstringReturn(name=name or "", annotation=annotation, description=description))
680 return DocstringSectionReturns(returns), new_offset
683def _read_yields_section(
684 docstring: Docstring,
685 *,
686 offset: int,
687 returns_multiple_items: bool = True,
688 returns_named_value: bool = True,
689 warn_missing_types: bool = True,
690 warnings: bool = True,
691 **options: Any,
692) -> tuple[DocstringSectionYields | None, int]:
693 yields = []
695 block, new_offset = _read_block_items_maybe(
696 docstring,
697 offset=offset,
698 multiple=returns_multiple_items,
699 **options,
700 )
702 for index, (line_number, yield_lines) in enumerate(block):
703 try:
704 name, annotation, description = _get_name_annotation_description(
705 docstring,
706 line_number,
707 yield_lines,
708 named=returns_named_value,
709 )
710 except ValueError:
711 continue
713 if annotation:
714 # Try to compile the annotation to transform it into an expression.
715 annotation = parse_docstring_annotation(annotation, docstring)
716 else:
717 # Try to retrieve the annotation from the docstring parent.
718 annotation = _annotation_from_parent(docstring, gen_index=0, multiple=len(block) > 1, index=index)
720 if warnings and warn_missing_types and annotation is None:
721 yielded_value = repr(name) if name else index + 1
722 docstring_warning(docstring, line_number, f"No type or annotation for yielded value {yielded_value}")
724 yields.append(DocstringYield(name=name or "", annotation=annotation, description=description))
726 return DocstringSectionYields(yields), new_offset
729def _read_receives_section(
730 docstring: Docstring,
731 *,
732 offset: int,
733 receives_multiple_items: bool = True,
734 receives_named_value: bool = True,
735 warn_missing_types: bool = True,
736 warnings: bool = True,
737 **options: Any,
738) -> tuple[DocstringSectionReceives | None, int]:
739 receives = []
741 block, new_offset = _read_block_items_maybe(
742 docstring,
743 offset=offset,
744 multiple=receives_multiple_items,
745 **options,
746 )
748 for index, (line_number, receive_lines) in enumerate(block):
749 try:
750 name, annotation, description = _get_name_annotation_description(
751 docstring,
752 line_number,
753 receive_lines,
754 named=receives_named_value,
755 )
756 except ValueError:
757 continue
759 if annotation:
760 # Try to compile the annotation to transform it into an expression.
761 annotation = parse_docstring_annotation(annotation, docstring)
762 else:
763 # Try to retrieve the annotation from the docstring parent.
764 annotation = _annotation_from_parent(docstring, gen_index=1, multiple=len(block) > 1, index=index)
766 if warnings and warn_missing_types and annotation is None:
767 received_value = repr(name) if name else index + 1
768 docstring_warning(docstring, line_number, f"No type or annotation for received value {received_value}")
770 receives.append(DocstringReceive(name=name or "", annotation=annotation, description=description))
772 return DocstringSectionReceives(receives), new_offset
775def _read_examples_section(
776 docstring: Docstring,
777 *,
778 offset: int,
779 trim_doctest_flags: bool = True,
780 **options: Any,
781) -> tuple[DocstringSectionExamples | None, int]:
782 text, new_offset = _read_block(docstring, offset=offset, **options)
784 sub_sections: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = []
785 in_code_example = False
786 in_code_block = False
787 current_text: list[str] = []
788 current_example: list[str] = []
790 for line in text.split("\n"):
791 if _is_empty_line(line):
792 if in_code_example:
793 if current_example: 793 ↛ 796line 793 didn't jump to line 796 because the condition on line 793 was always true
794 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
795 current_example = []
796 in_code_example = False
797 else:
798 current_text.append(line)
800 elif in_code_example:
801 if trim_doctest_flags:
802 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
803 line = _RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901
804 current_example.append(line)
806 elif line.startswith("```"):
807 in_code_block = not in_code_block
808 current_text.append(line)
810 elif in_code_block:
811 current_text.append(line)
813 elif line.startswith(">>>"):
814 if current_text:
815 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
816 current_text = []
817 in_code_example = True
819 if trim_doctest_flags:
820 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
821 current_example.append(line)
823 else:
824 current_text.append(line)
826 if current_text: 826 ↛ 827line 826 didn't jump to line 827 because the condition on line 826 was never true
827 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
828 elif current_example: 828 ↛ 831line 828 didn't jump to line 831 because the condition on line 828 was always true
829 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
831 return DocstringSectionExamples(sub_sections), new_offset
834def _is_empty_line(line: str) -> bool:
835 return not line.strip()
838_section_reader = {
839 DocstringSectionKind.parameters: _read_parameters_section,
840 DocstringSectionKind.other_parameters: _read_other_parameters_section,
841 DocstringSectionKind.type_parameters: _read_type_parameters_section,
842 DocstringSectionKind.raises: _read_raises_section,
843 DocstringSectionKind.warns: _read_warns_section,
844 DocstringSectionKind.examples: _read_examples_section,
845 DocstringSectionKind.attributes: _read_attributes_section,
846 DocstringSectionKind.functions: _read_functions_section,
847 DocstringSectionKind.classes: _read_classes_section,
848 DocstringSectionKind.type_aliases: _read_type_aliases_section,
849 DocstringSectionKind.modules: _read_modules_section,
850 DocstringSectionKind.returns: _read_returns_section,
851 DocstringSectionKind.yields: _read_yields_section,
852 DocstringSectionKind.receives: _read_receives_section,
853}
856class GoogleOptions(TypedDict, total=False):
857 """Options for parsing Google-style docstrings."""
859 ignore_init_summary: bool
860 """Whether to ignore the summary in `__init__` methods' docstrings."""
861 trim_doctest_flags: bool
862 """Whether to remove doctest flags from Python example blocks."""
863 returns_multiple_items: bool
864 """Whether to parse multiple items in `Yields` and `Returns` sections."""
865 returns_named_value: bool
866 """Whether to parse `Yields` and `Returns` section items as name and description, rather than type and description."""
867 returns_type_in_property_summary: bool
868 """Whether to parse the return type of properties at the beginning of their summary."""
869 receives_multiple_items: bool
870 """Whether to parse multiple items in `Receives` sections."""
871 receives_named_value: bool
872 """Whether to parse `Receives` section items as name and description, rather than type and description."""
873 warn_unknown_params: bool
874 """Whether to warn about unknown parameters."""
875 warn_missing_types: bool
876 """Whether to warn about missing types/annotations for parameters, return values, etc."""
877 warnings: bool
878 """Whether to issue warnings for parsing issues."""
881def parse_google(
882 docstring: Docstring,
883 *,
884 ignore_init_summary: bool = False,
885 trim_doctest_flags: bool = True,
886 returns_multiple_items: bool = True,
887 returns_named_value: bool = True,
888 returns_type_in_property_summary: bool = False,
889 receives_multiple_items: bool = True,
890 receives_named_value: bool = True,
891 warn_unknown_params: bool = True,
892 warn_missing_types: bool = True,
893 warnings: bool = True,
894) -> list[DocstringSection]:
895 """Parse a Google-style docstring.
897 This function iterates on lines of a docstring to build sections.
898 It then returns this list of sections.
900 Parameters:
901 docstring: The docstring to parse.
902 ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings.
903 trim_doctest_flags: Whether to remove doctest flags from Python example blocks.
904 returns_multiple_items: Whether to parse multiple items in `Yields` and `Returns` sections.
905 When true, each item's continuation lines must be indented.
906 When false (single item), no further indentation is required.
907 returns_named_value: Whether to parse `Yields` and `Returns` section items as name and description, rather than type and description.
908 When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`.
909 When false, parentheses are optional but the items cannot be named: `int: Description`.
910 receives_multiple_items: Whether to parse multiple items in `Receives` sections.
911 When true, each item's continuation lines must be indented.
912 When false (single item), no further indentation is required.
913 receives_named_value: Whether to parse `Receives` section items as name and description, rather than type and description.
914 When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`.
915 When false, parentheses are optional but the items cannot be named: `int: Description`.
916 returns_type_in_property_summary: Whether to parse the return type of properties
917 at the beginning of their summary: `str: Summary of the property`.
918 warn_unknown_params: Warn about documented parameters not appearing in the signature.
919 warn_missing_types: Warn about missing types/annotations for parameters, return values, etc.
920 warnings: Whether to log warnings at all.
922 Returns:
923 A list of docstring sections.
924 """
925 sections: list[DocstringSection] = []
926 current_section = []
928 in_code_block = False
929 lines = docstring.lines
931 options = {
932 "ignore_init_summary": ignore_init_summary,
933 "trim_doctest_flags": trim_doctest_flags,
934 "returns_multiple_items": returns_multiple_items,
935 "returns_named_value": returns_named_value,
936 "returns_type_in_property_summary": returns_type_in_property_summary,
937 "receives_multiple_items": receives_multiple_items,
938 "receives_named_value": receives_named_value,
939 "warn_unknown_params": warn_unknown_params,
940 "warn_missing_types": warn_missing_types,
941 "warnings": warnings,
942 }
944 ignore_summary = (
945 options["ignore_init_summary"]
946 and docstring.parent is not None
947 and docstring.parent.name == "__init__"
948 and docstring.parent.is_function
949 and docstring.parent.parent is not None
950 and docstring.parent.parent.is_class
951 )
953 offset = 2 if ignore_summary else 0
955 while offset < len(lines):
956 line_lower = lines[offset].lower()
958 if in_code_block:
959 if line_lower.lstrip(" ").startswith("```"):
960 in_code_block = False
961 current_section.append(lines[offset])
963 elif line_lower.lstrip(" ").startswith("```"):
964 in_code_block = True
965 current_section.append(lines[offset])
967 elif match := _RE_ADMONITION.match(lines[offset]):
968 groups = match.groupdict()
969 title = groups["title"]
970 admonition_type = groups["type"]
971 is_section = admonition_type.lower() in _section_kind
973 has_previous_line = offset > 0
974 blank_line_above = not has_previous_line or _is_empty_line(lines[offset - 1])
975 has_next_line = offset < len(lines) - 1
976 has_next_lines = offset < len(lines) - 2
977 blank_line_below = has_next_line and _is_empty_line(lines[offset + 1])
978 blank_lines_below = has_next_lines and _is_empty_line(lines[offset + 2])
979 indented_line_below = has_next_line and not blank_line_below and lines[offset + 1].startswith(" ")
980 indented_lines_below = has_next_lines and not blank_lines_below and lines[offset + 2].startswith(" ")
981 if not (indented_line_below or indented_lines_below):
982 # Do not warn when there are no contents,
983 # this is most probably not a section or admonition.
984 current_section.append(lines[offset])
985 offset += 1
986 continue
987 reasons = []
988 kind = "section" if is_section else "admonition"
989 if (indented_line_below or indented_lines_below) and not blank_line_above:
990 reasons.append(f"Missing blank line above {kind}")
991 if indented_lines_below and blank_line_below:
992 reasons.append(f"Extraneous blank line below {kind} title")
993 if reasons:
994 if warnings: 994 ↛ 1002line 994 didn't jump to line 1002 because the condition on line 994 was always true
995 reasons_string = "; ".join(reasons)
996 docstring_warning(
997 docstring,
998 offset,
999 f"Possible {kind} skipped, reasons: {reasons_string}",
1000 LogLevel.debug,
1001 )
1002 current_section.append(lines[offset])
1003 offset += 1
1004 continue
1006 if is_section:
1007 if current_section:
1008 if any(current_section): 1008 ↛ 1010line 1008 didn't jump to line 1010 because the condition on line 1008 was always true
1009 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
1010 current_section = []
1011 reader = _section_reader[_section_kind[admonition_type.lower()]]
1012 section, offset = reader(docstring, offset=offset + 1, **options)
1013 if section:
1014 section.title = title
1015 sections.append(section)
1017 else:
1018 contents, offset = _read_block(docstring, offset=offset + 1)
1019 if contents: 1019 ↛ 1029line 1019 didn't jump to line 1029 because the condition on line 1019 was always true
1020 if current_section:
1021 if any(current_section): 1021 ↛ 1023line 1021 didn't jump to line 1023 because the condition on line 1021 was always true
1022 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
1023 current_section = []
1024 if title is None:
1025 title = admonition_type
1026 admonition_type = admonition_type.lower().replace(" ", "-")
1027 sections.append(DocstringSectionAdmonition(kind=admonition_type, text=contents, title=title))
1028 else:
1029 with suppress(IndexError):
1030 current_section.append(lines[offset])
1031 else:
1032 current_section.append(lines[offset])
1034 offset += 1
1036 if current_section and any(current_section):
1037 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
1039 if (
1040 returns_type_in_property_summary
1041 and sections
1042 and docstring.parent
1043 and docstring.parent.is_attribute
1044 and "property" in docstring.parent.labels
1045 ):
1046 lines = sections[0].value.lstrip().split("\n")
1047 if ":" in lines[0]: 1047 ↛ 1057line 1047 didn't jump to line 1057 because the condition on line 1047 was always true
1048 annotation, line = lines[0].split(":", 1)
1049 lines = [line, *lines[1:]]
1050 sections[0].value = "\n".join(lines)
1051 sections.append(
1052 DocstringSectionReturns(
1053 [DocstringReturn("", description="", annotation=parse_docstring_annotation(annotation, docstring))],
1054 ),
1055 )
1057 return sections