Coverage for src/griffe/_internal/docstrings/google.py: 86.37%

461 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-14 23:10 +0200

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._internal.docstrings.models import ( 

10 DocstringAttribute, 

11 DocstringClass, 

12 DocstringFunction, 

13 DocstringModule, 

14 DocstringParameter, 

15 DocstringRaise, 

16 DocstringReceive, 

17 DocstringReturn, 

18 DocstringSection, 

19 DocstringSectionAdmonition, 

20 DocstringSectionAttributes, 

21 DocstringSectionClasses, 

22 DocstringSectionExamples, 

23 DocstringSectionFunctions, 

24 DocstringSectionModules, 

25 DocstringSectionOtherParameters, 

26 DocstringSectionParameters, 

27 DocstringSectionRaises, 

28 DocstringSectionReceives, 

29 DocstringSectionReturns, 

30 DocstringSectionText, 

31 DocstringSectionTypeAliases, 

32 DocstringSectionTypeParameters, 

33 DocstringSectionWarns, 

34 DocstringSectionYields, 

35 DocstringTypeAlias, 

36 DocstringTypeParameter, 

37 DocstringWarn, 

38 DocstringYield, 

39) 

40from griffe._internal.docstrings.utils import docstring_warning, parse_docstring_annotation 

41from griffe._internal.enumerations import DocstringSectionKind, LogLevel 

42 

43if TYPE_CHECKING: 

44 from re import Pattern 

45 from typing import Any, Literal 

46 

47 from griffe._internal.expressions import Expr 

48 from griffe._internal.models import Docstring 

49 

50 

51_section_kind = { 

52 "args": DocstringSectionKind.parameters, 

53 "arguments": DocstringSectionKind.parameters, 

54 "params": DocstringSectionKind.parameters, 

55 "parameters": DocstringSectionKind.parameters, 

56 "keyword args": DocstringSectionKind.other_parameters, 

57 "keyword arguments": DocstringSectionKind.other_parameters, 

58 "other args": DocstringSectionKind.other_parameters, 

59 "other arguments": DocstringSectionKind.other_parameters, 

60 "other params": DocstringSectionKind.other_parameters, 

61 "other parameters": DocstringSectionKind.other_parameters, 

62 "type args": DocstringSectionKind.type_parameters, 

63 "type arguments": DocstringSectionKind.type_parameters, 

64 "type params": DocstringSectionKind.type_parameters, 

65 "type parameters": DocstringSectionKind.type_parameters, 

66 "raises": DocstringSectionKind.raises, 

67 "exceptions": DocstringSectionKind.raises, 

68 "returns": DocstringSectionKind.returns, 

69 "yields": DocstringSectionKind.yields, 

70 "receives": DocstringSectionKind.receives, 

71 "examples": DocstringSectionKind.examples, 

72 "attributes": DocstringSectionKind.attributes, 

73 "functions": DocstringSectionKind.functions, 

74 "methods": DocstringSectionKind.functions, 

75 "classes": DocstringSectionKind.classes, 

76 "type aliases": DocstringSectionKind.type_aliases, 

77 "modules": DocstringSectionKind.modules, 

78 "warns": DocstringSectionKind.warns, 

79 "warnings": DocstringSectionKind.warns, 

80} 

81 

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

83_BlockItems = list[_BlockItem] 

84_ItemsBlock = tuple[_BlockItems, int] 

85 

86_RE_ADMONITION: Pattern = re.compile(r"^(?P<type>[\w][\s\w-]*):(\s+(?P<title>[^\s].*))?\s*$", re.IGNORECASE) 

87_RE_NAME_ANNOTATION_DESCRIPTION: Pattern = re.compile(r"^(?:(?P<name>\w+)?\s*(?:\((?P<type>.+)\))?:\s*)?(?P<desc>.*)$") 

88_RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$") 

89_RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$") 

90 

91 

92def _read_block_items(docstring: Docstring, *, offset: int, warnings: bool = True, **options: Any) -> _ItemsBlock: # noqa: ARG001 

93 lines = docstring.lines 

94 if offset >= len(lines): 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true

95 return [], offset 

96 

97 new_offset = offset 

98 items: _BlockItems = [] 

99 

100 # Skip first empty lines. 

101 while _is_empty_line(lines[new_offset]): 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true

102 new_offset += 1 

103 

104 # Get initial indent. 

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

106 

107 if indent == 0: 107 ↛ 109line 107 didn't jump to line 109 because the condition on line 107 was never true

108 # First non-empty line was not indented, abort. 

109 return [], new_offset - 1 

110 

111 # Start processing first item. 

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

113 new_offset += 1 

114 

115 # Loop on next lines. 

116 while new_offset < len(lines): 

117 line = lines[new_offset] 

118 

119 if _is_empty_line(line): 

120 # Empty line: preserve it in the current item. 

121 current_item[1].append("") 

122 

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

124 # Continuation line. 

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

126 

127 elif line.startswith((indent + 1) * " "): 

128 # Indent between initial and continuation: append but warn. 

129 cont_indent = len(line) - len(line.lstrip()) 

130 current_item[1].append(line[cont_indent:]) 

131 if warnings: 131 ↛ 148line 131 didn't jump to line 148 because the condition on line 131 was always true

132 docstring_warning( 

133 docstring, 

134 new_offset, 

135 f"Confusing indentation for continuation line {new_offset + 1} in docstring, " 

136 f"should be {indent} * 2 = {indent * 2} spaces, not {cont_indent}", 

137 ) 

138 

139 elif line.startswith(indent * " "): 

140 # Indent equal to initial one: new item. 

141 items.append(current_item) 

142 current_item = (new_offset, [line[indent:]]) 

143 

144 else: 

145 # Indent lower than initial one: end of section. 

146 break 

147 

148 new_offset += 1 

149 

150 if current_item: 150 ↛ 153line 150 didn't jump to line 153 because the condition on line 150 was always true

151 items.append(current_item) 

152 

153 return items, new_offset - 1 

154 

155 

156def _read_block(docstring: Docstring, *, offset: int, **options: Any) -> tuple[str, int]: # noqa: ARG001 

157 lines = docstring.lines 

158 if offset >= len(lines): 158 ↛ 159line 158 didn't jump to line 159 because the condition on line 158 was never true

159 return "", offset - 1 

160 

161 new_offset = offset 

162 block: list[str] = [] 

163 

164 # skip first empty lines. 

165 while _is_empty_line(lines[new_offset]): 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true

166 new_offset += 1 

167 

168 # Get initial indent. 

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

170 

171 if indent == 0: 171 ↛ 173line 171 didn't jump to line 173 because the condition on line 171 was never true

172 # First non-empty line was not indented, abort. 

173 return "", offset - 1 

174 

175 # Start processing first item. 

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

177 new_offset += 1 

178 

179 # Loop on next lines. 

180 while new_offset < len(lines) and (lines[new_offset].startswith(indent * " ") or _is_empty_line(lines[new_offset])): 

181 block.append(lines[new_offset][indent:]) 

182 new_offset += 1 

183 

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

185 

186 

187def _read_parameters( 

188 docstring: Docstring, 

189 *, 

190 offset: int, 

191 warn_unknown_params: bool = True, 

192 warn_missing_types: bool = True, 

193 warnings: bool = True, 

194 **options: Any, 

195) -> tuple[list[DocstringParameter], int]: 

196 parameters = [] 

197 annotation: str | Expr | None 

198 

199 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

200 

201 for line_number, param_lines in block: 

202 # Check the presence of a name and description, separated by a colon. 

203 try: 

204 name_with_type, description = param_lines[0].split(":", 1) 

205 except ValueError: 

206 if warnings: 206 ↛ 212line 206 didn't jump to line 212 because the condition on line 206 was always true

207 docstring_warning( 

208 docstring, 

209 line_number, 

210 f"Failed to get 'name: description' pair from '{param_lines[0]}'", 

211 ) 

212 continue 

213 

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

215 

216 # Use the type given after the parameter name, if any. 

217 if " " in name_with_type: 

218 name, annotation = name_with_type.split(" ", 1) 

219 annotation = annotation.strip("()") 

220 annotation = annotation.removesuffix(", optional") 

221 # Try to compile the annotation to transform it into an expression. 

222 annotation = parse_docstring_annotation(annotation, docstring) 

223 else: 

224 name = name_with_type 

225 # Try to use the annotation from the signature. 

226 try: 

227 annotation = docstring.parent.parameters[name].annotation # type: ignore[union-attr] 

228 except (AttributeError, KeyError): 

229 annotation = None 

230 

231 try: 

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

233 except (AttributeError, KeyError): 

234 default = None 

235 

236 if warnings and warn_missing_types and annotation is None: 

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

238 

239 if warnings and warn_unknown_params: 

240 with suppress(AttributeError): # For Parameters sections in objects without parameters. 

241 params = docstring.parent.parameters # type: ignore[union-attr] 

242 if name not in params: 

243 message = f"Parameter '{name}' does not appear in the function signature" 

244 for starred_name in (f"*{name}", f"**{name}"): 

245 if starred_name in params: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true

246 message += f". Did you mean '{starred_name}'?" 

247 break 

248 docstring_warning(docstring, line_number, message) 

249 

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

251 

252 return parameters, new_offset 

253 

254 

255def _read_parameters_section( 

256 docstring: Docstring, 

257 *, 

258 offset: int, 

259 **options: Any, 

260) -> tuple[DocstringSectionParameters | None, int]: 

261 parameters, new_offset = _read_parameters(docstring, offset=offset, **options) 

262 return DocstringSectionParameters(parameters), new_offset 

263 

264 

265def _read_other_parameters_section( 

266 docstring: Docstring, 

267 *, 

268 offset: int, 

269 warn_unknown_params: bool = True, # noqa: ARG001 

270 **options: Any, 

271) -> tuple[DocstringSectionOtherParameters | None, int]: 

272 parameters, new_offset = _read_parameters(docstring, offset=offset, warn_unknown_params=False, **options) 

273 return DocstringSectionOtherParameters(parameters), new_offset 

274 

275 

276def _read_type_parameters_section( 

277 docstring: Docstring, 

278 *, 

279 offset: int, 

280 warn_unknown_params: bool = True, 

281 **options: Any, 

282) -> tuple[DocstringSectionTypeParameters | None, int]: 

283 type_parameters = [] 

284 bound: str | Expr | None 

285 

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

287 

288 for line_number, type_param_lines in block: 

289 # check the presence of a name and description, separated by a colon 

290 try: 

291 name_with_bound, description = type_param_lines[0].split(":", 1) 

292 except ValueError: 

293 docstring_warning( 

294 docstring, 

295 line_number, 

296 f"Failed to get 'name: description' pair from '{type_param_lines[0]}'", 

297 ) 

298 continue 

299 

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

301 

302 # use the type given after the type parameter name, if any 

303 if " " in name_with_bound: 

304 name, bound = name_with_bound.split(" ", 1) 

305 if bound.startswith("(") and bound.endswith(")"): 

306 bound = bound[1:-1] 

307 # try to compile the annotation to transform it into an expression 

308 bound = parse_docstring_annotation(bound, docstring) 

309 else: 

310 name = name_with_bound 

311 # try to use the annotation from the signature 

312 try: 

313 bound = docstring.parent.type_parameters[name].annotation # type: ignore[union-attr] 

314 except (AttributeError, KeyError): 

315 bound = None 

316 

317 try: 

318 default = docstring.parent.type_parameters[name].default # type: ignore[union-attr] 

319 except (AttributeError, KeyError): 

320 default = None 

321 

322 if warn_unknown_params: 322 ↛ 333line 322 didn't jump to line 333 because the condition on line 322 was always true

323 with suppress(AttributeError): # for type parameters sections in objects without type parameters 

324 type_params = docstring.parent.type_parameters # type: ignore[union-attr] 

325 if name not in type_params: 

326 message = f"Type parameter '{name}' does not appear in the {docstring.parent.kind.value} signature" # type: ignore[union-attr] 

327 for starred_name in (f"*{name}", f"**{name}"): 

328 if starred_name in type_params: 328 ↛ 329line 328 didn't jump to line 329 because the condition on line 328 was never true

329 message += f". Did you mean '{starred_name}'?" 

330 break 

331 docstring_warning(docstring, line_number, message) 

332 

333 type_parameters.append( 

334 DocstringTypeParameter( 

335 name=name, 

336 value=default, 

337 annotation=bound, 

338 description=description, 

339 ), 

340 ) 

341 

342 return DocstringSectionTypeParameters(type_parameters), new_offset 

343 

344 

345def _read_attributes_section( 

346 docstring: Docstring, 

347 *, 

348 offset: int, 

349 warnings: bool = True, 

350 **options: Any, 

351) -> tuple[DocstringSectionAttributes | None, int]: 

352 attributes = [] 

353 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

354 

355 annotation: str | Expr | None = None 

356 for line_number, attr_lines in block: 

357 try: 

358 name_with_type, description = attr_lines[0].split(":", 1) 

359 except ValueError: 

360 if warnings: 

361 docstring_warning( 

362 docstring, 

363 line_number, 

364 f"Failed to get 'name: description' pair from '{attr_lines[0]}'", 

365 ) 

366 continue 

367 

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

369 

370 if " " in name_with_type: 370 ↛ 371line 370 didn't jump to line 371 because the condition on line 370 was never true

371 name, annotation = name_with_type.split(" ", 1) 

372 annotation = annotation.strip("()") 

373 annotation = annotation.removesuffix(", optional") 

374 # Try to compile the annotation to transform it into an expression. 

375 annotation = parse_docstring_annotation(annotation, docstring) 

376 else: 

377 name = name_with_type 

378 with suppress(AttributeError, KeyError, TypeError): 

379 # Use subscript syntax to fetch annotation from inherited members too. 

380 annotation = docstring.parent[name].annotation # type: ignore[index] 

381 

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

383 

384 return DocstringSectionAttributes(attributes), new_offset 

385 

386 

387def _read_functions_section( 

388 docstring: Docstring, 

389 *, 

390 offset: int, 

391 warnings: bool = True, 

392 **options: Any, 

393) -> tuple[DocstringSectionFunctions | None, int]: 

394 functions = [] 

395 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

396 

397 signature: str | Expr | None = None 

398 for line_number, func_lines in block: 

399 try: 

400 name_with_signature, description = func_lines[0].split(":", 1) 

401 except ValueError: 

402 if warnings: 

403 docstring_warning( 

404 docstring, 

405 line_number, 

406 f"Failed to get 'signature: description' pair from '{func_lines[0]}'", 

407 ) 

408 continue 

409 

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

411 

412 if "(" in name_with_signature: 

413 name = name_with_signature.split("(", 1)[0] 

414 signature = name_with_signature 

415 else: 

416 name = name_with_signature 

417 signature = None 

418 

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

420 

421 return DocstringSectionFunctions(functions), new_offset 

422 

423 

424def _read_classes_section( 

425 docstring: Docstring, 

426 *, 

427 offset: int, 

428 warnings: bool = True, 

429 **options: Any, 

430) -> tuple[DocstringSectionClasses | None, int]: 

431 classes = [] 

432 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

433 

434 signature: str | Expr | None = None 

435 for line_number, class_lines in block: 

436 try: 

437 name_with_signature, description = class_lines[0].split(":", 1) 

438 except ValueError: 

439 if warnings: 

440 docstring_warning( 

441 docstring, 

442 line_number, 

443 f"Failed to get 'signature: description' pair from '{class_lines[0]}'", 

444 ) 

445 continue 

446 

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

448 

449 if "(" in name_with_signature: 

450 name = name_with_signature.split("(", 1)[0] 

451 signature = name_with_signature 

452 else: 

453 name = name_with_signature 

454 signature = None 

455 

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

457 

458 return DocstringSectionClasses(classes), new_offset 

459 

460 

461def _read_type_aliases_section( 

462 docstring: Docstring, 

463 *, 

464 offset: int, 

465 **options: Any, 

466) -> tuple[DocstringSectionTypeAliases | None, int]: 

467 type_aliases = [] 

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

469 

470 for line_number, type_alias_lines in block: 

471 try: 

472 name, description = type_alias_lines[0].split(":", 1) 

473 except ValueError: 

474 docstring_warning( 

475 docstring, 

476 line_number, 

477 f"Failed to get 'name: description' pair from '{type_alias_lines[0]}'", 

478 ) 

479 continue 

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

481 type_aliases.append(DocstringTypeAlias(name=name, description=description)) 

482 

483 return DocstringSectionTypeAliases(type_aliases), new_offset 

484 

485 

486def _read_modules_section( 

487 docstring: Docstring, 

488 *, 

489 offset: int, 

490 warnings: bool = True, 

491 **options: Any, 

492) -> tuple[DocstringSectionModules | None, int]: 

493 modules = [] 

494 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

495 

496 for line_number, module_lines in block: 

497 try: 

498 name, description = module_lines[0].split(":", 1) 

499 except ValueError: 

500 if warnings: 

501 docstring_warning( 

502 docstring, 

503 line_number, 

504 f"Failed to get 'name: description' pair from '{module_lines[0]}'", 

505 ) 

506 continue 

507 

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

509 modules.append(DocstringModule(name=name, description=description)) 

510 

511 return DocstringSectionModules(modules), new_offset 

512 

513 

514def _read_raises_section( 

515 docstring: Docstring, 

516 *, 

517 offset: int, 

518 warnings: bool = True, 

519 **options: Any, 

520) -> tuple[DocstringSectionRaises | None, int]: 

521 exceptions = [] 

522 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

523 

524 annotation: str | Expr 

525 for line_number, exception_lines in block: 

526 try: 

527 annotation, description = exception_lines[0].split(":", 1) 

528 except ValueError: 

529 if warnings: 

530 docstring_warning( 

531 docstring, 

532 line_number, 

533 f"Failed to get 'exception: description' pair from '{exception_lines[0]}'", 

534 ) 

535 continue 

536 

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

538 # Try to compile the annotation to transform it into an expression. 

539 annotation = parse_docstring_annotation(annotation, docstring) 

540 exceptions.append(DocstringRaise(annotation=annotation, description=description)) 

541 

542 return DocstringSectionRaises(exceptions), new_offset 

543 

544 

545def _read_warns_section( 

546 docstring: Docstring, 

547 *, 

548 offset: int, 

549 warnings: bool = True, 

550 **options: Any, 

551) -> tuple[DocstringSectionWarns | None, int]: 

552 warns = [] 

553 block, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

554 

555 for line_number, warning_lines in block: 

556 try: 

557 annotation, description = warning_lines[0].split(":", 1) 

558 except ValueError: 

559 if warnings: 

560 docstring_warning( 

561 docstring, 

562 line_number, 

563 f"Failed to get 'warning: description' pair from '{warning_lines[0]}'", 

564 ) 

565 continue 

566 

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

568 warns.append(DocstringWarn(annotation=annotation, description=description)) 

569 

570 return DocstringSectionWarns(warns), new_offset 

571 

572 

573def _read_block_items_maybe( 

574 docstring: Docstring, 

575 *, 

576 offset: int, 

577 multiple: bool = True, 

578 **options: Any, 

579) -> _ItemsBlock: 

580 if multiple: 

581 return _read_block_items(docstring, offset=offset, **options) 

582 one_block, new_offset = _read_block(docstring, offset=offset, **options) 

583 return [(new_offset, one_block.splitlines())], new_offset 

584 

585 

586def _get_name_annotation_description( 

587 docstring: Docstring, 

588 line_number: int, 

589 lines: list[str], 

590 *, 

591 named: bool = True, 

592 warnings: bool = True, 

593) -> tuple[str | None, Any, str]: 

594 if named: 

595 match = _RE_NAME_ANNOTATION_DESCRIPTION.match(lines[0]) 

596 if not match: 596 ↛ 597line 596 didn't jump to line 597 because the condition on line 596 was never true

597 if warnings: 

598 docstring_warning( 

599 docstring, 

600 line_number, 

601 f"Failed to get name, annotation or description from '{lines[0]}'", 

602 ) 

603 raise ValueError 

604 name, annotation, description = match.groups() 

605 else: 

606 name = None 

607 if ":" in lines[0]: 

608 annotation, description = lines[0].split(":", 1) 

609 annotation = annotation.lstrip("(").rstrip(")") 

610 else: 

611 annotation = None 

612 description = lines[0] 

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

614 return name, annotation, description 

615 

616 

617def _annotation_from_parent( 

618 docstring: Docstring, 

619 *, 

620 gen_index: Literal[0, 1, 2], 

621 multiple: bool = False, 

622 index: int = 0, 

623) -> str | Expr | None: 

624 annotation = None 

625 with suppress(Exception): 

626 annotation = docstring.parent.annotation # type: ignore[union-attr] 

627 if annotation.is_generator: 

628 annotation = annotation.slice.elements[gen_index] 

629 elif annotation.is_iterator and gen_index == 0: 

630 annotation = annotation.slice 

631 if multiple and annotation.is_tuple: 

632 annotation = annotation.slice.elements[index] 

633 return annotation 

634 

635 

636def _read_returns_section( 

637 docstring: Docstring, 

638 *, 

639 offset: int, 

640 returns_multiple_items: bool = True, 

641 returns_named_value: bool = True, 

642 warn_missing_types: bool = True, 

643 warnings: bool = True, 

644 **options: Any, 

645) -> tuple[DocstringSectionReturns | None, int]: 

646 returns = [] 

647 

648 block, new_offset = _read_block_items_maybe( 

649 docstring, 

650 offset=offset, 

651 multiple=returns_multiple_items, 

652 **options, 

653 ) 

654 

655 for index, (line_number, return_lines) in enumerate(block): 

656 try: 

657 name, annotation, description = _get_name_annotation_description( 

658 docstring, 

659 line_number, 

660 return_lines, 

661 named=returns_named_value, 

662 ) 

663 except ValueError: 

664 continue 

665 

666 if annotation: 

667 # Try to compile the annotation to transform it into an expression. 

668 annotation = parse_docstring_annotation(annotation, docstring) 

669 else: 

670 # Try to retrieve the annotation from the docstring parent. 

671 annotation = _annotation_from_parent(docstring, gen_index=2, multiple=len(block) > 1, index=index) 

672 

673 if warnings and warn_missing_types and annotation is None: 

674 returned_value = repr(name) if name else index + 1 

675 docstring_warning(docstring, line_number, f"No type or annotation for returned value {returned_value}") 

676 

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

678 

679 return DocstringSectionReturns(returns), new_offset 

680 

681 

682def _read_yields_section( 

683 docstring: Docstring, 

684 *, 

685 offset: int, 

686 returns_multiple_items: bool = True, 

687 returns_named_value: bool = True, 

688 warn_missing_types: bool = True, 

689 warnings: bool = True, 

690 **options: Any, 

691) -> tuple[DocstringSectionYields | None, int]: 

692 yields = [] 

693 

694 block, new_offset = _read_block_items_maybe( 

695 docstring, 

696 offset=offset, 

697 multiple=returns_multiple_items, 

698 **options, 

699 ) 

700 

701 for index, (line_number, yield_lines) in enumerate(block): 

702 try: 

703 name, annotation, description = _get_name_annotation_description( 

704 docstring, 

705 line_number, 

706 yield_lines, 

707 named=returns_named_value, 

708 ) 

709 except ValueError: 

710 continue 

711 

712 if annotation: 

713 # Try to compile the annotation to transform it into an expression. 

714 annotation = parse_docstring_annotation(annotation, docstring) 

715 else: 

716 # Try to retrieve the annotation from the docstring parent. 

717 annotation = _annotation_from_parent(docstring, gen_index=0, multiple=len(block) > 1, index=index) 

718 

719 if warnings and warn_missing_types and annotation is None: 

720 yielded_value = repr(name) if name else index + 1 

721 docstring_warning(docstring, line_number, f"No type or annotation for yielded value {yielded_value}") 

722 

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

724 

725 return DocstringSectionYields(yields), new_offset 

726 

727 

728def _read_receives_section( 

729 docstring: Docstring, 

730 *, 

731 offset: int, 

732 receives_multiple_items: bool = True, 

733 receives_named_value: bool = True, 

734 warn_missing_types: bool = True, 

735 warnings: bool = True, 

736 **options: Any, 

737) -> tuple[DocstringSectionReceives | None, int]: 

738 receives = [] 

739 

740 block, new_offset = _read_block_items_maybe( 

741 docstring, 

742 offset=offset, 

743 multiple=receives_multiple_items, 

744 **options, 

745 ) 

746 

747 for index, (line_number, receive_lines) in enumerate(block): 

748 try: 

749 name, annotation, description = _get_name_annotation_description( 

750 docstring, 

751 line_number, 

752 receive_lines, 

753 named=receives_named_value, 

754 ) 

755 except ValueError: 

756 continue 

757 

758 if annotation: 

759 # Try to compile the annotation to transform it into an expression. 

760 annotation = parse_docstring_annotation(annotation, docstring) 

761 else: 

762 # Try to retrieve the annotation from the docstring parent. 

763 annotation = _annotation_from_parent(docstring, gen_index=1, multiple=len(block) > 1, index=index) 

764 

765 if warnings and warn_missing_types and annotation is None: 

766 received_value = repr(name) if name else index + 1 

767 docstring_warning(docstring, line_number, f"No type or annotation for received value {received_value}") 

768 

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

770 

771 return DocstringSectionReceives(receives), new_offset 

772 

773 

774def _read_examples_section( 

775 docstring: Docstring, 

776 *, 

777 offset: int, 

778 trim_doctest_flags: bool = True, 

779 **options: Any, 

780) -> tuple[DocstringSectionExamples | None, int]: 

781 text, new_offset = _read_block(docstring, offset=offset, **options) 

782 

783 sub_sections: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = [] 

784 in_code_example = False 

785 in_code_block = False 

786 current_text: list[str] = [] 

787 current_example: list[str] = [] 

788 

789 for line in text.split("\n"): 

790 if _is_empty_line(line): 

791 if in_code_example: 

792 if current_example: 792 ↛ 795line 792 didn't jump to line 795 because the condition on line 792 was always true

793 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example))) 

794 current_example = [] 

795 in_code_example = False 

796 else: 

797 current_text.append(line) 

798 

799 elif in_code_example: 

800 if trim_doctest_flags: 

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

802 line = _RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901 

803 current_example.append(line) 

804 

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

806 in_code_block = not in_code_block 

807 current_text.append(line) 

808 

809 elif in_code_block: 

810 current_text.append(line) 

811 

812 elif line.startswith(">>>"): 

813 if current_text: 

814 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n"))) 

815 current_text = [] 

816 in_code_example = True 

817 

818 if trim_doctest_flags: 

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

820 current_example.append(line) 

821 

822 else: 

823 current_text.append(line) 

824 

825 if current_text: 825 ↛ 826line 825 didn't jump to line 826 because the condition on line 825 was never true

826 sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n"))) 

827 elif current_example: 827 ↛ 830line 827 didn't jump to line 830 because the condition on line 827 was always true

828 sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example))) 

829 

830 return DocstringSectionExamples(sub_sections), new_offset 

831 

832 

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

834 return not line.strip() 

835 

836 

837_section_reader = { 

838 DocstringSectionKind.parameters: _read_parameters_section, 

839 DocstringSectionKind.other_parameters: _read_other_parameters_section, 

840 DocstringSectionKind.type_parameters: _read_type_parameters_section, 

841 DocstringSectionKind.raises: _read_raises_section, 

842 DocstringSectionKind.warns: _read_warns_section, 

843 DocstringSectionKind.examples: _read_examples_section, 

844 DocstringSectionKind.attributes: _read_attributes_section, 

845 DocstringSectionKind.functions: _read_functions_section, 

846 DocstringSectionKind.classes: _read_classes_section, 

847 DocstringSectionKind.type_aliases: _read_type_aliases_section, 

848 DocstringSectionKind.modules: _read_modules_section, 

849 DocstringSectionKind.returns: _read_returns_section, 

850 DocstringSectionKind.yields: _read_yields_section, 

851 DocstringSectionKind.receives: _read_receives_section, 

852} 

853 

854_sentinel = object() 

855 

856 

857def parse_google( 

858 docstring: Docstring, 

859 *, 

860 ignore_init_summary: bool = False, 

861 trim_doctest_flags: bool = True, 

862 returns_multiple_items: bool = True, 

863 returns_named_value: bool = True, 

864 returns_type_in_property_summary: bool = False, 

865 receives_multiple_items: bool = True, 

866 receives_named_value: bool = True, 

867 warn_unknown_params: bool = True, 

868 warn_missing_types: bool = True, 

869 warnings: bool = True, 

870 **options: Any, 

871) -> list[DocstringSection]: 

872 """Parse a Google-style docstring. 

873 

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

875 It then returns this list of sections. 

876 

877 Parameters: 

878 docstring: The docstring to parse. 

879 ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings. 

880 trim_doctest_flags: Whether to remove doctest flags from Python example blocks. 

881 returns_multiple_items: Whether to parse multiple items in `Yields` and `Returns` sections. 

882 When true, each item's continuation lines must be indented. 

883 When false (single item), no further indentation is required. 

884 returns_named_value: Whether to parse `Yields` and `Returns` section items as name and description, rather than type and description. 

885 When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`. 

886 When false, parentheses are optional but the items cannot be named: `int: Description`. 

887 receives_multiple_items: Whether to parse multiple items in `Receives` sections. 

888 When true, each item's continuation lines must be indented. 

889 When false (single item), no further indentation is required. 

890 receives_named_value: Whether to parse `Receives` section items as name and description, rather than type and description. 

891 When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`. 

892 When false, parentheses are optional but the items cannot be named: `int: Description`. 

893 returns_type_in_property_summary: Whether to parse the return type of properties 

894 at the beginning of their summary: `str: Summary of the property`. 

895 warn_unknown_params: Warn about documented parameters not appearing in the signature. 

896 warn_missing_types: Warn about missing types/annotations for parameters, return values, etc. 

897 warnings: Whether to log warnings at all. 

898 **options: Additional parsing options. 

899 

900 Returns: 

901 A list of docstring sections. 

902 """ 

903 sections: list[DocstringSection] = [] 

904 current_section = [] 

905 

906 in_code_block = False 

907 lines = docstring.lines 

908 

909 options = { 

910 "ignore_init_summary": ignore_init_summary, 

911 "trim_doctest_flags": trim_doctest_flags, 

912 "returns_multiple_items": returns_multiple_items, 

913 "returns_named_value": returns_named_value, 

914 "returns_type_in_property_summary": returns_type_in_property_summary, 

915 "receives_multiple_items": receives_multiple_items, 

916 "receives_named_value": receives_named_value, 

917 "warn_unknown_params": warn_unknown_params, 

918 "warn_missing_types": warn_missing_types, 

919 "warnings": warnings, 

920 **options, 

921 } 

922 

923 ignore_summary = ( 

924 options["ignore_init_summary"] 

925 and docstring.parent is not None 

926 and docstring.parent.name == "__init__" 

927 and docstring.parent.is_function 

928 and docstring.parent.parent is not None 

929 and docstring.parent.parent.is_class 

930 ) 

931 

932 offset = 2 if ignore_summary else 0 

933 

934 while offset < len(lines): 

935 line_lower = lines[offset].lower() 

936 

937 if in_code_block: 

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

939 in_code_block = False 

940 current_section.append(lines[offset]) 

941 

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

943 in_code_block = True 

944 current_section.append(lines[offset]) 

945 

946 elif match := _RE_ADMONITION.match(lines[offset]): 

947 groups = match.groupdict() 

948 title = groups["title"] 

949 admonition_type = groups["type"] 

950 is_section = admonition_type.lower() in _section_kind 

951 

952 has_previous_line = offset > 0 

953 blank_line_above = not has_previous_line or _is_empty_line(lines[offset - 1]) 

954 has_next_line = offset < len(lines) - 1 

955 has_next_lines = offset < len(lines) - 2 

956 blank_line_below = has_next_line and _is_empty_line(lines[offset + 1]) 

957 blank_lines_below = has_next_lines and _is_empty_line(lines[offset + 2]) 

958 indented_line_below = has_next_line and not blank_line_below and lines[offset + 1].startswith(" ") 

959 indented_lines_below = has_next_lines and not blank_lines_below and lines[offset + 2].startswith(" ") 

960 if not (indented_line_below or indented_lines_below): 

961 # Do not warn when there are no contents, 

962 # this is most probably not a section or admonition. 

963 current_section.append(lines[offset]) 

964 offset += 1 

965 continue 

966 reasons = [] 

967 kind = "section" if is_section else "admonition" 

968 if (indented_line_below or indented_lines_below) and not blank_line_above: 

969 reasons.append(f"Missing blank line above {kind}") 

970 if indented_lines_below and blank_line_below: 

971 reasons.append(f"Extraneous blank line below {kind} title") 

972 if reasons: 

973 if warnings: 973 ↛ 981line 973 didn't jump to line 981 because the condition on line 973 was always true

974 reasons_string = "; ".join(reasons) 

975 docstring_warning( 

976 docstring, 

977 offset, 

978 f"Possible {kind} skipped, reasons: {reasons_string}", 

979 LogLevel.debug, 

980 ) 

981 current_section.append(lines[offset]) 

982 offset += 1 

983 continue 

984 

985 if is_section: 

986 if current_section: 

987 if any(current_section): 987 ↛ 989line 987 didn't jump to line 989 because the condition on line 987 was always true

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

989 current_section = [] 

990 reader = _section_reader[_section_kind[admonition_type.lower()]] 

991 section, offset = reader(docstring, offset=offset + 1, **options) # type: ignore[operator] 

992 if section: 

993 section.title = title 

994 sections.append(section) 

995 

996 else: 

997 contents, offset = _read_block(docstring, offset=offset + 1) 

998 if contents: 998 ↛ 1008line 998 didn't jump to line 1008 because the condition on line 998 was always true

999 if current_section: 

1000 if any(current_section): 1000 ↛ 1002line 1000 didn't jump to line 1002 because the condition on line 1000 was always true

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

1002 current_section = [] 

1003 if title is None: 

1004 title = admonition_type 

1005 admonition_type = admonition_type.lower().replace(" ", "-") 

1006 sections.append(DocstringSectionAdmonition(kind=admonition_type, text=contents, title=title)) 

1007 else: 

1008 with suppress(IndexError): 

1009 current_section.append(lines[offset]) 

1010 else: 

1011 current_section.append(lines[offset]) 

1012 

1013 offset += 1 

1014 

1015 if current_section: 

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

1017 

1018 if ( 

1019 returns_type_in_property_summary 

1020 and sections 

1021 and docstring.parent 

1022 and docstring.parent.is_attribute 

1023 and "property" in docstring.parent.labels 

1024 ): 

1025 lines = sections[0].value.lstrip().split("\n") 

1026 if ":" in lines[0]: 1026 ↛ 1036line 1026 didn't jump to line 1036 because the condition on line 1026 was always true

1027 annotation, line = lines[0].split(":", 1) 

1028 lines = [line, *lines[1:]] 

1029 sections[0].value = "\n".join(lines) 

1030 sections.append( 

1031 DocstringSectionReturns( 

1032 [DocstringReturn("", description="", annotation=parse_docstring_annotation(annotation, docstring))], 

1033 ), 

1034 ) 

1035 

1036 return sections