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

1# This module defines functions to parse Google-style docstrings into structured data. 

2 

3from __future__ import annotations 

4 

5import re 

6from contextlib import suppress 

7from typing import TYPE_CHECKING 

8 

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 

39 

40if TYPE_CHECKING: 

41 from re import Pattern 

42 from typing import Any, Literal 

43 

44 from _griffe.expressions import Expr 

45 from _griffe.models import Docstring 

46 

47 

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} 

73 

74_BlockItem = tuple[int, list[str]] 

75_BlockItems = list[_BlockItem] 

76_ItemsBlock = tuple[_BlockItems, int] 

77 

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:.+)$") 

82 

83 

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 

88 

89 new_offset = offset 

90 items: _BlockItems = [] 

91 

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 

95 

96 # get initial indent 

97 indent = len(lines[new_offset]) - len(lines[new_offset].lstrip()) 

98 

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 

102 

103 # start processing first item 

104 current_item = (new_offset, [lines[new_offset][indent:]]) 

105 new_offset += 1 

106 

107 # loop on next lines 

108 while new_offset < len(lines): 

109 line = lines[new_offset] 

110 

111 if _is_empty_line(line): 

112 # empty line: preserve it in the current item 

113 current_item[1].append("") 

114 

115 elif line.startswith(indent * 2 * " "): 

116 # continuation line 

117 current_item[1].append(line[indent * 2 :]) 

118 

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 ) 

129 

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:]]) 

134 

135 else: 

136 # indent lower than initial one: end of section 

137 break 

138 

139 new_offset += 1 

140 

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) 

143 

144 return items, new_offset - 1 

145 

146 

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 

151 

152 new_offset = offset 

153 block: list[str] = [] 

154 

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 

158 

159 # get initial indent 

160 indent = len(lines[new_offset]) - len(lines[new_offset].lstrip()) 

161 

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 

165 

166 # start processing first item 

167 block.append(lines[new_offset].lstrip()) 

168 new_offset += 1 

169 

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 

174 

175 return "\n".join(block).rstrip("\n"), new_offset - 1 

176 

177 

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 

187 

188 block, new_offset = _read_block_items(docstring, offset=offset, **options) 

189 

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 

197 

198 description = "\n".join([description.lstrip(), *param_lines[1:]]).rstrip("\n") 

199 

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 

215 

216 try: 

217 default = docstring.parent.parameters[name].default # type: ignore[union-attr] 

218 except (AttributeError, KeyError): 

219 default = None 

220 

221 if annotation is None: 

222 docstring_warning(docstring, line_number, f"No type or annotation for parameter '{name}'") 

223 

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) 

234 

235 parameters.append(DocstringParameter(name=name, value=default, annotation=annotation, description=description)) 

236 

237 return parameters, new_offset 

238 

239 

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 

248 

249 

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 

259 

260 

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) 

269 

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 

277 

278 description = "\n".join([description.lstrip(), *attr_lines[1:]]).rstrip("\n") 

279 

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] 

291 

292 attributes.append(DocstringAttribute(name=name, annotation=annotation, description=description)) 

293 

294 return DocstringSectionAttributes(attributes), new_offset 

295 

296 

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) 

305 

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 

317 

318 description = "\n".join([description.lstrip(), *func_lines[1:]]).rstrip("\n") 

319 

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 

326 

327 functions.append(DocstringFunction(name=name, annotation=signature, description=description)) 

328 

329 return DocstringSectionFunctions(functions), new_offset 

330 

331 

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) 

340 

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 

352 

353 description = "\n".join([description.lstrip(), *class_lines[1:]]).rstrip("\n") 

354 

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 

361 

362 classes.append(DocstringClass(name=name, annotation=signature, description=description)) 

363 

364 return DocstringSectionClasses(classes), new_offset 

365 

366 

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) 

375 

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)) 

388 

389 return DocstringSectionModules(modules), new_offset 

390 

391 

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) 

400 

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)) 

416 

417 return DocstringSectionRaises(exceptions), new_offset 

418 

419 

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) 

428 

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)) 

441 

442 return DocstringSectionWarns(warns), new_offset 

443 

444 

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 

456 

457 

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 

485 

486 

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 

504 

505 

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 = [] 

515 

516 block, new_offset = _read_block_items_maybe( 

517 docstring, 

518 offset=offset, 

519 multiple=returns_multiple_items, 

520 **options, 

521 ) 

522 

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 

533 

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) 

540 

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}") 

544 

545 returns.append(DocstringReturn(name=name or "", annotation=annotation, description=description)) 

546 

547 return DocstringSectionReturns(returns), new_offset 

548 

549 

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 = [] 

559 

560 block, new_offset = _read_block_items_maybe( 

561 docstring, 

562 offset=offset, 

563 multiple=returns_multiple_items, 

564 **options, 

565 ) 

566 

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 

577 

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) 

584 

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}") 

588 

589 yields.append(DocstringYield(name=name or "", annotation=annotation, description=description)) 

590 

591 return DocstringSectionYields(yields), new_offset 

592 

593 

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 = [] 

603 

604 block, new_offset = _read_block_items_maybe( 

605 docstring, 

606 offset=offset, 

607 multiple=receives_multiple_items, 

608 **options, 

609 ) 

610 

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 

621 

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) 

628 

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}") 

632 

633 receives.append(DocstringReceive(name=name or "", annotation=annotation, description=description)) 

634 

635 return DocstringSectionReceives(receives), new_offset 

636 

637 

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) 

646 

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] = [] 

652 

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) 

662 

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) 

668 

669 elif line.startswith("```"): 

670 in_code_block = not in_code_block 

671 current_text.append(line) 

672 

673 elif in_code_block: 

674 current_text.append(line) 

675 

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 

681 

682 if trim_doctest_flags: 

683 line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901 

684 current_example.append(line) 

685 

686 else: 

687 current_text.append(line) 

688 

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))) 

693 

694 return DocstringSectionExamples(sub_sections), new_offset 

695 

696 

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) 

704 

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 

711 

712 version = version.lstrip() 

713 description = text.lstrip() 

714 

715 return ( 

716 DocstringSectionDeprecated(version=version, text=description), 

717 new_offset, 

718 ) 

719 

720 

721def _is_empty_line(line: str) -> bool: 

722 return not line.strip() 

723 

724 

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} 

740 

741_sentinel = object() 

742 

743 

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. 

756 

757 This function iterates on lines of a docstring to build sections. 

758 It then returns this list of sections. 

759 

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. 

772 

773 Returns: 

774 A list of docstring sections. 

775 """ 

776 sections: list[DocstringSection] = [] 

777 current_section = [] 

778 

779 in_code_block = False 

780 lines = docstring.lines 

781 

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 } 

791 

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 ) 

800 

801 offset = 2 if ignore_summary else 0 

802 

803 while offset < len(lines): 

804 line_lower = lines[offset].lower() 

805 

806 if in_code_block: 

807 if line_lower.lstrip(" ").startswith("```"): 

808 in_code_block = False 

809 current_section.append(lines[offset]) 

810 

811 elif line_lower.lstrip(" ").startswith("```"): 

812 in_code_block = True 

813 current_section.append(lines[offset]) 

814 

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 

820 

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 

852 

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) 

863 

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]) 

880 

881 offset += 1 

882 

883 if current_section: 

884 sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n"))) 

885 

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 ) 

903 

904 return sections