Coverage for src/_griffe/docstrings/numpy.py: 85.23%

423 statements  

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

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

2# 

3# Based on https://numpydoc.readthedocs.io/en/latest/format.html, 

4# it seems Numpydoc is a superset of RST. 

5# Since fully parsing RST is a non-goal of this project, 

6# some things are stripped from the Numpydoc specification. 

7# 

8# Rejected as non particularly Pythonic or useful as sections: 

9# 

10# - See also: this section feels too subjective (specially crafted as a standard for Numpy itself), 

11# and there are may ways to reference related items in a docstring, depending on the chosen markup. 

12# 

13# Rejected as naturally handled by the user-chosen markup: 

14# 

15# - Warnings: this is just markup. 

16# - Notes: again, just markup. 

17# - References: again, just markup. 

18 

19 

20from __future__ import annotations 

21 

22import re 

23from contextlib import suppress 

24from textwrap import dedent 

25from typing import TYPE_CHECKING 

26 

27from _griffe.docstrings.models import ( 

28 DocstringAttribute, 

29 DocstringClass, 

30 DocstringFunction, 

31 DocstringModule, 

32 DocstringParameter, 

33 DocstringRaise, 

34 DocstringReceive, 

35 DocstringReturn, 

36 DocstringSection, 

37 DocstringSectionAdmonition, 

38 DocstringSectionAttributes, 

39 DocstringSectionClasses, 

40 DocstringSectionDeprecated, 

41 DocstringSectionExamples, 

42 DocstringSectionFunctions, 

43 DocstringSectionModules, 

44 DocstringSectionOtherParameters, 

45 DocstringSectionParameters, 

46 DocstringSectionRaises, 

47 DocstringSectionReceives, 

48 DocstringSectionReturns, 

49 DocstringSectionText, 

50 DocstringSectionWarns, 

51 DocstringSectionYields, 

52 DocstringWarn, 

53 DocstringYield, 

54) 

55from _griffe.docstrings.utils import docstring_warning, parse_docstring_annotation 

56from _griffe.enumerations import DocstringSectionKind, LogLevel 

57from _griffe.expressions import ExprName 

58 

59if TYPE_CHECKING: 

60 from typing import Any, Literal, Pattern 

61 

62 from _griffe.expressions import Expr 

63 from _griffe.models import Docstring 

64 

65 

66_section_kind = { 

67 "deprecated": DocstringSectionKind.deprecated, 

68 "parameters": DocstringSectionKind.parameters, 

69 "other parameters": DocstringSectionKind.other_parameters, 

70 "returns": DocstringSectionKind.returns, 

71 "yields": DocstringSectionKind.yields, 

72 "receives": DocstringSectionKind.receives, 

73 "raises": DocstringSectionKind.raises, 

74 "warns": DocstringSectionKind.warns, 

75 "examples": DocstringSectionKind.examples, 

76 "attributes": DocstringSectionKind.attributes, 

77 "functions": DocstringSectionKind.functions, 

78 "methods": DocstringSectionKind.functions, 

79 "classes": DocstringSectionKind.classes, 

80 "modules": DocstringSectionKind.modules, 

81} 

82 

83 

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

85 return not line.strip() 

86 

87 

88def _is_dash_line(line: str) -> bool: 

89 return not _is_empty_line(line) and _is_empty_line(line.replace("-", "")) 

90 

91 

92def _read_block_items( 

93 docstring: Docstring, 

94 *, 

95 offset: int, 

96 **options: Any, # noqa: ARG001 

97) -> tuple[list[list[str]], int]: 

98 lines = docstring.lines 

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

100 return [], offset 

101 

102 new_offset = offset 

103 items: list[list[str]] = [] 

104 

105 # skip first empty lines 

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

107 new_offset += 1 

108 

109 # start processing first item 

110 current_item = [lines[new_offset]] 

111 new_offset += 1 

112 

113 # loop on next lines 

114 while new_offset < len(lines): 

115 line = lines[new_offset] 

116 

117 if _is_empty_line(line): 

118 # empty line: preserve it in the current item 

119 current_item.append("") 

120 

121 elif line.startswith(4 * " "): 

122 # continuation line 

123 current_item.append(line[4:]) 

124 

125 elif line.startswith(" "): 125 ↛ 127line 125 didn't jump to line 127 because the condition on line 125 was never true

126 # indent between initial and continuation: append but warn 

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

128 current_item.append(line[cont_indent:]) 

129 docstring_warning( 

130 docstring, 

131 new_offset, 

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

133 f"should be 4 spaces, not {cont_indent}", 

134 ) 

135 

136 elif new_offset + 1 < len(lines) and _is_dash_line(lines[new_offset + 1]): 

137 # detect the start of a new section 

138 break 

139 

140 else: 

141 items.append(current_item) 

142 current_item = [line] 

143 

144 new_offset += 1 

145 

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

147 items.append(current_item) 

148 

149 return items, new_offset - 1 

150 

151 

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

153 lines = docstring.lines 

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

155 return "", offset 

156 

157 new_offset = offset 

158 block: list[str] = [] 

159 

160 # skip first empty lines 

161 while _is_empty_line(lines[new_offset]): 

162 new_offset += 1 

163 while new_offset < len(lines): 

164 is_empty = _is_empty_line(lines[new_offset]) 

165 if is_empty and new_offset < len(lines) - 1 and _is_dash_line(lines[new_offset + 1]): 

166 break # Break if a new unnamed section is reached. 

167 

168 if is_empty and new_offset < len(lines) - 2 and _is_dash_line(lines[new_offset + 2]): 

169 break # Break if a new named section is reached. 

170 

171 block.append(lines[new_offset]) 

172 new_offset += 1 

173 

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

175 

176 

177_RE_OB: str = r"\{" # opening bracket 

178_RE_CB: str = r"\}" # closing bracket 

179_RE_NAME: str = r"\*{0,2}[_a-z][_a-z0-9]*" 

180_RE_TYPE: str = r".+" 

181_RE_RETURNS: Pattern = re.compile( 

182 rf""" 

183 (?: 

184 (?P<nt_name>{_RE_NAME})\s*:\s*(?P<nt_type>{_RE_TYPE}) # name and type 

185 | # or 

186 (?P<name>{_RE_NAME})\s*:\s* # just name 

187 | # or 

188 \s*:\s*$ # no name, no type 

189 | # or 

190 (?::\s*)?(?P<type>{_RE_TYPE})\s* # just type 

191 ) 

192 """, 

193 re.IGNORECASE | re.VERBOSE, 

194) 

195_RE_YIELDS: Pattern = _RE_RETURNS 

196_RE_RECEIVES: Pattern = _RE_RETURNS 

197_RE_PARAMETER: Pattern = re.compile( 

198 rf""" 

199 (?P<names>{_RE_NAME}(?:,\s{_RE_NAME})*) 

200 (?: 

201 \s:\s 

202 (?: 

203 (?:{_RE_OB}(?P<choices>.+){_RE_CB})| 

204 (?P<type>{_RE_TYPE}) 

205 )? 

206 )? 

207 """, 

208 re.IGNORECASE | re.VERBOSE, 

209) 

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

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

212 

213 

214def _read_parameters( 

215 docstring: Docstring, 

216 *, 

217 offset: int, 

218 warn_unknown_params: bool = True, 

219 **options: Any, 

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

221 parameters = [] 

222 annotation: str | Expr | None 

223 

224 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

225 

226 for item in items: 

227 match = _RE_PARAMETER.match(item[0]) 

228 if not match: 

229 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'") 

230 continue 

231 

232 names = match.group("names").split(", ") 

233 annotation = match.group("type") or None 

234 choices = match.group("choices") 

235 default = None 

236 if choices: 

237 annotation = choices 

238 default = choices.split(", ", 1)[0] 

239 elif annotation: 

240 match = re.match(r"^(?P<annotation>.+),\s+default(?: |: |=)(?P<default>.+)$", annotation) 

241 if match: 

242 default = match.group("default") 

243 annotation = match.group("annotation") 

244 if annotation and annotation.endswith(", optional"): 

245 annotation = annotation[:-10] 

246 description = "\n".join(item[1:]).rstrip() if len(item) > 1 else "" 

247 

248 if annotation is None: 

249 # try to use the annotation from the signature 

250 for name in names: 

251 with suppress(AttributeError, KeyError): 

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

253 break 

254 else: 

255 docstring_warning(docstring, new_offset, f"No types or annotations for parameters {names}") 

256 else: 

257 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) 

258 

259 if default is None: 

260 for name in names: 

261 with suppress(AttributeError, KeyError): 

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

263 break 

264 

265 if warn_unknown_params: 

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

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

268 for name in names: 

269 if name not in params: 

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

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

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

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

274 break 

275 docstring_warning(docstring, new_offset, message) 

276 

277 for name in names: 

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

279 

280 return parameters, new_offset 

281 

282 

283def _read_parameters_section( 

284 docstring: Docstring, 

285 *, 

286 offset: int, 

287 **options: Any, 

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

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

290 

291 if parameters: 291 ↛ 294line 291 didn't jump to line 294 because the condition on line 291 was always true

292 return DocstringSectionParameters(parameters), new_offset 

293 

294 docstring_warning(docstring, new_offset, f"Empty parameters section at line {offset}") 

295 return None, new_offset 

296 

297 

298def _read_other_parameters_section( 

299 docstring: Docstring, 

300 *, 

301 offset: int, 

302 warn_unknown_params: bool = True, # noqa: ARG001 

303 **options: Any, 

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

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

306 

307 if parameters: 307 ↛ 310line 307 didn't jump to line 310 because the condition on line 307 was always true

308 return DocstringSectionOtherParameters(parameters), new_offset 

309 

310 docstring_warning(docstring, new_offset, f"Empty other parameters section at line {offset}") 

311 return None, new_offset 

312 

313 

314def _read_deprecated_section( 

315 docstring: Docstring, 

316 *, 

317 offset: int, 

318 **options: Any, 

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

320 # deprecated 

321 # SINCE_VERSION 

322 # TEXT? 

323 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

324 

325 if not items: 325 ↛ 326line 325 didn't jump to line 326 because the condition on line 325 was never true

326 docstring_warning(docstring, new_offset, f"Empty deprecated section at line {offset}") 

327 return None, new_offset 

328 

329 if len(items) > 1: 329 ↛ 330line 329 didn't jump to line 330 because the condition on line 329 was never true

330 docstring_warning(docstring, new_offset, f"Too many deprecated items at {offset}") 

331 

332 item = items[0] 

333 version = item[0] 

334 text = dedent("\n".join(item[1:])) 

335 return DocstringSectionDeprecated(version=version, text=text), new_offset 

336 

337 

338def _read_returns_section( 

339 docstring: Docstring, 

340 *, 

341 offset: int, 

342 **options: Any, 

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

344 # (NAME : )?TYPE 

345 # TEXT? 

346 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

347 

348 if not items: 348 ↛ 349line 348 didn't jump to line 349 because the condition on line 348 was never true

349 docstring_warning(docstring, new_offset, f"Empty returns section at line {offset}") 

350 return None, new_offset 

351 

352 returns = [] 

353 for index, item in enumerate(items): 

354 match = _RE_RETURNS.match(item[0]) 

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

356 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'") 

357 continue 

358 

359 groups = match.groupdict() 

360 name = groups["nt_name"] or groups["name"] 

361 annotation = groups["nt_type"] or groups["type"] 

362 text = dedent("\n".join(item[1:])) 

363 if annotation is None: 

364 # try to retrieve the annotation from the docstring parent 

365 with suppress(AttributeError, KeyError, ValueError): 

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

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

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

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

370 else: 

371 raise ValueError 

372 if len(items) > 1: 372 ↛ 365line 372 didn't jump to line 365

373 if annotation.is_tuple: 373 ↛ 374line 373 didn't jump to line 374 because the condition on line 373 was never true

374 annotation = annotation.slice.elements[index] 

375 else: 

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

377 return_item = annotation.slice 

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

379 return_item = annotation.slice.elements[2] 

380 else: 

381 raise ValueError 

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

383 annotation = return_item 

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

385 annotation = return_item.slice.elements[index] 

386 else: 

387 annotation = return_item 

388 else: 

389 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) 

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

391 return DocstringSectionReturns(returns), new_offset 

392 

393 

394def _read_yields_section( 

395 docstring: Docstring, 

396 *, 

397 offset: int, 

398 **options: Any, 

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

400 # yields 

401 # (NAME : )?TYPE 

402 # TEXT? 

403 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

404 

405 if not items: 405 ↛ 406line 405 didn't jump to line 406 because the condition on line 405 was never true

406 docstring_warning(docstring, new_offset, f"Empty yields section at line {offset}") 

407 return None, new_offset 

408 

409 yields = [] 

410 for index, item in enumerate(items): 

411 match = _RE_YIELDS.match(item[0]) 

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

413 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'") 

414 continue 

415 

416 groups = match.groupdict() 

417 name = groups["nt_name"] or groups["name"] 

418 annotation = groups["nt_type"] or groups["type"] 

419 text = dedent("\n".join(item[1:])) 

420 if annotation is None: 

421 # try to retrieve the annotation from the docstring parent 

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

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

424 if annotation.is_iterator: 

425 yield_item = annotation.slice 

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

427 yield_item = annotation.slice.elements[0] 

428 else: 

429 raise ValueError 

430 if isinstance(yield_item, ExprName): 

431 annotation = yield_item 

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

433 annotation = yield_item.slice.elements[index] 

434 else: 

435 annotation = yield_item 

436 else: 

437 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) 

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

439 return DocstringSectionYields(yields), new_offset 

440 

441 

442def _read_receives_section( 

443 docstring: Docstring, 

444 *, 

445 offset: int, 

446 **options: Any, 

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

448 # receives 

449 # (NAME : )?TYPE 

450 # TEXT? 

451 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

452 

453 if not items: 453 ↛ 454line 453 didn't jump to line 454 because the condition on line 453 was never true

454 docstring_warning(docstring, new_offset, f"Empty receives section at line {offset}") 

455 return None, new_offset 

456 

457 receives = [] 

458 for index, item in enumerate(items): 

459 match = _RE_RECEIVES.match(item[0]) 

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

461 docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'") 

462 continue 

463 

464 groups = match.groupdict() 

465 name = groups["nt_name"] or groups["name"] 

466 annotation = groups["nt_type"] or groups["type"] 

467 text = dedent("\n".join(item[1:])) 

468 if annotation is None: 

469 # try to retrieve the annotation from the docstring parent 

470 with suppress(AttributeError, KeyError): 

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

472 if annotation.is_generator: 472 ↛ 470line 472 didn't jump to line 470

473 receives_item = annotation.slice.elements[1] 

474 if isinstance(receives_item, ExprName): 

475 annotation = receives_item 

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

477 annotation = receives_item.slice.elements[index] 

478 else: 

479 annotation = receives_item 

480 else: 

481 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) 

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

483 return DocstringSectionReceives(receives), new_offset 

484 

485 

486def _read_raises_section( 

487 docstring: Docstring, 

488 *, 

489 offset: int, 

490 **options: Any, 

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

492 # raises 

493 # EXCEPTION 

494 # TEXT? 

495 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

496 

497 if not items: 497 ↛ 498line 497 didn't jump to line 498 because the condition on line 497 was never true

498 docstring_warning(docstring, new_offset, f"Empty raises section at line {offset}") 

499 return None, new_offset 

500 

501 raises = [] 

502 for item in items: 

503 annotation = parse_docstring_annotation(item[0], docstring) 

504 text = dedent("\n".join(item[1:])) 

505 raises.append(DocstringRaise(annotation=annotation, description=text)) 

506 return DocstringSectionRaises(raises), new_offset 

507 

508 

509def _read_warns_section( 

510 docstring: Docstring, 

511 *, 

512 offset: int, 

513 **options: Any, 

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

515 # warns 

516 # WARNING 

517 # TEXT? 

518 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

519 

520 if not items: 520 ↛ 521line 520 didn't jump to line 521 because the condition on line 520 was never true

521 docstring_warning(docstring, new_offset, f"Empty warns section at line {offset}") 

522 return None, new_offset 

523 

524 warns = [] 

525 for item in items: 

526 annotation = parse_docstring_annotation(item[0], docstring) 

527 text = dedent("\n".join(item[1:])) 

528 warns.append(DocstringWarn(annotation=annotation, description=text)) 

529 return DocstringSectionWarns(warns), new_offset 

530 

531 

532def _read_attributes_section( 

533 docstring: Docstring, 

534 *, 

535 offset: int, 

536 **options: Any, 

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

538 # attributes (for classes) 

539 # NAME( : TYPE)? 

540 # TEXT? 

541 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

542 

543 if not items: 543 ↛ 544line 543 didn't jump to line 544 because the condition on line 543 was never true

544 docstring_warning(docstring, new_offset, f"Empty attributes section at line {offset}") 

545 return None, new_offset 

546 

547 annotation: str | Expr | None 

548 attributes = [] 

549 for item in items: 

550 name_type = item[0] 

551 if ":" in name_type: 

552 name, annotation = name_type.split(":", 1) 

553 name = name.strip() 

554 annotation = annotation.strip() or None 

555 else: 

556 name = name_type 

557 annotation = None 

558 if annotation is None: 

559 with suppress(AttributeError, KeyError): 

560 annotation = docstring.parent.members[name].annotation # type: ignore[union-attr] 

561 else: 

562 annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) 

563 text = dedent("\n".join(item[1:])) 

564 attributes.append(DocstringAttribute(name=name, annotation=annotation, description=text)) 

565 return DocstringSectionAttributes(attributes), new_offset 

566 

567 

568def _read_functions_section( 

569 docstring: Docstring, 

570 *, 

571 offset: int, 

572 **options: Any, 

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

574 # SIGNATURE 

575 # TEXT? 

576 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

577 

578 if not items: 578 ↛ 579line 578 didn't jump to line 579 because the condition on line 578 was never true

579 docstring_warning(docstring, new_offset, f"Empty functions/methods section at line {offset}") 

580 return None, new_offset 

581 

582 functions = [] 

583 signature: str | Expr | None 

584 for item in items: 

585 name_signature = item[0] 

586 if "(" in name_signature: 

587 name = name_signature.split("(", 1)[0] 

588 name = name.strip() 

589 signature = name_signature.strip() 

590 else: 

591 name = name_signature 

592 signature = None 

593 text = dedent("\n".join(item[1:])).strip() 

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

595 return DocstringSectionFunctions(functions), new_offset 

596 

597 

598def _read_classes_section( 

599 docstring: Docstring, 

600 *, 

601 offset: int, 

602 **options: Any, 

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

604 # SIGNATURE 

605 # TEXT? 

606 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

607 

608 if not items: 608 ↛ 609line 608 didn't jump to line 609 because the condition on line 608 was never true

609 docstring_warning(docstring, new_offset, f"Empty classes section at line {offset}") 

610 return None, new_offset 

611 

612 classes = [] 

613 signature: str | Expr | None 

614 for item in items: 

615 name_signature = item[0] 

616 if "(" in name_signature: 

617 name = name_signature.split("(", 1)[0] 

618 name = name.strip() 

619 signature = name_signature.strip() 

620 else: 

621 name = name_signature 

622 signature = None 

623 text = dedent("\n".join(item[1:])).strip() 

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

625 return DocstringSectionClasses(classes), new_offset 

626 

627 

628def _read_modules_section( 

629 docstring: Docstring, 

630 *, 

631 offset: int, 

632 **options: Any, 

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

634 # NAME 

635 # TEXT? 

636 items, new_offset = _read_block_items(docstring, offset=offset, **options) 

637 

638 if not items: 638 ↛ 639line 638 didn't jump to line 639 because the condition on line 638 was never true

639 docstring_warning(docstring, new_offset, f"Empty modules section at line {offset}") 

640 return None, new_offset 

641 

642 modules = [] 

643 signature: str | Expr | None 

644 for item in items: 

645 name_signature = item[0] 

646 if "(" in name_signature: 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true

647 name = name_signature.split("(", 1)[0] 

648 name = name.strip() 

649 signature = name_signature.strip() 

650 else: 

651 name = name_signature 

652 signature = None 

653 text = dedent("\n".join(item[1:])).strip() 

654 modules.append(DocstringModule(name=name, annotation=signature, description=text)) 

655 return DocstringSectionModules(modules), new_offset 

656 

657 

658def _read_examples_section( 

659 docstring: Docstring, 

660 *, 

661 offset: int, 

662 trim_doctest_flags: bool = True, 

663 **options: Any, 

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

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

666 

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

668 in_code_example = False 

669 in_code_block = False 

670 current_text: list[str] = [] 

671 current_example: list[str] = [] 

672 

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

674 if _is_empty_line(line): 

675 if in_code_example: 

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

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

678 current_example = [] 

679 in_code_example = False 

680 else: 

681 current_text.append(line) 

682 

683 elif in_code_example: 

684 if trim_doctest_flags: 

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

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

687 current_example.append(line) 

688 

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

690 in_code_block = not in_code_block 

691 current_text.append(line) 

692 

693 elif in_code_block: 

694 current_text.append(line) 

695 

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

697 if current_text: 697 ↛ 700line 697 didn't jump to line 700 because the condition on line 697 was always true

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

699 current_text = [] 

700 in_code_example = True 

701 

702 if trim_doctest_flags: 

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

704 current_example.append(line) 

705 

706 else: 

707 current_text.append(line) 

708 

709 if current_text: 

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

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

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

713 

714 if sub_sections: 714 ↛ 717line 714 didn't jump to line 717 because the condition on line 714 was always true

715 return DocstringSectionExamples(sub_sections), new_offset 

716 

717 docstring_warning(docstring, new_offset, f"Empty examples section at line {offset}") 

718 return None, new_offset 

719 

720 

721def _append_section(sections: list, current: list[str], admonition_title: str) -> None: 

722 if admonition_title: 

723 kind = admonition_title.lower().replace(" ", "-") 

724 if kind in ("warnings", "notes"): 

725 # NumpyDoc sections are pluralised but admonitions aren't. 

726 # We can special-case these explicitly so that it renders 

727 # as one would expect. 

728 kind = kind[:-1] 

729 sections.append( 

730 DocstringSectionAdmonition( 

731 kind=kind, 

732 text="\n".join(current).rstrip("\n"), 

733 title=admonition_title, 

734 ), 

735 ) 

736 elif current and any(current): 

737 sections.append(DocstringSectionText("\n".join(current).rstrip("\n"))) 

738 

739 

740_section_reader = { 

741 DocstringSectionKind.parameters: _read_parameters_section, 

742 DocstringSectionKind.other_parameters: _read_other_parameters_section, 

743 DocstringSectionKind.deprecated: _read_deprecated_section, 

744 DocstringSectionKind.raises: _read_raises_section, 

745 DocstringSectionKind.warns: _read_warns_section, 

746 DocstringSectionKind.examples: _read_examples_section, 

747 DocstringSectionKind.attributes: _read_attributes_section, 

748 DocstringSectionKind.functions: _read_functions_section, 

749 DocstringSectionKind.classes: _read_classes_section, 

750 DocstringSectionKind.modules: _read_modules_section, 

751 DocstringSectionKind.returns: _read_returns_section, 

752 DocstringSectionKind.yields: _read_yields_section, 

753 DocstringSectionKind.receives: _read_receives_section, 

754} 

755 

756 

757def parse_numpy( 

758 docstring: Docstring, 

759 *, 

760 ignore_init_summary: bool = False, 

761 trim_doctest_flags: bool = True, 

762 warn_unknown_params: bool = True, 

763 **options: Any, 

764) -> list[DocstringSection]: 

765 """Parse a Numpydoc-style docstring. 

766 

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

768 It then returns this list of sections. 

769 

770 Parameters: 

771 docstring: The docstring to parse. 

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

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

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

775 **options: Additional parsing options. 

776 

777 Returns: 

778 A list of docstring sections. 

779 """ 

780 sections: list[DocstringSection] = [] 

781 current_section = [] 

782 admonition_title = "" 

783 

784 in_code_block = False 

785 lines = docstring.lines 

786 

787 options = { 

788 "trim_doctest_flags": trim_doctest_flags, 

789 "ignore_init_summary": ignore_init_summary, 

790 "warn_unknown_params": warn_unknown_params, 

791 **options, 

792 } 

793 

794 ignore_summary = ( 

795 options["ignore_init_summary"] 

796 and docstring.parent is not None 

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

798 and docstring.parent.is_function 

799 and docstring.parent.parent is not None 

800 and docstring.parent.parent.is_class 

801 ) 

802 

803 offset = 2 if ignore_summary else 0 

804 

805 while offset < len(lines): 

806 line_lower = lines[offset].lower() 

807 

808 # Code blocks can contain dash lines that we must not interpret. 

809 if in_code_block: 

810 # End of code block. 

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

812 in_code_block = False 

813 # Lines in code block must not be interpreted in any way. 

814 current_section.append(lines[offset]) 

815 

816 # Start of code block. 

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

818 in_code_block = True 

819 current_section.append(lines[offset]) 

820 

821 # Dash lines after empty lines lose their meaning. 

822 elif _is_empty_line(lines[offset]): 

823 current_section.append("") 

824 

825 # End of the docstring, wrap up. 

826 elif offset == len(lines) - 1: 

827 current_section.append(lines[offset]) 

828 _append_section(sections, current_section, admonition_title) 

829 admonition_title = "" 

830 current_section = [] 

831 

832 # Dash line after regular, non-empty line. 

833 elif _is_dash_line(lines[offset + 1]): 

834 # Finish reading current section. 

835 _append_section(sections, current_section, admonition_title) 

836 current_section = [] 

837 

838 # Start parsing new (known) section. 

839 if line_lower in _section_kind: 

840 admonition_title = "" 

841 reader = _section_reader[_section_kind[line_lower]] 

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

843 if section: 843 ↛ 855line 843 didn't jump to line 855 because the condition on line 843 was always true

844 sections.append(section) 

845 

846 # Start parsing admonition. 

847 else: 

848 admonition_title = lines[offset] 

849 offset += 1 # skip next dash line 

850 

851 # Regular line. 

852 else: 

853 current_section.append(lines[offset]) 

854 

855 offset += 1 

856 

857 # Finish current section. 

858 _append_section(sections, current_section, admonition_title) 

859 

860 return sections