Coverage for src/_griffe/docstrings/google.py: 88.13%
413 statements
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
1# This module defines functions to parse Google-style docstrings into structured data.
3from __future__ import annotations
5import re
6from contextlib import suppress
7from typing import TYPE_CHECKING
9from _griffe.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 DocstringSectionDeprecated,
23 DocstringSectionExamples,
24 DocstringSectionFunctions,
25 DocstringSectionModules,
26 DocstringSectionOtherParameters,
27 DocstringSectionParameters,
28 DocstringSectionRaises,
29 DocstringSectionReceives,
30 DocstringSectionReturns,
31 DocstringSectionText,
32 DocstringSectionWarns,
33 DocstringSectionYields,
34 DocstringWarn,
35 DocstringYield,
36)
37from _griffe.docstrings.utils import docstring_warning, parse_docstring_annotation
38from _griffe.enumerations import DocstringSectionKind, LogLevel
40if TYPE_CHECKING:
41 from re import Pattern
42 from typing import Any, Literal
44 from _griffe.expressions import Expr
45 from _griffe.models import Docstring
48_section_kind = {
49 "args": DocstringSectionKind.parameters,
50 "arguments": DocstringSectionKind.parameters,
51 "params": DocstringSectionKind.parameters,
52 "parameters": DocstringSectionKind.parameters,
53 "keyword args": DocstringSectionKind.other_parameters,
54 "keyword arguments": DocstringSectionKind.other_parameters,
55 "other args": DocstringSectionKind.other_parameters,
56 "other arguments": DocstringSectionKind.other_parameters,
57 "other params": DocstringSectionKind.other_parameters,
58 "other parameters": DocstringSectionKind.other_parameters,
59 "raises": DocstringSectionKind.raises,
60 "exceptions": DocstringSectionKind.raises,
61 "returns": DocstringSectionKind.returns,
62 "yields": DocstringSectionKind.yields,
63 "receives": DocstringSectionKind.receives,
64 "examples": DocstringSectionKind.examples,
65 "attributes": DocstringSectionKind.attributes,
66 "functions": DocstringSectionKind.functions,
67 "methods": DocstringSectionKind.functions,
68 "classes": DocstringSectionKind.classes,
69 "modules": DocstringSectionKind.modules,
70 "warns": DocstringSectionKind.warns,
71 "warnings": DocstringSectionKind.warns,
72}
74_BlockItem = tuple[int, list[str]]
75_BlockItems = list[_BlockItem]
76_ItemsBlock = tuple[_BlockItems, int]
78_RE_ADMONITION: Pattern = re.compile(r"^(?P<type>[\w][\s\w-]*):(\s+(?P<title>[^\s].*))?\s*$", re.IGNORECASE)
79_RE_NAME_ANNOTATION_DESCRIPTION: Pattern = re.compile(r"^(?:(?P<name>\w+)?\s*(?:\((?P<type>.+)\))?:\s*)?(?P<desc>.*)$")
80_RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$")
81_RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$")
84def _read_block_items(docstring: Docstring, *, offset: int, **options: Any) -> _ItemsBlock: # noqa: ARG001
85 lines = docstring.lines
86 if offset >= len(lines): 86 ↛ 87line 86 didn't jump to line 87 because the condition on line 86 was never true
87 return [], offset
89 new_offset = offset
90 items: _BlockItems = []
92 # skip first empty lines
93 while _is_empty_line(lines[new_offset]): 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true
94 new_offset += 1
96 # get initial indent
97 indent = len(lines[new_offset]) - len(lines[new_offset].lstrip())
99 if indent == 0: 99 ↛ 101line 99 didn't jump to line 101 because the condition on line 99 was never true
100 # first non-empty line was not indented, abort
101 return [], new_offset - 1
103 # start processing first item
104 current_item = (new_offset, [lines[new_offset][indent:]])
105 new_offset += 1
107 # loop on next lines
108 while new_offset < len(lines):
109 line = lines[new_offset]
111 if _is_empty_line(line):
112 # empty line: preserve it in the current item
113 current_item[1].append("")
115 elif line.startswith(indent * 2 * " "):
116 # continuation line
117 current_item[1].append(line[indent * 2 :])
119 elif line.startswith((indent + 1) * " "):
120 # indent between initial and continuation: append but warn
121 cont_indent = len(line) - len(line.lstrip())
122 current_item[1].append(line[cont_indent:])
123 docstring_warning(
124 docstring,
125 new_offset,
126 f"Confusing indentation for continuation line {new_offset+1} in docstring, "
127 f"should be {indent} * 2 = {indent*2} spaces, not {cont_indent}",
128 )
130 elif line.startswith(indent * " "):
131 # indent equal to initial one: new item
132 items.append(current_item)
133 current_item = (new_offset, [line[indent:]])
135 else:
136 # indent lower than initial one: end of section
137 break
139 new_offset += 1
141 if current_item: 141 ↛ 144line 141 didn't jump to line 144 because the condition on line 141 was always true
142 items.append(current_item)
144 return items, new_offset - 1
147def _read_block(docstring: Docstring, *, offset: int, **options: Any) -> tuple[str, int]: # noqa: ARG001
148 lines = docstring.lines
149 if offset >= len(lines): 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true
150 return "", offset - 1
152 new_offset = offset
153 block: list[str] = []
155 # skip first empty lines
156 while _is_empty_line(lines[new_offset]): 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true
157 new_offset += 1
159 # get initial indent
160 indent = len(lines[new_offset]) - len(lines[new_offset].lstrip())
162 if indent == 0: 162 ↛ 164line 162 didn't jump to line 164 because the condition on line 162 was never true
163 # first non-empty line was not indented, abort
164 return "", offset - 1
166 # start processing first item
167 block.append(lines[new_offset].lstrip())
168 new_offset += 1
170 # loop on next lines
171 while new_offset < len(lines) and (lines[new_offset].startswith(indent * " ") or _is_empty_line(lines[new_offset])):
172 block.append(lines[new_offset][indent:])
173 new_offset += 1
175 return "\n".join(block).rstrip("\n"), new_offset - 1
178def _read_parameters(
179 docstring: Docstring,
180 *,
181 offset: int,
182 warn_unknown_params: bool = True,
183 **options: Any,
184) -> tuple[list[DocstringParameter], int]:
185 parameters = []
186 annotation: str | Expr | None
188 block, new_offset = _read_block_items(docstring, offset=offset, **options)
190 for line_number, param_lines in block:
191 # check the presence of a name and description, separated by a colon
192 try:
193 name_with_type, description = param_lines[0].split(":", 1)
194 except ValueError:
195 docstring_warning(docstring, line_number, f"Failed to get 'name: description' pair from '{param_lines[0]}'")
196 continue
198 description = "\n".join([description.lstrip(), *param_lines[1:]]).rstrip("\n")
200 # use the type given after the parameter name, if any
201 if " " in name_with_type:
202 name, annotation = name_with_type.split(" ", 1)
203 annotation = annotation.strip("()")
204 if annotation.endswith(", optional"):
205 annotation = annotation[:-10]
206 # try to compile the annotation to transform it into an expression
207 annotation = parse_docstring_annotation(annotation, docstring)
208 else:
209 name = name_with_type
210 # try to use the annotation from the signature
211 try:
212 annotation = docstring.parent.parameters[name].annotation # type: ignore[union-attr]
213 except (AttributeError, KeyError):
214 annotation = None
216 try:
217 default = docstring.parent.parameters[name].default # type: ignore[union-attr]
218 except (AttributeError, KeyError):
219 default = None
221 if annotation is None:
222 docstring_warning(docstring, line_number, f"No type or annotation for parameter '{name}'")
224 if warn_unknown_params:
225 with suppress(AttributeError): # for parameters sections in objects without parameters
226 params = docstring.parent.parameters # type: ignore[union-attr]
227 if name not in params:
228 message = f"Parameter '{name}' does not appear in the function signature"
229 for starred_name in (f"*{name}", f"**{name}"):
230 if starred_name in params: 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true
231 message += f". Did you mean '{starred_name}'?"
232 break
233 docstring_warning(docstring, line_number, message)
235 parameters.append(DocstringParameter(name=name, value=default, annotation=annotation, description=description))
237 return parameters, new_offset
240def _read_parameters_section(
241 docstring: Docstring,
242 *,
243 offset: int,
244 **options: Any,
245) -> tuple[DocstringSectionParameters | None, int]:
246 parameters, new_offset = _read_parameters(docstring, offset=offset, **options)
247 return DocstringSectionParameters(parameters), new_offset
250def _read_other_parameters_section(
251 docstring: Docstring,
252 *,
253 offset: int,
254 warn_unknown_params: bool = True, # noqa: ARG001
255 **options: Any,
256) -> tuple[DocstringSectionOtherParameters | None, int]:
257 parameters, new_offset = _read_parameters(docstring, offset=offset, warn_unknown_params=False, **options)
258 return DocstringSectionOtherParameters(parameters), new_offset
261def _read_attributes_section(
262 docstring: Docstring,
263 *,
264 offset: int,
265 **options: Any,
266) -> tuple[DocstringSectionAttributes | None, int]:
267 attributes = []
268 block, new_offset = _read_block_items(docstring, offset=offset, **options)
270 annotation: str | Expr | None = None
271 for line_number, attr_lines in block:
272 try:
273 name_with_type, description = attr_lines[0].split(":", 1)
274 except ValueError:
275 docstring_warning(docstring, line_number, f"Failed to get 'name: description' pair from '{attr_lines[0]}'")
276 continue
278 description = "\n".join([description.lstrip(), *attr_lines[1:]]).rstrip("\n")
280 if " " in name_with_type: 280 ↛ 281line 280 didn't jump to line 281 because the condition on line 280 was never true
281 name, annotation = name_with_type.split(" ", 1)
282 annotation = annotation.strip("()")
283 if annotation.endswith(", optional"):
284 annotation = annotation[:-10]
285 # try to compile the annotation to transform it into an expression
286 annotation = parse_docstring_annotation(annotation, docstring)
287 else:
288 name = name_with_type
289 with suppress(AttributeError, KeyError):
290 annotation = docstring.parent.members[name].annotation # type: ignore[union-attr]
292 attributes.append(DocstringAttribute(name=name, annotation=annotation, description=description))
294 return DocstringSectionAttributes(attributes), new_offset
297def _read_functions_section(
298 docstring: Docstring,
299 *,
300 offset: int,
301 **options: Any,
302) -> tuple[DocstringSectionFunctions | None, int]:
303 functions = []
304 block, new_offset = _read_block_items(docstring, offset=offset, **options)
306 signature: str | Expr | None = None
307 for line_number, func_lines in block:
308 try:
309 name_with_signature, description = func_lines[0].split(":", 1)
310 except ValueError:
311 docstring_warning(
312 docstring,
313 line_number,
314 f"Failed to get 'signature: description' pair from '{func_lines[0]}'",
315 )
316 continue
318 description = "\n".join([description.lstrip(), *func_lines[1:]]).rstrip("\n")
320 if "(" in name_with_signature:
321 name = name_with_signature.split("(", 1)[0]
322 signature = name_with_signature
323 else:
324 name = name_with_signature
325 signature = None
327 functions.append(DocstringFunction(name=name, annotation=signature, description=description))
329 return DocstringSectionFunctions(functions), new_offset
332def _read_classes_section(
333 docstring: Docstring,
334 *,
335 offset: int,
336 **options: Any,
337) -> tuple[DocstringSectionClasses | None, int]:
338 classes = []
339 block, new_offset = _read_block_items(docstring, offset=offset, **options)
341 signature: str | Expr | None = None
342 for line_number, class_lines in block:
343 try:
344 name_with_signature, description = class_lines[0].split(":", 1)
345 except ValueError:
346 docstring_warning(
347 docstring,
348 line_number,
349 f"Failed to get 'signature: description' pair from '{class_lines[0]}'",
350 )
351 continue
353 description = "\n".join([description.lstrip(), *class_lines[1:]]).rstrip("\n")
355 if "(" in name_with_signature:
356 name = name_with_signature.split("(", 1)[0]
357 signature = name_with_signature
358 else:
359 name = name_with_signature
360 signature = None
362 classes.append(DocstringClass(name=name, annotation=signature, description=description))
364 return DocstringSectionClasses(classes), new_offset
367def _read_modules_section(
368 docstring: Docstring,
369 *,
370 offset: int,
371 **options: Any,
372) -> tuple[DocstringSectionModules | None, int]:
373 modules = []
374 block, new_offset = _read_block_items(docstring, offset=offset, **options)
376 for line_number, module_lines in block:
377 try:
378 name, description = module_lines[0].split(":", 1)
379 except ValueError:
380 docstring_warning(
381 docstring,
382 line_number,
383 f"Failed to get 'name: description' pair from '{module_lines[0]}'",
384 )
385 continue
386 description = "\n".join([description.lstrip(), *module_lines[1:]]).rstrip("\n")
387 modules.append(DocstringModule(name=name, description=description))
389 return DocstringSectionModules(modules), new_offset
392def _read_raises_section(
393 docstring: Docstring,
394 *,
395 offset: int,
396 **options: Any,
397) -> tuple[DocstringSectionRaises | None, int]:
398 exceptions = []
399 block, new_offset = _read_block_items(docstring, offset=offset, **options)
401 annotation: str | Expr
402 for line_number, exception_lines in block:
403 try:
404 annotation, description = exception_lines[0].split(":", 1)
405 except ValueError:
406 docstring_warning(
407 docstring,
408 line_number,
409 f"Failed to get 'exception: description' pair from '{exception_lines[0]}'",
410 )
411 else:
412 description = "\n".join([description.lstrip(), *exception_lines[1:]]).rstrip("\n")
413 # try to compile the annotation to transform it into an expression
414 annotation = parse_docstring_annotation(annotation, docstring)
415 exceptions.append(DocstringRaise(annotation=annotation, description=description))
417 return DocstringSectionRaises(exceptions), new_offset
420def _read_warns_section(
421 docstring: Docstring,
422 *,
423 offset: int,
424 **options: Any,
425) -> tuple[DocstringSectionWarns | None, int]:
426 warns = []
427 block, new_offset = _read_block_items(docstring, offset=offset, **options)
429 for line_number, warning_lines in block:
430 try:
431 annotation, description = warning_lines[0].split(":", 1)
432 except ValueError:
433 docstring_warning(
434 docstring,
435 line_number,
436 f"Failed to get 'warning: description' pair from '{warning_lines[0]}'",
437 )
438 else:
439 description = "\n".join([description.lstrip(), *warning_lines[1:]]).rstrip("\n")
440 warns.append(DocstringWarn(annotation=annotation, description=description))
442 return DocstringSectionWarns(warns), new_offset
445def _read_block_items_maybe(
446 docstring: Docstring,
447 *,
448 offset: int,
449 multiple: bool = True,
450 **options: Any,
451) -> _ItemsBlock:
452 if multiple:
453 return _read_block_items(docstring, offset=offset, **options)
454 one_block, new_offset = _read_block(docstring, offset=offset, **options)
455 return [(new_offset, one_block.splitlines())], new_offset
458def _get_name_annotation_description(
459 docstring: Docstring,
460 line_number: int,
461 lines: list[str],
462 *,
463 named: bool = True,
464) -> tuple[str | None, Any, str]:
465 if named:
466 match = _RE_NAME_ANNOTATION_DESCRIPTION.match(lines[0])
467 if not match: 467 ↛ 468line 467 didn't jump to line 468 because the condition on line 467 was never true
468 docstring_warning(
469 docstring,
470 line_number,
471 f"Failed to get name, annotation or description from '{lines[0]}'",
472 )
473 raise ValueError
474 name, annotation, description = match.groups()
475 else:
476 name = None
477 if ":" in lines[0]:
478 annotation, description = lines[0].split(":", 1)
479 annotation = annotation.lstrip("(").rstrip(")")
480 else:
481 annotation = None
482 description = lines[0]
483 description = "\n".join([description.lstrip(), *lines[1:]]).rstrip("\n")
484 return name, annotation, description
487def _annotation_from_parent(
488 docstring: Docstring,
489 *,
490 gen_index: Literal[0, 1, 2],
491 multiple: bool = False,
492 index: int = 0,
493) -> str | Expr | None:
494 annotation = None
495 with suppress(Exception):
496 annotation = docstring.parent.annotation # type: ignore[union-attr]
497 if annotation.is_generator:
498 annotation = annotation.slice.elements[gen_index]
499 elif annotation.is_iterator and gen_index == 0:
500 annotation = annotation.slice
501 if multiple and annotation.is_tuple:
502 annotation = annotation.slice.elements[index]
503 return annotation
506def _read_returns_section(
507 docstring: Docstring,
508 *,
509 offset: int,
510 returns_multiple_items: bool = True,
511 returns_named_value: bool = True,
512 **options: Any,
513) -> tuple[DocstringSectionReturns | None, int]:
514 returns = []
516 block, new_offset = _read_block_items_maybe(
517 docstring,
518 offset=offset,
519 multiple=returns_multiple_items,
520 **options,
521 )
523 for index, (line_number, return_lines) in enumerate(block):
524 try:
525 name, annotation, description = _get_name_annotation_description(
526 docstring,
527 line_number,
528 return_lines,
529 named=returns_named_value,
530 )
531 except ValueError:
532 continue
534 if annotation:
535 # try to compile the annotation to transform it into an expression
536 annotation = parse_docstring_annotation(annotation, docstring)
537 else:
538 # try to retrieve the annotation from the docstring parent
539 annotation = _annotation_from_parent(docstring, gen_index=2, multiple=len(block) > 1, index=index)
541 if annotation is None:
542 returned_value = repr(name) if name else index + 1
543 docstring_warning(docstring, line_number, f"No type or annotation for returned value {returned_value}")
545 returns.append(DocstringReturn(name=name or "", annotation=annotation, description=description))
547 return DocstringSectionReturns(returns), new_offset
550def _read_yields_section(
551 docstring: Docstring,
552 *,
553 offset: int,
554 returns_multiple_items: bool = True,
555 returns_named_value: bool = True,
556 **options: Any,
557) -> tuple[DocstringSectionYields | None, int]:
558 yields = []
560 block, new_offset = _read_block_items_maybe(
561 docstring,
562 offset=offset,
563 multiple=returns_multiple_items,
564 **options,
565 )
567 for index, (line_number, yield_lines) in enumerate(block):
568 try:
569 name, annotation, description = _get_name_annotation_description(
570 docstring,
571 line_number,
572 yield_lines,
573 named=returns_named_value,
574 )
575 except ValueError:
576 continue
578 if annotation:
579 # try to compile the annotation to transform it into an expression
580 annotation = parse_docstring_annotation(annotation, docstring)
581 else:
582 # try to retrieve the annotation from the docstring parent
583 annotation = _annotation_from_parent(docstring, gen_index=0, multiple=len(block) > 1, index=index)
585 if annotation is None:
586 yielded_value = repr(name) if name else index + 1
587 docstring_warning(docstring, line_number, f"No type or annotation for yielded value {yielded_value}")
589 yields.append(DocstringYield(name=name or "", annotation=annotation, description=description))
591 return DocstringSectionYields(yields), new_offset
594def _read_receives_section(
595 docstring: Docstring,
596 *,
597 offset: int,
598 receives_multiple_items: bool = True,
599 receives_named_value: bool = True,
600 **options: Any,
601) -> tuple[DocstringSectionReceives | None, int]:
602 receives = []
604 block, new_offset = _read_block_items_maybe(
605 docstring,
606 offset=offset,
607 multiple=receives_multiple_items,
608 **options,
609 )
611 for index, (line_number, receive_lines) in enumerate(block):
612 try:
613 name, annotation, description = _get_name_annotation_description(
614 docstring,
615 line_number,
616 receive_lines,
617 named=receives_named_value,
618 )
619 except ValueError:
620 continue
622 if annotation:
623 # try to compile the annotation to transform it into an expression
624 annotation = parse_docstring_annotation(annotation, docstring)
625 else:
626 # try to retrieve the annotation from the docstring parent
627 annotation = _annotation_from_parent(docstring, gen_index=1, multiple=len(block) > 1, index=index)
629 if annotation is None:
630 received_value = repr(name) if name else index + 1
631 docstring_warning(docstring, line_number, f"No type or annotation for received value {received_value}")
633 receives.append(DocstringReceive(name=name or "", annotation=annotation, description=description))
635 return DocstringSectionReceives(receives), new_offset
638def _read_examples_section(
639 docstring: Docstring,
640 *,
641 offset: int,
642 trim_doctest_flags: bool = True,
643 **options: Any,
644) -> tuple[DocstringSectionExamples | None, int]:
645 text, new_offset = _read_block(docstring, offset=offset, **options)
647 sub_sections: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = []
648 in_code_example = False
649 in_code_block = False
650 current_text: list[str] = []
651 current_example: list[str] = []
653 for line in text.split("\n"):
654 if _is_empty_line(line):
655 if in_code_example:
656 if current_example: 656 ↛ 659line 656 didn't jump to line 659 because the condition on line 656 was always true
657 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
658 current_example = []
659 in_code_example = False
660 else:
661 current_text.append(line)
663 elif in_code_example:
664 if trim_doctest_flags:
665 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
666 line = _RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901
667 current_example.append(line)
669 elif line.startswith("```"):
670 in_code_block = not in_code_block
671 current_text.append(line)
673 elif in_code_block:
674 current_text.append(line)
676 elif line.startswith(">>>"):
677 if current_text:
678 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
679 current_text = []
680 in_code_example = True
682 if trim_doctest_flags:
683 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
684 current_example.append(line)
686 else:
687 current_text.append(line)
689 if current_text: 689 ↛ 690line 689 didn't jump to line 690 because the condition on line 689 was never true
690 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
691 elif current_example: 691 ↛ 694line 691 didn't jump to line 694 because the condition on line 691 was always true
692 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
694 return DocstringSectionExamples(sub_sections), new_offset
697def _read_deprecated_section(
698 docstring: Docstring,
699 *,
700 offset: int,
701 **options: Any,
702) -> tuple[DocstringSectionDeprecated | None, int]:
703 text, new_offset = _read_block(docstring, offset=offset, **options)
705 # check the presence of a name and description, separated by a semi-colon
706 try:
707 version, text = text.split(":", 1)
708 except ValueError:
709 docstring_warning(docstring, new_offset, f"Could not parse version, text at line {offset}")
710 return None, new_offset
712 version = version.lstrip()
713 description = text.lstrip()
715 return (
716 DocstringSectionDeprecated(version=version, text=description),
717 new_offset,
718 )
721def _is_empty_line(line: str) -> bool:
722 return not line.strip()
725_section_reader = {
726 DocstringSectionKind.parameters: _read_parameters_section,
727 DocstringSectionKind.other_parameters: _read_other_parameters_section,
728 DocstringSectionKind.raises: _read_raises_section,
729 DocstringSectionKind.warns: _read_warns_section,
730 DocstringSectionKind.examples: _read_examples_section,
731 DocstringSectionKind.attributes: _read_attributes_section,
732 DocstringSectionKind.functions: _read_functions_section,
733 DocstringSectionKind.classes: _read_classes_section,
734 DocstringSectionKind.modules: _read_modules_section,
735 DocstringSectionKind.returns: _read_returns_section,
736 DocstringSectionKind.yields: _read_yields_section,
737 DocstringSectionKind.receives: _read_receives_section,
738 DocstringSectionKind.deprecated: _read_deprecated_section,
739}
741_sentinel = object()
744def parse_google(
745 docstring: Docstring,
746 *,
747 ignore_init_summary: bool = False,
748 trim_doctest_flags: bool = True,
749 returns_multiple_items: bool = True,
750 warn_unknown_params: bool = True,
751 returns_named_value: bool = True,
752 returns_type_in_property_summary: bool = False,
753 **options: Any,
754) -> list[DocstringSection]:
755 """Parse a Google-style docstring.
757 This function iterates on lines of a docstring to build sections.
758 It then returns this list of sections.
760 Parameters:
761 docstring: The docstring to parse.
762 ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings.
763 trim_doctest_flags: Whether to remove doctest flags from Python example blocks.
764 returns_multiple_items: Whether the `Returns` section has multiple items.
765 warn_unknown_params: Warn about documented parameters not appearing in the signature.
766 returns_named_value: Whether to parse `thing: Description` in returns sections as a name and description,
767 rather than a type and description. When true, type must be wrapped in parentheses: `(int): Description.`.
768 When false, parentheses are optional but the items cannot be named: `int: Description`.
769 returns_type_in_property_summary: Whether to parse the return type of properties
770 at the beginning of their summary: `str: Summary of the property`.
771 **options: Additional parsing options.
773 Returns:
774 A list of docstring sections.
775 """
776 sections: list[DocstringSection] = []
777 current_section = []
779 in_code_block = False
780 lines = docstring.lines
782 options = {
783 "ignore_init_summary": ignore_init_summary,
784 "trim_doctest_flags": trim_doctest_flags,
785 "returns_multiple_items": returns_multiple_items,
786 "warn_unknown_params": warn_unknown_params,
787 "returns_named_value": returns_named_value,
788 "returns_type_in_property_summary": returns_type_in_property_summary,
789 **options,
790 }
792 ignore_summary = (
793 options["ignore_init_summary"]
794 and docstring.parent is not None
795 and docstring.parent.name == "__init__"
796 and docstring.parent.is_function
797 and docstring.parent.parent is not None
798 and docstring.parent.parent.is_class
799 )
801 offset = 2 if ignore_summary else 0
803 while offset < len(lines):
804 line_lower = lines[offset].lower()
806 if in_code_block:
807 if line_lower.lstrip(" ").startswith("```"):
808 in_code_block = False
809 current_section.append(lines[offset])
811 elif line_lower.lstrip(" ").startswith("```"):
812 in_code_block = True
813 current_section.append(lines[offset])
815 elif match := _RE_ADMONITION.match(lines[offset]):
816 groups = match.groupdict()
817 title = groups["title"]
818 admonition_type = groups["type"]
819 is_section = admonition_type.lower() in _section_kind
821 has_previous_line = offset > 0
822 blank_line_above = not has_previous_line or _is_empty_line(lines[offset - 1])
823 has_next_line = offset < len(lines) - 1
824 has_next_lines = offset < len(lines) - 2
825 blank_line_below = has_next_line and _is_empty_line(lines[offset + 1])
826 blank_lines_below = has_next_lines and _is_empty_line(lines[offset + 2])
827 indented_line_below = has_next_line and not blank_line_below and lines[offset + 1].startswith(" ")
828 indented_lines_below = has_next_lines and not blank_lines_below and lines[offset + 2].startswith(" ")
829 if not (indented_line_below or indented_lines_below):
830 # Do not warn when there are no contents,
831 # this is most probably not a section or admonition.
832 current_section.append(lines[offset])
833 offset += 1
834 continue
835 reasons = []
836 kind = "section" if is_section else "admonition"
837 if (indented_line_below or indented_lines_below) and not blank_line_above:
838 reasons.append(f"Missing blank line above {kind}")
839 if indented_lines_below and blank_line_below:
840 reasons.append(f"Extraneous blank line below {kind} title")
841 if reasons:
842 reasons_string = "; ".join(reasons)
843 docstring_warning(
844 docstring,
845 offset,
846 f"Possible {kind} skipped, reasons: {reasons_string}",
847 LogLevel.debug,
848 )
849 current_section.append(lines[offset])
850 offset += 1
851 continue
853 if is_section:
854 if current_section:
855 if any(current_section): 855 ↛ 857line 855 didn't jump to line 857 because the condition on line 855 was always true
856 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
857 current_section = []
858 reader = _section_reader[_section_kind[admonition_type.lower()]]
859 section, offset = reader(docstring, offset=offset + 1, **options) # type: ignore[operator]
860 if section:
861 section.title = title
862 sections.append(section)
864 else:
865 contents, offset = _read_block(docstring, offset=offset + 1)
866 if contents: 866 ↛ 876line 866 didn't jump to line 876 because the condition on line 866 was always true
867 if current_section:
868 if any(current_section): 868 ↛ 870line 868 didn't jump to line 870 because the condition on line 868 was always true
869 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
870 current_section = []
871 if title is None:
872 title = admonition_type
873 admonition_type = admonition_type.lower().replace(" ", "-")
874 sections.append(DocstringSectionAdmonition(kind=admonition_type, text=contents, title=title))
875 else:
876 with suppress(IndexError):
877 current_section.append(lines[offset])
878 else:
879 current_section.append(lines[offset])
881 offset += 1
883 if current_section:
884 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
886 if (
887 returns_type_in_property_summary
888 and sections
889 and docstring.parent
890 and docstring.parent.is_attribute
891 and "property" in docstring.parent.labels
892 ):
893 lines = sections[0].value.lstrip().split("\n")
894 if ":" in lines[0]: 894 ↛ 904line 894 didn't jump to line 904 because the condition on line 894 was always true
895 annotation, line = lines[0].split(":", 1)
896 lines = [line, *lines[1:]]
897 sections[0].value = "\n".join(lines)
898 sections.append(
899 DocstringSectionReturns(
900 [DocstringReturn("", description="", annotation=parse_docstring_annotation(annotation, docstring))],
901 ),
902 )
904 return sections