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

422 statements  

« prev     ^ index     » next       coverage.py v7.6.2, created at 2024-10-12 01:34 +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 re import Pattern 

61 from typing import Any, Literal 

62 

63 from _griffe.expressions import Expr 

64 from _griffe.models import Docstring 

65 

66 

67_section_kind = { 

68 "deprecated": DocstringSectionKind.deprecated, 

69 "parameters": DocstringSectionKind.parameters, 

70 "other parameters": DocstringSectionKind.other_parameters, 

71 "returns": DocstringSectionKind.returns, 

72 "yields": DocstringSectionKind.yields, 

73 "receives": DocstringSectionKind.receives, 

74 "raises": DocstringSectionKind.raises, 

75 "warns": DocstringSectionKind.warns, 

76 "examples": DocstringSectionKind.examples, 

77 "attributes": DocstringSectionKind.attributes, 

78 "functions": DocstringSectionKind.functions, 

79 "methods": DocstringSectionKind.functions, 

80 "classes": DocstringSectionKind.classes, 

81 "modules": DocstringSectionKind.modules, 

82} 

83 

84 

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

86 return not line.strip() 

87 

88 

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

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

91 

92 

93def _read_block_items( 

94 docstring: Docstring, 

95 *, 

96 offset: int, 

97 **options: Any, # noqa: ARG001 

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

99 lines = docstring.lines 

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

101 return [], offset 

102 

103 new_offset = offset 

104 items: list[list[str]] = [] 

105 

106 # skip first empty lines 

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

108 new_offset += 1 

109 

110 # start processing first item 

111 current_item = [lines[new_offset]] 

112 new_offset += 1 

113 

114 # loop on next lines 

115 while new_offset < len(lines): 

116 line = lines[new_offset] 

117 

118 if _is_empty_line(line): 

119 # empty line: preserve it in the current item 

120 current_item.append("") 

121 

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

123 # continuation line 

124 current_item.append(line[4:]) 

125 

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

127 # indent between initial and continuation: append but warn 

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

129 current_item.append(line[cont_indent:]) 

130 docstring_warning( 

131 docstring, 

132 new_offset, 

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

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

135 ) 

136 

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

138 # detect the start of a new section 

139 break 

140 

141 else: 

142 items.append(current_item) 

143 current_item = [line] 

144 

145 new_offset += 1 

146 

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

148 items.append(current_item) 

149 

150 return items, new_offset - 1 

151 

152 

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

154 lines = docstring.lines 

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

156 return "", offset 

157 

158 new_offset = offset 

159 block: list[str] = [] 

160 

161 # skip first empty lines 

162 while _is_empty_line(lines[new_offset]): 

163 new_offset += 1 

164 while new_offset < len(lines): 

165 is_empty = _is_empty_line(lines[new_offset]) 

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

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

168 

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

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

171 

172 block.append(lines[new_offset]) 

173 new_offset += 1 

174 

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

176 

177 

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

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

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

181_RE_TYPE: str = r".+" 

182_RE_RETURNS: Pattern = re.compile( 

183 rf""" 

184 (?: 

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

186 | # or 

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

188 | # or 

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

190 | # or 

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

192 ) 

193 """, 

194 re.IGNORECASE | re.VERBOSE, 

195) 

196_RE_YIELDS: Pattern = _RE_RETURNS 

197_RE_RECEIVES: Pattern = _RE_RETURNS 

198_RE_PARAMETER: Pattern = re.compile( 

199 rf""" 

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

201 (?: 

202 \s:\s 

203 (?: 

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

205 (?P<type>{_RE_TYPE}) 

206 )? 

207 )? 

208 """, 

209 re.IGNORECASE | re.VERBOSE, 

210) 

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

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

213 

214 

215def _read_parameters( 

216 docstring: Docstring, 

217 *, 

218 offset: int, 

219 warn_unknown_params: bool = True, 

220 **options: Any, 

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

222 parameters: list[DocstringParameter] = [] 

223 annotation: str | Expr | None 

224 

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

226 

227 for item in items: 

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

229 if not match: 

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

231 continue 

232 

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

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

235 choices = match.group("choices") 

236 default = None 

237 if choices: 

238 annotation = choices 

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

240 elif annotation: 

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

242 if match: 

243 default = match.group("default") 

244 annotation = match.group("annotation") 

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

246 annotation = annotation[:-10] 

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

248 

249 if annotation is None: 

250 # try to use the annotation from the signature 

251 for name in names: 

252 with suppress(AttributeError, KeyError): 

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

254 break 

255 else: 

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

257 else: 

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

259 

260 if default is None: 

261 for name in names: 

262 with suppress(AttributeError, KeyError): 

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

264 break 

265 

266 if warn_unknown_params: 

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

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

269 for name in names: 

270 if name not in params: 

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

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

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

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

275 break 

276 docstring_warning(docstring, new_offset, message) 

277 

278 parameters.extend( 

279 DocstringParameter(name, value=default, annotation=annotation, description=description) for name in names 

280 ) 

281 

282 return parameters, new_offset 

283 

284 

285def _read_parameters_section( 

286 docstring: Docstring, 

287 *, 

288 offset: int, 

289 **options: Any, 

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

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

292 

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

294 return DocstringSectionParameters(parameters), new_offset 

295 

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

297 return None, new_offset 

298 

299 

300def _read_other_parameters_section( 

301 docstring: Docstring, 

302 *, 

303 offset: int, 

304 warn_unknown_params: bool = True, # noqa: ARG001 

305 **options: Any, 

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

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

308 

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

310 return DocstringSectionOtherParameters(parameters), new_offset 

311 

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

313 return None, new_offset 

314 

315 

316def _read_deprecated_section( 

317 docstring: Docstring, 

318 *, 

319 offset: int, 

320 **options: Any, 

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

322 # deprecated 

323 # SINCE_VERSION 

324 # TEXT? 

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

326 

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

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

329 return None, new_offset 

330 

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

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

333 

334 item = items[0] 

335 version = item[0] 

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

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

338 

339 

340def _read_returns_section( 

341 docstring: Docstring, 

342 *, 

343 offset: int, 

344 **options: Any, 

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

346 # (NAME : )?TYPE 

347 # TEXT? 

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

349 

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

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

352 return None, new_offset 

353 

354 returns = [] 

355 for index, item in enumerate(items): 

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

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

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

359 continue 

360 

361 groups = match.groupdict() 

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

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

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

365 if annotation is None: 

366 # try to retrieve the annotation from the docstring parent 

367 with suppress(AttributeError, KeyError, ValueError): 

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

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

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

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

372 else: 

373 raise ValueError 

374 if len(items) > 1: 374 ↛ 392line 374 didn't jump to line 392

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

376 annotation = annotation.slice.elements[index] 

377 else: 

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

379 return_item = annotation.slice 

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

381 return_item = annotation.slice.elements[2] 

382 else: 

383 raise ValueError 

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

385 annotation = return_item 

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

387 annotation = return_item.slice.elements[index] 

388 else: 

389 annotation = return_item 

390 else: 

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

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

393 return DocstringSectionReturns(returns), new_offset 

394 

395 

396def _read_yields_section( 

397 docstring: Docstring, 

398 *, 

399 offset: int, 

400 **options: Any, 

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

402 # yields 

403 # (NAME : )?TYPE 

404 # TEXT? 

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

406 

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

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

409 return None, new_offset 

410 

411 yields = [] 

412 for index, item in enumerate(items): 

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

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

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

416 continue 

417 

418 groups = match.groupdict() 

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

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

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

422 if annotation is None: 

423 # try to retrieve the annotation from the docstring parent 

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

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

426 if annotation.is_iterator: 

427 yield_item = annotation.slice 

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

429 yield_item = annotation.slice.elements[0] 

430 else: 

431 raise ValueError 

432 if isinstance(yield_item, ExprName): 

433 annotation = yield_item 

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

435 annotation = yield_item.slice.elements[index] 

436 else: 

437 annotation = yield_item 

438 else: 

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

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

441 return DocstringSectionYields(yields), new_offset 

442 

443 

444def _read_receives_section( 

445 docstring: Docstring, 

446 *, 

447 offset: int, 

448 **options: Any, 

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

450 # receives 

451 # (NAME : )?TYPE 

452 # TEXT? 

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

454 

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

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

457 return None, new_offset 

458 

459 receives = [] 

460 for index, item in enumerate(items): 

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

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

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

464 continue 

465 

466 groups = match.groupdict() 

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

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

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

470 if annotation is None: 

471 # try to retrieve the annotation from the docstring parent 

472 with suppress(AttributeError, KeyError): 

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

474 if annotation.is_generator: 474 ↛ 484line 474 didn't jump to line 484

475 receives_item = annotation.slice.elements[1] 

476 if isinstance(receives_item, ExprName): 

477 annotation = receives_item 

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

479 annotation = receives_item.slice.elements[index] 

480 else: 

481 annotation = receives_item 

482 else: 

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

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

485 return DocstringSectionReceives(receives), new_offset 

486 

487 

488def _read_raises_section( 

489 docstring: Docstring, 

490 *, 

491 offset: int, 

492 **options: Any, 

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

494 # raises 

495 # EXCEPTION 

496 # TEXT? 

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

498 

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

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

501 return None, new_offset 

502 

503 raises = [] 

504 for item in items: 

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

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

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

508 return DocstringSectionRaises(raises), new_offset 

509 

510 

511def _read_warns_section( 

512 docstring: Docstring, 

513 *, 

514 offset: int, 

515 **options: Any, 

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

517 # warns 

518 # WARNING 

519 # TEXT? 

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

521 

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

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

524 return None, new_offset 

525 

526 warns = [] 

527 for item in items: 

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

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

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

531 return DocstringSectionWarns(warns), new_offset 

532 

533 

534def _read_attributes_section( 

535 docstring: Docstring, 

536 *, 

537 offset: int, 

538 **options: Any, 

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

540 # attributes (for classes) 

541 # NAME( : TYPE)? 

542 # TEXT? 

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

544 

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

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

547 return None, new_offset 

548 

549 annotation: str | Expr | None 

550 attributes = [] 

551 for item in items: 

552 name_type = item[0] 

553 if ":" in name_type: 

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

555 name = name.strip() 

556 annotation = annotation.strip() or None 

557 else: 

558 name = name_type 

559 annotation = None 

560 if annotation is None: 

561 with suppress(AttributeError, KeyError): 

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

563 else: 

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

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

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

567 return DocstringSectionAttributes(attributes), new_offset 

568 

569 

570def _read_functions_section( 

571 docstring: Docstring, 

572 *, 

573 offset: int, 

574 **options: Any, 

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

576 # SIGNATURE 

577 # TEXT? 

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

579 

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

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

582 return None, new_offset 

583 

584 functions = [] 

585 signature: str | Expr | None 

586 for item in items: 

587 name_signature = item[0] 

588 if "(" in name_signature: 

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

590 name = name.strip() 

591 signature = name_signature.strip() 

592 else: 

593 name = name_signature 

594 signature = None 

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

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

597 return DocstringSectionFunctions(functions), new_offset 

598 

599 

600def _read_classes_section( 

601 docstring: Docstring, 

602 *, 

603 offset: int, 

604 **options: Any, 

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

606 # SIGNATURE 

607 # TEXT? 

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

609 

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

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

612 return None, new_offset 

613 

614 classes = [] 

615 signature: str | Expr | None 

616 for item in items: 

617 name_signature = item[0] 

618 if "(" in name_signature: 

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

620 name = name.strip() 

621 signature = name_signature.strip() 

622 else: 

623 name = name_signature 

624 signature = None 

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

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

627 return DocstringSectionClasses(classes), new_offset 

628 

629 

630def _read_modules_section( 

631 docstring: Docstring, 

632 *, 

633 offset: int, 

634 **options: Any, 

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

636 # NAME 

637 # TEXT? 

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

639 

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

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

642 return None, new_offset 

643 

644 modules = [] 

645 signature: str | Expr | None 

646 for item in items: 

647 name_signature = item[0] 

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

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

650 name = name.strip() 

651 signature = name_signature.strip() 

652 else: 

653 name = name_signature 

654 signature = None 

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

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

657 return DocstringSectionModules(modules), new_offset 

658 

659 

660def _read_examples_section( 

661 docstring: Docstring, 

662 *, 

663 offset: int, 

664 trim_doctest_flags: bool = True, 

665 **options: Any, 

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

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

668 

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

670 in_code_example = False 

671 in_code_block = False 

672 current_text: list[str] = [] 

673 current_example: list[str] = [] 

674 

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

676 if _is_empty_line(line): 

677 if in_code_example: 

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

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

680 current_example = [] 

681 in_code_example = False 

682 else: 

683 current_text.append(line) 

684 

685 elif in_code_example: 

686 if trim_doctest_flags: 

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

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

689 current_example.append(line) 

690 

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

692 in_code_block = not in_code_block 

693 current_text.append(line) 

694 

695 elif in_code_block: 

696 current_text.append(line) 

697 

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

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

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

701 current_text = [] 

702 in_code_example = True 

703 

704 if trim_doctest_flags: 

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

706 current_example.append(line) 

707 

708 else: 

709 current_text.append(line) 

710 

711 if current_text: 

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

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

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

715 

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

717 return DocstringSectionExamples(sub_sections), new_offset 

718 

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

720 return None, new_offset 

721 

722 

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

724 if admonition_title: 

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

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

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

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

729 # as one would expect. 

730 kind = kind[:-1] 

731 sections.append( 

732 DocstringSectionAdmonition( 

733 kind=kind, 

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

735 title=admonition_title, 

736 ), 

737 ) 

738 elif current and any(current): 

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

740 

741 

742_section_reader = { 

743 DocstringSectionKind.parameters: _read_parameters_section, 

744 DocstringSectionKind.other_parameters: _read_other_parameters_section, 

745 DocstringSectionKind.deprecated: _read_deprecated_section, 

746 DocstringSectionKind.raises: _read_raises_section, 

747 DocstringSectionKind.warns: _read_warns_section, 

748 DocstringSectionKind.examples: _read_examples_section, 

749 DocstringSectionKind.attributes: _read_attributes_section, 

750 DocstringSectionKind.functions: _read_functions_section, 

751 DocstringSectionKind.classes: _read_classes_section, 

752 DocstringSectionKind.modules: _read_modules_section, 

753 DocstringSectionKind.returns: _read_returns_section, 

754 DocstringSectionKind.yields: _read_yields_section, 

755 DocstringSectionKind.receives: _read_receives_section, 

756} 

757 

758 

759def parse_numpy( 

760 docstring: Docstring, 

761 *, 

762 ignore_init_summary: bool = False, 

763 trim_doctest_flags: bool = True, 

764 warn_unknown_params: bool = True, 

765 **options: Any, 

766) -> list[DocstringSection]: 

767 """Parse a Numpydoc-style docstring. 

768 

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

770 It then returns this list of sections. 

771 

772 Parameters: 

773 docstring: The docstring to parse. 

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

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

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

777 **options: Additional parsing options. 

778 

779 Returns: 

780 A list of docstring sections. 

781 """ 

782 sections: list[DocstringSection] = [] 

783 current_section = [] 

784 admonition_title = "" 

785 

786 in_code_block = False 

787 lines = docstring.lines 

788 

789 options = { 

790 "trim_doctest_flags": trim_doctest_flags, 

791 "ignore_init_summary": ignore_init_summary, 

792 "warn_unknown_params": warn_unknown_params, 

793 **options, 

794 } 

795 

796 ignore_summary = ( 

797 options["ignore_init_summary"] 

798 and docstring.parent is not None 

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

800 and docstring.parent.is_function 

801 and docstring.parent.parent is not None 

802 and docstring.parent.parent.is_class 

803 ) 

804 

805 offset = 2 if ignore_summary else 0 

806 

807 while offset < len(lines): 

808 line_lower = lines[offset].lower() 

809 

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

811 if in_code_block: 

812 # End of code block. 

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

814 in_code_block = False 

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

816 current_section.append(lines[offset]) 

817 

818 # Start of code block. 

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

820 in_code_block = True 

821 current_section.append(lines[offset]) 

822 

823 # Dash lines after empty lines lose their meaning. 

824 elif _is_empty_line(lines[offset]): 

825 current_section.append("") 

826 

827 # End of the docstring, wrap up. 

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

829 current_section.append(lines[offset]) 

830 _append_section(sections, current_section, admonition_title) 

831 admonition_title = "" 

832 current_section = [] 

833 

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

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

836 # Finish reading current section. 

837 _append_section(sections, current_section, admonition_title) 

838 current_section = [] 

839 

840 # Start parsing new (known) section. 

841 if line_lower in _section_kind: 

842 admonition_title = "" 

843 reader = _section_reader[_section_kind[line_lower]] 

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

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

846 sections.append(section) 

847 

848 # Start parsing admonition. 

849 else: 

850 admonition_title = lines[offset] 

851 offset += 1 # skip next dash line 

852 

853 # Regular line. 

854 else: 

855 current_section.append(lines[offset]) 

856 

857 offset += 1 

858 

859 # Finish current section. 

860 _append_section(sections, current_section, admonition_title) 

861 

862 return sections