Coverage for src/_griffe/docstrings/google.py: 86.16%
436 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:47 +0200
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:47 +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, List, Tuple
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
39from _griffe.expressions import ExprName
41if TYPE_CHECKING:
42 from typing import Any, Literal, Pattern
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.I)
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_returns_section(
446 docstring: Docstring,
447 *,
448 offset: int,
449 returns_multiple_items: bool = True,
450 returns_named_value: bool = True,
451 **options: Any,
452) -> tuple[DocstringSectionReturns | None, int]:
453 returns = []
455 if returns_multiple_items:
456 block, new_offset = _read_block_items(docstring, offset=offset, **options)
457 else:
458 one_block, new_offset = _read_block(docstring, offset=offset, **options)
459 block = [(new_offset, one_block.splitlines())]
461 for index, (line_number, return_lines) in enumerate(block):
462 if returns_named_value:
463 match = _RE_NAME_ANNOTATION_DESCRIPTION.match(return_lines[0])
464 if not match: 464 ↛ 465line 464 didn't jump to line 465 because the condition on line 464 was never true
465 docstring_warning(
466 docstring,
467 line_number,
468 f"Failed to get name, annotation or description from '{return_lines[0]}'",
469 )
470 continue
471 name, annotation, description = match.groups()
472 else:
473 name = None
474 if ":" in return_lines[0]:
475 annotation, description = return_lines[0].split(":", 1)
476 annotation = annotation.lstrip("(").rstrip(")")
477 else:
478 annotation = None
479 description = return_lines[0]
480 description = "\n".join([description.lstrip(), *return_lines[1:]]).rstrip("\n")
482 if annotation:
483 # try to compile the annotation to transform it into an expression
484 annotation = parse_docstring_annotation(annotation, docstring)
485 else:
486 # try to retrieve the annotation from the docstring parent
487 with suppress(AttributeError, KeyError, ValueError):
488 if docstring.parent.is_function: # type: ignore[union-attr] 488 ↛ 490line 488 didn't jump to line 490 because the condition on line 488 was always true
489 annotation = docstring.parent.returns # type: ignore[union-attr]
490 elif docstring.parent.is_attribute: # type: ignore[union-attr]
491 annotation = docstring.parent.annotation # type: ignore[union-attr]
492 else:
493 raise ValueError
494 if len(block) > 1:
495 if annotation.is_tuple:
496 annotation = annotation.slice.elements[index]
497 else:
498 if annotation.is_iterator: 498 ↛ 499line 498 didn't jump to line 499 because the condition on line 498 was never true
499 return_item = annotation.slice
500 elif annotation.is_generator:
501 return_item = annotation.slice.elements[2]
502 else:
503 raise ValueError
504 if isinstance(return_item, ExprName): 504 ↛ 505line 504 didn't jump to line 505 because the condition on line 504 was never true
505 annotation = return_item
506 elif return_item.is_tuple: 506 ↛ 509line 506 didn't jump to line 509 because the condition on line 506 was always true
507 annotation = return_item.slice.elements[index]
508 else:
509 annotation = return_item
511 if annotation is None:
512 returned_value = repr(name) if name else index + 1
513 docstring_warning(docstring, line_number, f"No type or annotation for returned value {returned_value}")
515 returns.append(DocstringReturn(name=name or "", annotation=annotation, description=description))
517 return DocstringSectionReturns(returns), new_offset
520def _read_yields_section(
521 docstring: Docstring,
522 *,
523 offset: int,
524 **options: Any,
525) -> tuple[DocstringSectionYields | None, int]:
526 yields = []
527 block, new_offset = _read_block_items(docstring, offset=offset, **options)
529 for index, (line_number, yield_lines) in enumerate(block):
530 match = _RE_NAME_ANNOTATION_DESCRIPTION.match(yield_lines[0])
531 if not match: 531 ↛ 532line 531 didn't jump to line 532 because the condition on line 531 was never true
532 docstring_warning(
533 docstring,
534 line_number,
535 f"Failed to get name, annotation or description from '{yield_lines[0]}'",
536 )
537 continue
539 name, annotation, description = match.groups()
540 description = "\n".join([description.lstrip(), *yield_lines[1:]]).rstrip("\n")
542 if annotation:
543 # try to compile the annotation to transform it into an expression
544 annotation = parse_docstring_annotation(annotation, docstring)
545 else:
546 # try to retrieve the annotation from the docstring parent
547 with suppress(AttributeError, IndexError, KeyError, ValueError):
548 annotation = docstring.parent.annotation # type: ignore[union-attr]
549 if annotation.is_iterator:
550 yield_item = annotation.slice
551 elif annotation.is_generator: 551 ↛ 554line 551 didn't jump to line 554 because the condition on line 551 was always true
552 yield_item = annotation.slice.elements[0]
553 else:
554 raise ValueError
555 if isinstance(yield_item, ExprName):
556 annotation = yield_item
557 elif yield_item.is_tuple: 557 ↛ 560line 557 didn't jump to line 560 because the condition on line 557 was always true
558 annotation = yield_item.slice.elements[index]
559 else:
560 annotation = yield_item
562 if annotation is None:
563 yielded_value = repr(name) if name else index + 1
564 docstring_warning(docstring, line_number, f"No type or annotation for yielded value {yielded_value}")
566 yields.append(DocstringYield(name=name or "", annotation=annotation, description=description))
568 return DocstringSectionYields(yields), new_offset
571def _read_receives_section(
572 docstring: Docstring,
573 *,
574 offset: int,
575 **options: Any,
576) -> tuple[DocstringSectionReceives | None, int]:
577 receives = []
578 block, new_offset = _read_block_items(docstring, offset=offset, **options)
580 for index, (line_number, receive_lines) in enumerate(block):
581 match = _RE_NAME_ANNOTATION_DESCRIPTION.match(receive_lines[0])
582 if not match: 582 ↛ 583line 582 didn't jump to line 583 because the condition on line 582 was never true
583 docstring_warning(
584 docstring,
585 line_number,
586 f"Failed to get name, annotation or description from '{receive_lines[0]}'",
587 )
588 continue
590 name, annotation, description = match.groups()
591 description = "\n".join([description.lstrip(), *receive_lines[1:]]).rstrip("\n")
593 if annotation: 593 ↛ 595line 593 didn't jump to line 595 because the condition on line 593 was never true
594 # try to compile the annotation to transform it into an expression
595 annotation = parse_docstring_annotation(annotation, docstring)
596 else:
597 # try to retrieve the annotation from the docstring parent
598 with suppress(AttributeError, KeyError):
599 annotation = docstring.parent.returns # type: ignore[union-attr]
600 if annotation.is_generator: 600 ↛ 598line 600 didn't jump to line 598
601 receives_item = annotation.slice.elements[1]
602 if isinstance(receives_item, ExprName):
603 annotation = receives_item
604 elif receives_item.is_tuple: 604 ↛ 607line 604 didn't jump to line 607 because the condition on line 604 was always true
605 annotation = receives_item.slice.elements[index]
606 else:
607 annotation = receives_item
609 if annotation is None:
610 received_value = repr(name) if name else index + 1
611 docstring_warning(docstring, line_number, f"No type or annotation for received value {received_value}")
613 receives.append(DocstringReceive(name=name or "", annotation=annotation, description=description))
615 return DocstringSectionReceives(receives), new_offset
618def _read_examples_section(
619 docstring: Docstring,
620 *,
621 offset: int,
622 trim_doctest_flags: bool = True,
623 **options: Any,
624) -> tuple[DocstringSectionExamples | None, int]:
625 text, new_offset = _read_block(docstring, offset=offset, **options)
627 sub_sections: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = []
628 in_code_example = False
629 in_code_block = False
630 current_text: list[str] = []
631 current_example: list[str] = []
633 for line in text.split("\n"):
634 if _is_empty_line(line):
635 if in_code_example:
636 if current_example: 636 ↛ 639line 636 didn't jump to line 639 because the condition on line 636 was always true
637 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
638 current_example = []
639 in_code_example = False
640 else:
641 current_text.append(line)
643 elif in_code_example:
644 if trim_doctest_flags:
645 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
646 line = _RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901
647 current_example.append(line)
649 elif line.startswith("```"):
650 in_code_block = not in_code_block
651 current_text.append(line)
653 elif in_code_block:
654 current_text.append(line)
656 elif line.startswith(">>>"):
657 if current_text:
658 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
659 current_text = []
660 in_code_example = True
662 if trim_doctest_flags:
663 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901
664 current_example.append(line)
666 else:
667 current_text.append(line)
669 if current_text: 669 ↛ 670line 669 didn't jump to line 670 because the condition on line 669 was never true
670 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n")))
671 elif current_example: 671 ↛ 674line 671 didn't jump to line 674 because the condition on line 671 was always true
672 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example)))
674 return DocstringSectionExamples(sub_sections), new_offset
677def _read_deprecated_section(
678 docstring: Docstring,
679 *,
680 offset: int,
681 **options: Any,
682) -> tuple[DocstringSectionDeprecated | None, int]:
683 text, new_offset = _read_block(docstring, offset=offset, **options)
685 # check the presence of a name and description, separated by a semi-colon
686 try:
687 version, text = text.split(":", 1)
688 except ValueError:
689 docstring_warning(docstring, new_offset, f"Could not parse version, text at line {offset}")
690 return None, new_offset
692 version = version.lstrip()
693 description = text.lstrip()
695 return (
696 DocstringSectionDeprecated(version=version, text=description),
697 new_offset,
698 )
701def _is_empty_line(line: str) -> bool:
702 return not line.strip()
705_section_reader = {
706 DocstringSectionKind.parameters: _read_parameters_section,
707 DocstringSectionKind.other_parameters: _read_other_parameters_section,
708 DocstringSectionKind.raises: _read_raises_section,
709 DocstringSectionKind.warns: _read_warns_section,
710 DocstringSectionKind.examples: _read_examples_section,
711 DocstringSectionKind.attributes: _read_attributes_section,
712 DocstringSectionKind.functions: _read_functions_section,
713 DocstringSectionKind.classes: _read_classes_section,
714 DocstringSectionKind.modules: _read_modules_section,
715 DocstringSectionKind.returns: _read_returns_section,
716 DocstringSectionKind.yields: _read_yields_section,
717 DocstringSectionKind.receives: _read_receives_section,
718 DocstringSectionKind.deprecated: _read_deprecated_section,
719}
721_sentinel = object()
724def parse_google(
725 docstring: Docstring,
726 *,
727 ignore_init_summary: bool = False,
728 trim_doctest_flags: bool = True,
729 returns_multiple_items: bool = True,
730 warn_unknown_params: bool = True,
731 returns_named_value: bool = True,
732 returns_type_in_property_summary: bool = False,
733 **options: Any,
734) -> list[DocstringSection]:
735 """Parse a Google-style docstring.
737 This function iterates on lines of a docstring to build sections.
738 It then returns this list of sections.
740 Parameters:
741 docstring: The docstring to parse.
742 ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings.
743 trim_doctest_flags: Whether to remove doctest flags from Python example blocks.
744 returns_multiple_items: Whether the `Returns` section has multiple items.
745 warn_unknown_params: Warn about documented parameters not appearing in the signature.
746 returns_named_value: Whether to parse `thing: Description` in returns sections as a name and description,
747 rather than a type and description. When true, type must be wrapped in parentheses: `(int): Description.`.
748 When false, parentheses are optional but the items cannot be named: `int: Description`.
749 returns_type_in_property_summary: Whether to parse the return type of properties
750 at the beginning of their summary: `str: Summary of the property`.
751 **options: Additional parsing options.
753 Returns:
754 A list of docstring sections.
755 """
756 sections: list[DocstringSection] = []
757 current_section = []
759 in_code_block = False
760 lines = docstring.lines
762 options = {
763 "ignore_init_summary": ignore_init_summary,
764 "trim_doctest_flags": trim_doctest_flags,
765 "returns_multiple_items": returns_multiple_items,
766 "warn_unknown_params": warn_unknown_params,
767 "returns_named_value": returns_named_value,
768 "returns_type_in_property_summary": returns_type_in_property_summary,
769 **options,
770 }
772 ignore_summary = (
773 options["ignore_init_summary"]
774 and docstring.parent is not None
775 and docstring.parent.name == "__init__"
776 and docstring.parent.is_function
777 and docstring.parent.parent is not None
778 and docstring.parent.parent.is_class
779 )
781 offset = 2 if ignore_summary else 0
783 while offset < len(lines):
784 line_lower = lines[offset].lower()
786 if in_code_block:
787 if line_lower.lstrip(" ").startswith("```"):
788 in_code_block = False
789 current_section.append(lines[offset])
791 elif line_lower.lstrip(" ").startswith("```"):
792 in_code_block = True
793 current_section.append(lines[offset])
795 elif match := _RE_ADMONITION.match(lines[offset]):
796 groups = match.groupdict()
797 title = groups["title"]
798 admonition_type = groups["type"]
799 is_section = admonition_type.lower() in _section_kind
801 has_previous_line = offset > 0
802 blank_line_above = not has_previous_line or _is_empty_line(lines[offset - 1])
803 has_next_line = offset < len(lines) - 1
804 has_next_lines = offset < len(lines) - 2
805 blank_line_below = has_next_line and _is_empty_line(lines[offset + 1])
806 blank_lines_below = has_next_lines and _is_empty_line(lines[offset + 2])
807 indented_line_below = has_next_line and not blank_line_below and lines[offset + 1].startswith(" ")
808 indented_lines_below = has_next_lines and not blank_lines_below and lines[offset + 2].startswith(" ")
809 if not (indented_line_below or indented_lines_below):
810 # Do not warn when there are no contents,
811 # this is most probably not a section or admonition.
812 current_section.append(lines[offset])
813 offset += 1
814 continue
815 reasons = []
816 kind = "section" if is_section else "admonition"
817 if (indented_line_below or indented_lines_below) and not blank_line_above:
818 reasons.append(f"Missing blank line above {kind}")
819 if indented_lines_below and blank_line_below:
820 reasons.append(f"Extraneous blank line below {kind} title")
821 if reasons:
822 reasons_string = "; ".join(reasons)
823 docstring_warning(
824 docstring,
825 offset,
826 f"Possible {kind} skipped, reasons: {reasons_string}",
827 LogLevel.debug,
828 )
829 current_section.append(lines[offset])
830 offset += 1
831 continue
833 if is_section:
834 if current_section:
835 if any(current_section): 835 ↛ 837line 835 didn't jump to line 837 because the condition on line 835 was always true
836 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
837 current_section = []
838 reader = _section_reader[_section_kind[admonition_type.lower()]]
839 section, offset = reader(docstring, offset=offset + 1, **options) # type: ignore[operator]
840 if section:
841 section.title = title
842 sections.append(section)
844 else:
845 contents, offset = _read_block(docstring, offset=offset + 1)
846 if contents: 846 ↛ 856line 846 didn't jump to line 856 because the condition on line 846 was always true
847 if current_section:
848 if any(current_section): 848 ↛ 850line 848 didn't jump to line 850 because the condition on line 848 was always true
849 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
850 current_section = []
851 if title is None:
852 title = admonition_type
853 admonition_type = admonition_type.lower().replace(" ", "-")
854 sections.append(DocstringSectionAdmonition(kind=admonition_type, text=contents, title=title))
855 else:
856 with suppress(IndexError):
857 current_section.append(lines[offset])
858 else:
859 current_section.append(lines[offset])
861 offset += 1
863 if current_section:
864 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
866 if (
867 returns_type_in_property_summary
868 and sections
869 and docstring.parent
870 and docstring.parent.is_attribute
871 and "property" in docstring.parent.labels
872 ):
873 lines = sections[0].value.lstrip().split("\n")
874 if ":" in lines[0]: 874 ↛ 884line 874 didn't jump to line 884 because the condition on line 874 was always true
875 annotation, line = lines[0].split(":", 1)
876 lines = [line, *lines[1:]]
877 sections[0].value = "\n".join(lines)
878 sections.append(
879 DocstringSectionReturns(
880 [DocstringReturn("", description="", annotation=parse_docstring_annotation(annotation, docstring))],
881 ),
882 )
884 return sections