Coverage for packages / griffelib / src / griffe / _internal / docstrings / google.py: 86.78%

481 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-11 11:48 +0100

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, TypedDict 

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 # TODO: Use `get_name_annotation_description` here too? 

202 for line_number, param_lines in block: 

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

204 try: 

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

206 except ValueError: 

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

208 docstring_warning( 

209 docstring, 

210 line_number, 

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

212 ) 

213 continue 

214 

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

216 

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

218 if "(" in name_with_type and name_with_type.endswith(")"): 

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

220 name = name.strip() 

221 annotation = annotation.removesuffix(")").removesuffix(", optional").strip() 

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

223 annotation = parse_docstring_annotation(annotation, docstring) 

224 else: 

225 name = name_with_type 

226 # Try to use the annotation from the signature. 

227 try: 

228 annotation = docstring.parent.parameters[name].annotation # ty:ignore[unresolved-attribute] 

229 except (AttributeError, KeyError): 

230 annotation = None 

231 

232 try: 

233 default = docstring.parent.parameters[name].default # ty:ignore[unresolved-attribute] 

234 except (AttributeError, KeyError): 

235 default = None 

236 

237 if warnings and warn_missing_types and annotation is None: 

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

239 

240 if warnings and warn_unknown_params: 

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

242 params = docstring.parent.parameters # ty:ignore[unresolved-attribute] 

243 if name not in params: 

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

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

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

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

248 break 

249 docstring_warning(docstring, line_number, message) 

250 

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

252 

253 return parameters, new_offset 

254 

255 

256def _read_parameters_section( 

257 docstring: Docstring, 

258 *, 

259 offset: int, 

260 **options: Any, 

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

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

263 return DocstringSectionParameters(parameters), new_offset 

264 

265 

266def _read_other_parameters_section( 

267 docstring: Docstring, 

268 *, 

269 offset: int, 

270 warn_unknown_params: bool = True, # noqa: ARG001 

271 **options: Any, 

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

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

274 return DocstringSectionOtherParameters(parameters), new_offset 

275 

276 

277def _read_type_parameters_section( 

278 docstring: Docstring, 

279 *, 

280 offset: int, 

281 warn_unknown_params: bool = True, 

282 **options: Any, 

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

284 type_parameters = [] 

285 bound: str | Expr | None 

286 

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

288 

289 for line_number, type_param_lines in block: 

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

291 try: 

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

293 except ValueError: 

294 docstring_warning( 

295 docstring, 

296 line_number, 

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

298 ) 

299 continue 

300 

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

302 

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

304 if " " in name_with_bound: 

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

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

307 bound = bound[1:-1] 

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

309 bound = parse_docstring_annotation(bound, docstring) 

310 else: 

311 name = name_with_bound 

312 # try to use the annotation from the signature 

313 try: 

314 bound = docstring.parent.type_parameters[name].annotation # ty:ignore[possibly-missing-attribute] 

315 except (AttributeError, KeyError): 

316 bound = None 

317 

318 try: 

319 default = docstring.parent.type_parameters[name].default # ty:ignore[possibly-missing-attribute] 

320 except (AttributeError, KeyError): 

321 default = None 

322 

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

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

325 type_params = docstring.parent.type_parameters # ty:ignore[possibly-missing-attribute] 

326 if name not in type_params: 

327 message = f"Type parameter '{name}' does not appear in the {docstring.parent.kind.value} signature" # ty:ignore[possibly-missing-attribute] 

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

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

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

331 break 

332 docstring_warning(docstring, line_number, message) 

333 

334 type_parameters.append( 

335 DocstringTypeParameter( 

336 name=name, 

337 value=default, 

338 annotation=bound, 

339 description=description, 

340 ), 

341 ) 

342 

343 return DocstringSectionTypeParameters(type_parameters), new_offset 

344 

345 

346def _read_attributes_section( 

347 docstring: Docstring, 

348 *, 

349 offset: int, 

350 warnings: bool = True, 

351 **options: Any, 

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

353 attributes = [] 

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

355 

356 annotation: str | Expr | None = None 

357 for line_number, attr_lines in block: 

358 try: 

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

360 except ValueError: 

361 if warnings: 

362 docstring_warning( 

363 docstring, 

364 line_number, 

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

366 ) 

367 continue 

368 

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

370 

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

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

373 annotation = annotation.strip("()") 

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

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

376 annotation = parse_docstring_annotation(annotation, docstring) 

377 else: 

378 name = name_with_type 

379 with suppress(AttributeError, KeyError, TypeError): 

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

381 annotation = docstring.parent[name].annotation # ty:ignore[not-subscriptable] 

382 

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

384 

385 return DocstringSectionAttributes(attributes), new_offset 

386 

387 

388def _read_functions_section( 

389 docstring: Docstring, 

390 *, 

391 offset: int, 

392 warnings: bool = True, 

393 **options: Any, 

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

395 functions = [] 

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

397 

398 signature: str | Expr | None = None 

399 for line_number, func_lines in block: 

400 try: 

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

402 except ValueError: 

403 if warnings: 

404 docstring_warning( 

405 docstring, 

406 line_number, 

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

408 ) 

409 continue 

410 

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

412 

413 if "(" in name_with_signature: 

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

415 signature = name_with_signature 

416 else: 

417 name = name_with_signature 

418 signature = None 

419 

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

421 

422 return DocstringSectionFunctions(functions), new_offset 

423 

424 

425def _read_classes_section( 

426 docstring: Docstring, 

427 *, 

428 offset: int, 

429 warnings: bool = True, 

430 **options: Any, 

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

432 classes = [] 

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

434 

435 signature: str | Expr | None = None 

436 for line_number, class_lines in block: 

437 try: 

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

439 except ValueError: 

440 if warnings: 

441 docstring_warning( 

442 docstring, 

443 line_number, 

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

445 ) 

446 continue 

447 

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

449 

450 if "(" in name_with_signature: 

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

452 signature = name_with_signature 

453 else: 

454 name = name_with_signature 

455 signature = None 

456 

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

458 

459 return DocstringSectionClasses(classes), new_offset 

460 

461 

462def _read_type_aliases_section( 

463 docstring: Docstring, 

464 *, 

465 offset: int, 

466 **options: Any, 

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

468 type_aliases = [] 

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

470 

471 for line_number, type_alias_lines in block: 

472 try: 

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

474 except ValueError: 

475 docstring_warning( 

476 docstring, 

477 line_number, 

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

479 ) 

480 continue 

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

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

483 

484 return DocstringSectionTypeAliases(type_aliases), new_offset 

485 

486 

487def _read_modules_section( 

488 docstring: Docstring, 

489 *, 

490 offset: int, 

491 warnings: bool = True, 

492 **options: Any, 

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

494 modules = [] 

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

496 

497 for line_number, module_lines in block: 

498 try: 

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

500 except ValueError: 

501 if warnings: 

502 docstring_warning( 

503 docstring, 

504 line_number, 

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

506 ) 

507 continue 

508 

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

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

511 

512 return DocstringSectionModules(modules), new_offset 

513 

514 

515def _read_raises_section( 

516 docstring: Docstring, 

517 *, 

518 offset: int, 

519 warnings: bool = True, 

520 **options: Any, 

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

522 exceptions = [] 

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

524 

525 annotation: str | Expr 

526 for line_number, exception_lines in block: 

527 try: 

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

529 except ValueError: 

530 if warnings: 

531 docstring_warning( 

532 docstring, 

533 line_number, 

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

535 ) 

536 continue 

537 

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

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

540 annotation = parse_docstring_annotation(annotation, docstring) 

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

542 

543 return DocstringSectionRaises(exceptions), new_offset 

544 

545 

546def _read_warns_section( 

547 docstring: Docstring, 

548 *, 

549 offset: int, 

550 warnings: bool = True, 

551 **options: Any, 

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

553 warns = [] 

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

555 

556 for line_number, warning_lines in block: 

557 try: 

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

559 except ValueError: 

560 if warnings: 

561 docstring_warning( 

562 docstring, 

563 line_number, 

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

565 ) 

566 continue 

567 

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

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

570 

571 return DocstringSectionWarns(warns), new_offset 

572 

573 

574def _read_block_items_maybe( 

575 docstring: Docstring, 

576 *, 

577 offset: int, 

578 multiple: bool = True, 

579 **options: Any, 

580) -> _ItemsBlock: 

581 if multiple: 

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

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

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

585 

586 

587def _get_name_annotation_description( 

588 docstring: Docstring, 

589 line_number: int, 

590 lines: list[str], 

591 *, 

592 named: bool = True, 

593 warnings: bool = True, 

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

595 if named: 

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

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

598 if warnings: 

599 docstring_warning( 

600 docstring, 

601 line_number, 

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

603 ) 

604 raise ValueError 

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

606 else: 

607 name = None 

608 if ":" in lines[0]: 

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

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

611 else: 

612 annotation = None 

613 description = lines[0] 

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

615 return name, annotation, description 

616 

617 

618def _annotation_from_parent( 

619 docstring: Docstring, 

620 *, 

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

622 multiple: bool = False, 

623 index: int = 0, 

624) -> str | Expr | None: 

625 annotation = None 

626 with suppress(Exception): 

627 annotation = docstring.parent.annotation # ty:ignore[unresolved-attribute] 

628 if annotation.is_generator: 

629 annotation = annotation.slice.elements[gen_index] 

630 elif annotation.is_iterator and gen_index == 0: 

631 annotation = annotation.slice 

632 if multiple and annotation.is_tuple: 

633 annotation = annotation.slice.elements[index] 

634 return annotation 

635 

636 

637def _read_returns_section( 

638 docstring: Docstring, 

639 *, 

640 offset: int, 

641 returns_multiple_items: bool = True, 

642 returns_named_value: bool = True, 

643 warn_missing_types: bool = True, 

644 warnings: bool = True, 

645 **options: Any, 

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

647 returns = [] 

648 

649 block, new_offset = _read_block_items_maybe( 

650 docstring, 

651 offset=offset, 

652 multiple=returns_multiple_items, 

653 **options, 

654 ) 

655 

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

657 try: 

658 name, annotation, description = _get_name_annotation_description( 

659 docstring, 

660 line_number, 

661 return_lines, 

662 named=returns_named_value, 

663 ) 

664 except ValueError: 

665 continue 

666 

667 if annotation: 

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

669 annotation = parse_docstring_annotation(annotation, docstring) 

670 else: 

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

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

673 

674 if warnings and warn_missing_types and annotation is None: 

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

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

677 

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

679 

680 return DocstringSectionReturns(returns), new_offset 

681 

682 

683def _read_yields_section( 

684 docstring: Docstring, 

685 *, 

686 offset: int, 

687 returns_multiple_items: bool = True, 

688 returns_named_value: bool = True, 

689 warn_missing_types: bool = True, 

690 warnings: bool = True, 

691 **options: Any, 

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

693 yields = [] 

694 

695 block, new_offset = _read_block_items_maybe( 

696 docstring, 

697 offset=offset, 

698 multiple=returns_multiple_items, 

699 **options, 

700 ) 

701 

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

703 try: 

704 name, annotation, description = _get_name_annotation_description( 

705 docstring, 

706 line_number, 

707 yield_lines, 

708 named=returns_named_value, 

709 ) 

710 except ValueError: 

711 continue 

712 

713 if annotation: 

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

715 annotation = parse_docstring_annotation(annotation, docstring) 

716 else: 

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

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

719 

720 if warnings and warn_missing_types and annotation is None: 

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

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

723 

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

725 

726 return DocstringSectionYields(yields), new_offset 

727 

728 

729def _read_receives_section( 

730 docstring: Docstring, 

731 *, 

732 offset: int, 

733 receives_multiple_items: bool = True, 

734 receives_named_value: bool = True, 

735 warn_missing_types: bool = True, 

736 warnings: bool = True, 

737 **options: Any, 

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

739 receives = [] 

740 

741 block, new_offset = _read_block_items_maybe( 

742 docstring, 

743 offset=offset, 

744 multiple=receives_multiple_items, 

745 **options, 

746 ) 

747 

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

749 try: 

750 name, annotation, description = _get_name_annotation_description( 

751 docstring, 

752 line_number, 

753 receive_lines, 

754 named=receives_named_value, 

755 ) 

756 except ValueError: 

757 continue 

758 

759 if annotation: 

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

761 annotation = parse_docstring_annotation(annotation, docstring) 

762 else: 

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

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

765 

766 if warnings and warn_missing_types and annotation is None: 

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

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

769 

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

771 

772 return DocstringSectionReceives(receives), new_offset 

773 

774 

775def _read_examples_section( 

776 docstring: Docstring, 

777 *, 

778 offset: int, 

779 trim_doctest_flags: bool = True, 

780 **options: Any, 

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

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

783 

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

785 in_code_example = False 

786 in_code_block = False 

787 current_text: list[str] = [] 

788 current_example: list[str] = [] 

789 

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

791 if _is_empty_line(line): 

792 if in_code_example: 

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

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

795 current_example = [] 

796 in_code_example = False 

797 else: 

798 current_text.append(line) 

799 

800 elif in_code_example: 

801 if trim_doctest_flags: 

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

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

804 current_example.append(line) 

805 

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

807 in_code_block = not in_code_block 

808 current_text.append(line) 

809 

810 elif in_code_block: 

811 current_text.append(line) 

812 

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

814 if current_text: 

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

816 current_text = [] 

817 in_code_example = True 

818 

819 if trim_doctest_flags: 

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

821 current_example.append(line) 

822 

823 else: 

824 current_text.append(line) 

825 

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

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

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

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

830 

831 return DocstringSectionExamples(sub_sections), new_offset 

832 

833 

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

835 return not line.strip() 

836 

837 

838_section_reader = { 

839 DocstringSectionKind.parameters: _read_parameters_section, 

840 DocstringSectionKind.other_parameters: _read_other_parameters_section, 

841 DocstringSectionKind.type_parameters: _read_type_parameters_section, 

842 DocstringSectionKind.raises: _read_raises_section, 

843 DocstringSectionKind.warns: _read_warns_section, 

844 DocstringSectionKind.examples: _read_examples_section, 

845 DocstringSectionKind.attributes: _read_attributes_section, 

846 DocstringSectionKind.functions: _read_functions_section, 

847 DocstringSectionKind.classes: _read_classes_section, 

848 DocstringSectionKind.type_aliases: _read_type_aliases_section, 

849 DocstringSectionKind.modules: _read_modules_section, 

850 DocstringSectionKind.returns: _read_returns_section, 

851 DocstringSectionKind.yields: _read_yields_section, 

852 DocstringSectionKind.receives: _read_receives_section, 

853} 

854 

855 

856class GoogleOptions(TypedDict, total=False): 

857 """Options for parsing Google-style docstrings.""" 

858 

859 ignore_init_summary: bool 

860 """Whether to ignore the summary in `__init__` methods' docstrings.""" 

861 trim_doctest_flags: bool 

862 """Whether to remove doctest flags from Python example blocks.""" 

863 returns_multiple_items: bool 

864 """Whether to parse multiple items in `Yields` and `Returns` sections.""" 

865 returns_named_value: bool 

866 """Whether to parse `Yields` and `Returns` section items as name and description, rather than type and description.""" 

867 returns_type_in_property_summary: bool 

868 """Whether to parse the return type of properties at the beginning of their summary.""" 

869 receives_multiple_items: bool 

870 """Whether to parse multiple items in `Receives` sections.""" 

871 receives_named_value: bool 

872 """Whether to parse `Receives` section items as name and description, rather than type and description.""" 

873 warn_unknown_params: bool 

874 """Whether to warn about unknown parameters.""" 

875 warn_missing_types: bool 

876 """Whether to warn about missing types/annotations for parameters, return values, etc.""" 

877 warnings: bool 

878 """Whether to issue warnings for parsing issues.""" 

879 

880 

881def parse_google( 

882 docstring: Docstring, 

883 *, 

884 ignore_init_summary: bool = False, 

885 trim_doctest_flags: bool = True, 

886 returns_multiple_items: bool = True, 

887 returns_named_value: bool = True, 

888 returns_type_in_property_summary: bool = False, 

889 receives_multiple_items: bool = True, 

890 receives_named_value: bool = True, 

891 warn_unknown_params: bool = True, 

892 warn_missing_types: bool = True, 

893 warnings: bool = True, 

894) -> list[DocstringSection]: 

895 """Parse a Google-style docstring. 

896 

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

898 It then returns this list of sections. 

899 

900 Parameters: 

901 docstring: The docstring to parse. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

916 returns_type_in_property_summary: Whether to parse the return type of properties 

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

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

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

920 warnings: Whether to log warnings at all. 

921 

922 Returns: 

923 A list of docstring sections. 

924 """ 

925 sections: list[DocstringSection] = [] 

926 current_section = [] 

927 

928 in_code_block = False 

929 lines = docstring.lines 

930 

931 options = { 

932 "ignore_init_summary": ignore_init_summary, 

933 "trim_doctest_flags": trim_doctest_flags, 

934 "returns_multiple_items": returns_multiple_items, 

935 "returns_named_value": returns_named_value, 

936 "returns_type_in_property_summary": returns_type_in_property_summary, 

937 "receives_multiple_items": receives_multiple_items, 

938 "receives_named_value": receives_named_value, 

939 "warn_unknown_params": warn_unknown_params, 

940 "warn_missing_types": warn_missing_types, 

941 "warnings": warnings, 

942 } 

943 

944 ignore_summary = ( 

945 options["ignore_init_summary"] 

946 and docstring.parent is not None 

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

948 and docstring.parent.is_function 

949 and docstring.parent.parent is not None 

950 and docstring.parent.parent.is_class 

951 ) 

952 

953 offset = 2 if ignore_summary else 0 

954 

955 while offset < len(lines): 

956 line_lower = lines[offset].lower() 

957 

958 if in_code_block: 

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

960 in_code_block = False 

961 current_section.append(lines[offset]) 

962 

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

964 in_code_block = True 

965 current_section.append(lines[offset]) 

966 

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

968 groups = match.groupdict() 

969 title = groups["title"] 

970 admonition_type = groups["type"] 

971 is_section = admonition_type.lower() in _section_kind 

972 

973 has_previous_line = offset > 0 

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

975 has_next_line = offset < len(lines) - 1 

976 has_next_lines = offset < len(lines) - 2 

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

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

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

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

981 if not (indented_line_below or indented_lines_below): 

982 # Do not warn when there are no contents, 

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

984 current_section.append(lines[offset]) 

985 offset += 1 

986 continue 

987 reasons = [] 

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

989 if (indented_line_below or indented_lines_below) and not blank_line_above: 

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

991 if indented_lines_below and blank_line_below: 

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

993 if reasons: 

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

995 reasons_string = "; ".join(reasons) 

996 docstring_warning( 

997 docstring, 

998 offset, 

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

1000 LogLevel.debug, 

1001 ) 

1002 current_section.append(lines[offset]) 

1003 offset += 1 

1004 continue 

1005 

1006 if is_section: 

1007 if current_section: 

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

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

1010 current_section = [] 

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

1012 section, offset = reader(docstring, offset=offset + 1, **options) 

1013 if section: 

1014 section.title = title 

1015 sections.append(section) 

1016 

1017 else: 

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

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

1020 if current_section: 

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

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

1023 current_section = [] 

1024 if title is None: 

1025 title = admonition_type 

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

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

1028 else: 

1029 with suppress(IndexError): 

1030 current_section.append(lines[offset]) 

1031 else: 

1032 current_section.append(lines[offset]) 

1033 

1034 offset += 1 

1035 

1036 if current_section and any(current_section): 

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

1038 

1039 if ( 

1040 returns_type_in_property_summary 

1041 and sections 

1042 and docstring.parent 

1043 and docstring.parent.is_attribute 

1044 and "property" in docstring.parent.labels 

1045 ): 

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

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

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

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

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

1051 sections.append( 

1052 DocstringSectionReturns( 

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

1054 ), 

1055 ) 

1056 

1057 return sections