Coverage for src/_griffe/docstrings/google.py: 86.16%

436 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-15 16:47 +0200

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, List, Tuple 

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 

39from _griffe.expressions import ExprName 

40 

41if TYPE_CHECKING: 

42 from typing import Any, Literal, Pattern 

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

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

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

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

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_returns_section( 

446 docstring: Docstring, 

447 *, 

448 offset: int, 

449 returns_multiple_items: bool = True, 

450 returns_named_value: bool = True, 

451 **options: Any, 

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

453 returns = [] 

454 

455 if returns_multiple_items: 

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

457 else: 

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

459 block = [(new_offset, one_block.splitlines())] 

460 

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

462 if returns_named_value: 

463 match = _RE_NAME_ANNOTATION_DESCRIPTION.match(return_lines[0]) 

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

465 docstring_warning( 

466 docstring, 

467 line_number, 

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

469 ) 

470 continue 

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

472 else: 

473 name = None 

474 if ":" in return_lines[0]: 

475 annotation, description = return_lines[0].split(":", 1) 

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

477 else: 

478 annotation = None 

479 description = return_lines[0] 

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

481 

482 if annotation: 

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

484 annotation = parse_docstring_annotation(annotation, docstring) 

485 else: 

486 # try to retrieve the annotation from the docstring parent 

487 with suppress(AttributeError, KeyError, ValueError): 

488 if docstring.parent.is_function: # type: ignore[union-attr] 488 ↛ 490line 488 didn't jump to line 490 because the condition on line 488 was always true

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

490 elif docstring.parent.is_attribute: # type: ignore[union-attr] 

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

492 else: 

493 raise ValueError 

494 if len(block) > 1: 

495 if annotation.is_tuple: 

496 annotation = annotation.slice.elements[index] 

497 else: 

498 if annotation.is_iterator: 498 ↛ 499line 498 didn't jump to line 499 because the condition on line 498 was never true

499 return_item = annotation.slice 

500 elif annotation.is_generator: 

501 return_item = annotation.slice.elements[2] 

502 else: 

503 raise ValueError 

504 if isinstance(return_item, ExprName): 504 ↛ 505line 504 didn't jump to line 505 because the condition on line 504 was never true

505 annotation = return_item 

506 elif return_item.is_tuple: 506 ↛ 509line 506 didn't jump to line 509 because the condition on line 506 was always true

507 annotation = return_item.slice.elements[index] 

508 else: 

509 annotation = return_item 

510 

511 if annotation is None: 

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

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

514 

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

516 

517 return DocstringSectionReturns(returns), new_offset 

518 

519 

520def _read_yields_section( 

521 docstring: Docstring, 

522 *, 

523 offset: int, 

524 **options: Any, 

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

526 yields = [] 

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

528 

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

530 match = _RE_NAME_ANNOTATION_DESCRIPTION.match(yield_lines[0]) 

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

532 docstring_warning( 

533 docstring, 

534 line_number, 

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

536 ) 

537 continue 

538 

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

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

541 

542 if annotation: 

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

544 annotation = parse_docstring_annotation(annotation, docstring) 

545 else: 

546 # try to retrieve the annotation from the docstring parent 

547 with suppress(AttributeError, IndexError, KeyError, ValueError): 

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

549 if annotation.is_iterator: 

550 yield_item = annotation.slice 

551 elif annotation.is_generator: 551 ↛ 554line 551 didn't jump to line 554 because the condition on line 551 was always true

552 yield_item = annotation.slice.elements[0] 

553 else: 

554 raise ValueError 

555 if isinstance(yield_item, ExprName): 

556 annotation = yield_item 

557 elif yield_item.is_tuple: 557 ↛ 560line 557 didn't jump to line 560 because the condition on line 557 was always true

558 annotation = yield_item.slice.elements[index] 

559 else: 

560 annotation = yield_item 

561 

562 if annotation is None: 

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

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

565 

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

567 

568 return DocstringSectionYields(yields), new_offset 

569 

570 

571def _read_receives_section( 

572 docstring: Docstring, 

573 *, 

574 offset: int, 

575 **options: Any, 

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

577 receives = [] 

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

579 

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

581 match = _RE_NAME_ANNOTATION_DESCRIPTION.match(receive_lines[0]) 

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

583 docstring_warning( 

584 docstring, 

585 line_number, 

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

587 ) 

588 continue 

589 

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

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

592 

593 if annotation: 593 ↛ 595line 593 didn't jump to line 595 because the condition on line 593 was never true

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

595 annotation = parse_docstring_annotation(annotation, docstring) 

596 else: 

597 # try to retrieve the annotation from the docstring parent 

598 with suppress(AttributeError, KeyError): 

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

600 if annotation.is_generator: 600 ↛ 598line 600 didn't jump to line 598

601 receives_item = annotation.slice.elements[1] 

602 if isinstance(receives_item, ExprName): 

603 annotation = receives_item 

604 elif receives_item.is_tuple: 604 ↛ 607line 604 didn't jump to line 607 because the condition on line 604 was always true

605 annotation = receives_item.slice.elements[index] 

606 else: 

607 annotation = receives_item 

608 

609 if annotation is None: 

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

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

612 

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

614 

615 return DocstringSectionReceives(receives), new_offset 

616 

617 

618def _read_examples_section( 

619 docstring: Docstring, 

620 *, 

621 offset: int, 

622 trim_doctest_flags: bool = True, 

623 **options: Any, 

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

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

626 

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

628 in_code_example = False 

629 in_code_block = False 

630 current_text: list[str] = [] 

631 current_example: list[str] = [] 

632 

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

634 if _is_empty_line(line): 

635 if in_code_example: 

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

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

638 current_example = [] 

639 in_code_example = False 

640 else: 

641 current_text.append(line) 

642 

643 elif in_code_example: 

644 if trim_doctest_flags: 

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

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

647 current_example.append(line) 

648 

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

650 in_code_block = not in_code_block 

651 current_text.append(line) 

652 

653 elif in_code_block: 

654 current_text.append(line) 

655 

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

657 if current_text: 

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

659 current_text = [] 

660 in_code_example = True 

661 

662 if trim_doctest_flags: 

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

664 current_example.append(line) 

665 

666 else: 

667 current_text.append(line) 

668 

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

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

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

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

673 

674 return DocstringSectionExamples(sub_sections), new_offset 

675 

676 

677def _read_deprecated_section( 

678 docstring: Docstring, 

679 *, 

680 offset: int, 

681 **options: Any, 

682) -> tuple[DocstringSectionDeprecated | None, int]: 

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

684 

685 # check the presence of a name and description, separated by a semi-colon 

686 try: 

687 version, text = text.split(":", 1) 

688 except ValueError: 

689 docstring_warning(docstring, new_offset, f"Could not parse version, text at line {offset}") 

690 return None, new_offset 

691 

692 version = version.lstrip() 

693 description = text.lstrip() 

694 

695 return ( 

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

697 new_offset, 

698 ) 

699 

700 

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

702 return not line.strip() 

703 

704 

705_section_reader = { 

706 DocstringSectionKind.parameters: _read_parameters_section, 

707 DocstringSectionKind.other_parameters: _read_other_parameters_section, 

708 DocstringSectionKind.raises: _read_raises_section, 

709 DocstringSectionKind.warns: _read_warns_section, 

710 DocstringSectionKind.examples: _read_examples_section, 

711 DocstringSectionKind.attributes: _read_attributes_section, 

712 DocstringSectionKind.functions: _read_functions_section, 

713 DocstringSectionKind.classes: _read_classes_section, 

714 DocstringSectionKind.modules: _read_modules_section, 

715 DocstringSectionKind.returns: _read_returns_section, 

716 DocstringSectionKind.yields: _read_yields_section, 

717 DocstringSectionKind.receives: _read_receives_section, 

718 DocstringSectionKind.deprecated: _read_deprecated_section, 

719} 

720 

721_sentinel = object() 

722 

723 

724def parse_google( 

725 docstring: Docstring, 

726 *, 

727 ignore_init_summary: bool = False, 

728 trim_doctest_flags: bool = True, 

729 returns_multiple_items: bool = True, 

730 warn_unknown_params: bool = True, 

731 returns_named_value: bool = True, 

732 returns_type_in_property_summary: bool = False, 

733 **options: Any, 

734) -> list[DocstringSection]: 

735 """Parse a Google-style docstring. 

736 

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

738 It then returns this list of sections. 

739 

740 Parameters: 

741 docstring: The docstring to parse. 

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

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

744 returns_multiple_items: Whether the `Returns` section has multiple items. 

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

746 returns_named_value: Whether to parse `thing: Description` in returns sections as a name and description, 

747 rather than a type and description. When true, type must be wrapped in parentheses: `(int): Description.`. 

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

749 returns_type_in_property_summary: Whether to parse the return type of properties 

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

751 **options: Additional parsing options. 

752 

753 Returns: 

754 A list of docstring sections. 

755 """ 

756 sections: list[DocstringSection] = [] 

757 current_section = [] 

758 

759 in_code_block = False 

760 lines = docstring.lines 

761 

762 options = { 

763 "ignore_init_summary": ignore_init_summary, 

764 "trim_doctest_flags": trim_doctest_flags, 

765 "returns_multiple_items": returns_multiple_items, 

766 "warn_unknown_params": warn_unknown_params, 

767 "returns_named_value": returns_named_value, 

768 "returns_type_in_property_summary": returns_type_in_property_summary, 

769 **options, 

770 } 

771 

772 ignore_summary = ( 

773 options["ignore_init_summary"] 

774 and docstring.parent is not None 

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

776 and docstring.parent.is_function 

777 and docstring.parent.parent is not None 

778 and docstring.parent.parent.is_class 

779 ) 

780 

781 offset = 2 if ignore_summary else 0 

782 

783 while offset < len(lines): 

784 line_lower = lines[offset].lower() 

785 

786 if in_code_block: 

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

788 in_code_block = False 

789 current_section.append(lines[offset]) 

790 

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

792 in_code_block = True 

793 current_section.append(lines[offset]) 

794 

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

796 groups = match.groupdict() 

797 title = groups["title"] 

798 admonition_type = groups["type"] 

799 is_section = admonition_type.lower() in _section_kind 

800 

801 has_previous_line = offset > 0 

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

803 has_next_line = offset < len(lines) - 1 

804 has_next_lines = offset < len(lines) - 2 

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

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

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

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

809 if not (indented_line_below or indented_lines_below): 

810 # Do not warn when there are no contents, 

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

812 current_section.append(lines[offset]) 

813 offset += 1 

814 continue 

815 reasons = [] 

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

817 if (indented_line_below or indented_lines_below) and not blank_line_above: 

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

819 if indented_lines_below and blank_line_below: 

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

821 if reasons: 

822 reasons_string = "; ".join(reasons) 

823 docstring_warning( 

824 docstring, 

825 offset, 

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

827 LogLevel.debug, 

828 ) 

829 current_section.append(lines[offset]) 

830 offset += 1 

831 continue 

832 

833 if is_section: 

834 if current_section: 

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

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

837 current_section = [] 

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

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

840 if section: 

841 section.title = title 

842 sections.append(section) 

843 

844 else: 

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

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

847 if current_section: 

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

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

850 current_section = [] 

851 if title is None: 

852 title = admonition_type 

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

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

855 else: 

856 with suppress(IndexError): 

857 current_section.append(lines[offset]) 

858 else: 

859 current_section.append(lines[offset]) 

860 

861 offset += 1 

862 

863 if current_section: 

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

865 

866 if ( 

867 returns_type_in_property_summary 

868 and sections 

869 and docstring.parent 

870 and docstring.parent.is_attribute 

871 and "property" in docstring.parent.labels 

872 ): 

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

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

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

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

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

878 sections.append( 

879 DocstringSectionReturns( 

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

881 ), 

882 ) 

883 

884 return sections