Coverage for src/griffe/_internal/docstrings/numpy.py: 69.33%

500 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-14 23:10 +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._internal.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 DocstringSectionTypeAliases, 

51 DocstringSectionTypeParameters, 

52 DocstringSectionWarns, 

53 DocstringSectionYields, 

54 DocstringTypeAlias, 

55 DocstringTypeParameter, 

56 DocstringWarn, 

57 DocstringYield, 

58) 

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

60from griffe._internal.enumerations import DocstringSectionKind, LogLevel 

61from griffe._internal.expressions import ExprName 

62 

63if TYPE_CHECKING: 

64 from re import Pattern 

65 from typing import Any, Literal 

66 

67 from griffe._internal.expressions import Expr 

68 from griffe._internal.models import Docstring 

69 

70 

71_section_kind = { 

72 "deprecated": DocstringSectionKind.deprecated, 

73 "parameters": DocstringSectionKind.parameters, 

74 "other parameters": DocstringSectionKind.other_parameters, 

75 "type parameters": DocstringSectionKind.type_parameters, 

76 "returns": DocstringSectionKind.returns, 

77 "yields": DocstringSectionKind.yields, 

78 "receives": DocstringSectionKind.receives, 

79 "raises": DocstringSectionKind.raises, 

80 "warns": DocstringSectionKind.warns, 

81 "examples": DocstringSectionKind.examples, 

82 "attributes": DocstringSectionKind.attributes, 

83 "functions": DocstringSectionKind.functions, 

84 "methods": DocstringSectionKind.functions, 

85 "classes": DocstringSectionKind.classes, 

86 "type aliases": DocstringSectionKind.type_aliases, 

87 "modules": DocstringSectionKind.modules, 

88} 

89 

90 

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

92 return not line.strip() 

93 

94 

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

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

97 

98 

99def _read_block_items( 

100 docstring: Docstring, 

101 *, 

102 offset: int, 

103 warnings: bool = True, 

104 **options: Any, # noqa: ARG001 

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

106 lines = docstring.lines 

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

108 return [], offset 

109 

110 new_offset = offset 

111 items: list[list[str]] = [] 

112 

113 # skip first empty lines 

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

115 new_offset += 1 

116 

117 # start processing first item 

118 current_item = [lines[new_offset]] 

119 new_offset += 1 

120 

121 # loop on next lines 

122 while new_offset < len(lines): 

123 line = lines[new_offset] 

124 

125 if _is_empty_line(line): 

126 # empty line: preserve it in the current item 

127 current_item.append("") 

128 

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

130 # continuation line 

131 current_item.append(line[4:]) 

132 

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

134 # indent between initial and continuation: append but warn 

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

136 current_item.append(line[cont_indent:]) 

137 if warnings: 

138 docstring_warning( 

139 docstring, 

140 new_offset, 

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

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

143 ) 

144 

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

146 # detect the start of a new section 

147 break 

148 

149 else: 

150 items.append(current_item) 

151 current_item = [line] 

152 

153 new_offset += 1 

154 

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

156 items.append(current_item) 

157 

158 return items, new_offset - 1 

159 

160 

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

162 lines = docstring.lines 

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

164 return "", offset 

165 

166 new_offset = offset 

167 block: list[str] = [] 

168 

169 # skip first empty lines 

170 while _is_empty_line(lines[new_offset]): 

171 new_offset += 1 

172 while new_offset < len(lines): 

173 is_empty = _is_empty_line(lines[new_offset]) 

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

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

176 

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

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

179 

180 block.append(lines[new_offset]) 

181 new_offset += 1 

182 

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

184 

185 

186_RE_OB: str = r"\{" # Opening bracket. 

187_RE_CB: str = r"\}" # Closing bracket. 

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

189_RE_TYPE: str = r".+" 

190_RE_RETURNS: Pattern = re.compile( 

191 rf""" 

192 (?: 

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

194 | # or 

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

196 | # or 

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

198 | # or 

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

200 ) 

201 """, 

202 re.IGNORECASE | re.VERBOSE, 

203) 

204_RE_YIELDS: Pattern = _RE_RETURNS 

205_RE_RECEIVES: Pattern = _RE_RETURNS 

206_RE_PARAMETER: Pattern = re.compile( 

207 rf""" 

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

209 (?: 

210 \s:\s 

211 (?: 

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

213 (?P<type>{_RE_TYPE}) 

214 )? 

215 )? 

216 """, 

217 re.IGNORECASE | re.VERBOSE, 

218) 

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

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

221 

222 

223def _read_parameters( 

224 docstring: Docstring, 

225 *, 

226 offset: int, 

227 warn_unknown_params: bool = True, 

228 warn_missing_types: bool = True, 

229 warnings: bool = True, 

230 **options: Any, 

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

232 parameters: list[DocstringParameter] = [] 

233 annotation: str | Expr | None 

234 

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

236 

237 for item in items: 

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

239 if not match: 

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

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

242 continue 

243 

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

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

246 choices = match.group("choices") 

247 default = None 

248 if choices: 

249 annotation = choices 

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

251 elif annotation: 

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

253 if match: 

254 default = match.group("default") 

255 annotation = match.group("annotation") 

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

257 annotation = annotation[:-10] 

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

259 

260 if annotation is None: 

261 # try to use the annotation from the signature 

262 for name in names: 

263 with suppress(AttributeError, KeyError): 

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

265 break 

266 else: 

267 if warnings and warn_missing_types: 

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

269 else: 

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

271 

272 if default is None: 

273 for name in names: 

274 with suppress(AttributeError, KeyError): 

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

276 break 

277 

278 if warnings and warn_unknown_params: 

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

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

281 for name in names: 

282 if name not in params: 

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

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

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

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

287 break 

288 docstring_warning(docstring, new_offset, message) 

289 

290 parameters.extend( 

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

292 ) 

293 

294 return parameters, new_offset 

295 

296 

297def _read_parameters_section( 

298 docstring: Docstring, 

299 *, 

300 offset: int, 

301 warnings: bool = True, 

302 **options: Any, 

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

304 parameters, new_offset = _read_parameters(docstring, offset=offset, warnings=warnings, **options) 

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

306 return DocstringSectionParameters(parameters), new_offset 

307 if warnings: 

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

309 return None, new_offset 

310 

311 

312def _read_other_parameters_section( 

313 docstring: Docstring, 

314 *, 

315 offset: int, 

316 warn_unknown_params: bool = True, # noqa: ARG001 

317 warnings: bool = True, 

318 **options: Any, 

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

320 parameters, new_offset = _read_parameters( 

321 docstring, 

322 offset=offset, 

323 warn_unknown_params=False, 

324 warnings=warnings, 

325 **options, 

326 ) 

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

328 return DocstringSectionOtherParameters(parameters), new_offset 

329 if warnings: 

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

331 return None, new_offset 

332 

333 

334def _read_type_parameters_section( 

335 docstring: Docstring, 

336 *, 

337 offset: int, 

338 warn_unknown_params: bool = True, 

339 **options: Any, 

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

341 type_parameters: list[DocstringTypeParameter] = [] 

342 bound: str | Expr | None 

343 

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

345 

346 for item in items: 

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

348 if not match: 

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

350 continue 

351 

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

353 bound = match.group("type") or None 

354 choices = match.group("choices") 

355 default = None 

356 if choices: 

357 bound = choices 

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

359 elif bound: 

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

361 if match: 

362 default = match.group("default") 

363 bound = match.group("annotation") 

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

365 

366 if bound is None: 

367 # try to use the bound from the signature 

368 for name in names: 

369 with suppress(AttributeError, KeyError): 

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

371 break 

372 else: 

373 bound = parse_docstring_annotation(bound, docstring, log_level=LogLevel.debug) 

374 

375 if default is None: 

376 for name in names: 

377 with suppress(AttributeError, KeyError): 

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

379 break 

380 

381 if warn_unknown_params: 

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

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

384 for name in names: 

385 if name not in type_params: 

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

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

388 if starred_name in type_params: 

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

390 break 

391 docstring_warning(docstring, new_offset, message) 

392 

393 type_parameters.extend( 

394 DocstringTypeParameter(name, value=default, annotation=bound, description=description) for name in names 

395 ) 

396 

397 if type_parameters: 

398 return DocstringSectionTypeParameters(type_parameters), new_offset 

399 

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

401 return None, new_offset 

402 

403 

404def _read_deprecated_section( 

405 docstring: Docstring, 

406 *, 

407 offset: int, 

408 warnings: bool = True, 

409 **options: Any, 

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

411 # deprecated 

412 # SINCE_VERSION 

413 # TEXT? 

414 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

415 

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

417 if warnings: 

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

419 return None, new_offset 

420 

421 if warnings and len(items) > 1: 421 ↛ 422line 421 didn't jump to line 422 because the condition on line 421 was never true

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

423 

424 item = items[0] 

425 version = item[0] 

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

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

428 

429 

430def _read_returns_section( 

431 docstring: Docstring, 

432 *, 

433 offset: int, 

434 warnings: bool = True, 

435 **options: Any, 

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

437 # (NAME : )?TYPE 

438 # TEXT? 

439 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

440 

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

442 if warnings: 

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

444 return None, new_offset 

445 

446 returns = [] 

447 for index, item in enumerate(items): 

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

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

450 if warnings: 

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

452 continue 

453 

454 groups = match.groupdict() 

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

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

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

458 if annotation is None: 

459 # try to retrieve the annotation from the docstring parent 

460 with suppress(AttributeError, KeyError, ValueError): 

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

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

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

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

465 else: 

466 raise ValueError 

467 if len(items) > 1: 467 ↛ 485line 467 didn't jump to line 485

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

469 annotation = annotation.slice.elements[index] 

470 else: 

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

472 return_item = annotation.slice 

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

474 return_item = annotation.slice.elements[2] 

475 else: 

476 raise ValueError 

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

478 annotation = return_item 

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

480 annotation = return_item.slice.elements[index] 

481 else: 

482 annotation = return_item 

483 else: 

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

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

486 return DocstringSectionReturns(returns), new_offset 

487 

488 

489def _read_yields_section( 

490 docstring: Docstring, 

491 *, 

492 offset: int, 

493 warnings: bool = True, 

494 **options: Any, 

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

496 # yields 

497 # (NAME : )?TYPE 

498 # TEXT? 

499 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

500 

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

502 if warnings: 

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

504 return None, new_offset 

505 

506 yields = [] 

507 for index, item in enumerate(items): 

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

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

510 if warnings: 

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

512 continue 

513 

514 groups = match.groupdict() 

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

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

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

518 if annotation is None: 

519 # try to retrieve the annotation from the docstring parent 

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

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

522 if annotation.is_iterator: 

523 yield_item = annotation.slice 

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

525 yield_item = annotation.slice.elements[0] 

526 else: 

527 raise ValueError 

528 if isinstance(yield_item, ExprName): 

529 annotation = yield_item 

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

531 annotation = yield_item.slice.elements[index] 

532 else: 

533 annotation = yield_item 

534 else: 

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

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

537 return DocstringSectionYields(yields), new_offset 

538 

539 

540def _read_receives_section( 

541 docstring: Docstring, 

542 *, 

543 offset: int, 

544 warnings: bool = True, 

545 **options: Any, 

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

547 # receives 

548 # (NAME : )?TYPE 

549 # TEXT? 

550 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

551 

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

553 if warnings: 

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

555 return None, new_offset 

556 

557 receives = [] 

558 for index, item in enumerate(items): 

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

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

561 if warnings: 

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

563 continue 

564 

565 groups = match.groupdict() 

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

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

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

569 if annotation is None: 

570 # try to retrieve the annotation from the docstring parent 

571 with suppress(AttributeError, KeyError): 

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

573 if annotation.is_generator: 573 ↛ 583line 573 didn't jump to line 583

574 receives_item = annotation.slice.elements[1] 

575 if isinstance(receives_item, ExprName): 

576 annotation = receives_item 

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

578 annotation = receives_item.slice.elements[index] 

579 else: 

580 annotation = receives_item 

581 else: 

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

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

584 return DocstringSectionReceives(receives), new_offset 

585 

586 

587def _read_raises_section( 

588 docstring: Docstring, 

589 *, 

590 offset: int, 

591 warnings: bool = True, 

592 **options: Any, 

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

594 # raises 

595 # EXCEPTION 

596 # TEXT? 

597 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

598 

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

600 if warnings: 

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

602 return None, new_offset 

603 

604 raises = [] 

605 for item in items: 

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

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

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

609 return DocstringSectionRaises(raises), new_offset 

610 

611 

612def _read_warns_section( 

613 docstring: Docstring, 

614 *, 

615 offset: int, 

616 warnings: bool = True, 

617 **options: Any, 

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

619 # warns 

620 # WARNING 

621 # TEXT? 

622 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

623 

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

625 if warnings: 

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

627 return None, new_offset 

628 

629 warns = [] 

630 for item in items: 

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

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

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

634 return DocstringSectionWarns(warns), new_offset 

635 

636 

637def _read_attributes_section( 

638 docstring: Docstring, 

639 *, 

640 offset: int, 

641 warnings: bool = True, 

642 **options: Any, 

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

644 # attributes (for classes) 

645 # NAME( : TYPE)? 

646 # TEXT? 

647 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

648 

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

650 if warnings: 

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

652 return None, new_offset 

653 

654 annotation: str | Expr | None 

655 attributes = [] 

656 for item in items: 

657 name_type = item[0] 

658 if ":" in name_type: 

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

660 name = name.strip() 

661 annotation = annotation.strip() or None 

662 else: 

663 name = name_type 

664 annotation = None 

665 if annotation is None: 

666 with suppress(AttributeError, KeyError, TypeError): 

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

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

669 else: 

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

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

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

673 return DocstringSectionAttributes(attributes), new_offset 

674 

675 

676def _read_functions_section( 

677 docstring: Docstring, 

678 *, 

679 offset: int, 

680 warnings: bool = True, 

681 **options: Any, 

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

683 # SIGNATURE 

684 # TEXT? 

685 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

686 

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

688 if warnings: 

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

690 return None, new_offset 

691 

692 functions = [] 

693 signature: str | Expr | None 

694 for item in items: 

695 name_signature = item[0] 

696 if "(" in name_signature: 

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

698 name = name.strip() 

699 signature = name_signature.strip() 

700 else: 

701 name = name_signature 

702 signature = None 

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

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

705 return DocstringSectionFunctions(functions), new_offset 

706 

707 

708def _read_classes_section( 

709 docstring: Docstring, 

710 *, 

711 offset: int, 

712 warnings: bool = True, 

713 **options: Any, 

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

715 # SIGNATURE 

716 # TEXT? 

717 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

718 

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

720 if warnings: 

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

722 return None, new_offset 

723 

724 classes = [] 

725 signature: str | Expr | None 

726 for item in items: 

727 name_signature = item[0] 

728 if "(" in name_signature: 

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

730 name = name.strip() 

731 signature = name_signature.strip() 

732 else: 

733 name = name_signature 

734 signature = None 

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

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

737 return DocstringSectionClasses(classes), new_offset 

738 

739 

740def _read_type_aliases_section( 

741 docstring: Docstring, 

742 *, 

743 offset: int, 

744 **options: Any, 

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

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

747 

748 if not items: 

749 docstring_warning(docstring, new_offset, f"Empty type aliases section at line {offset}") 

750 return None, new_offset 

751 

752 type_aliases = [] 

753 for item in items: 

754 name = item[0] 

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

756 type_aliases.append(DocstringTypeAlias(name=name, description=text)) 

757 return DocstringSectionTypeAliases(type_aliases), new_offset 

758 

759 

760def _read_modules_section( 

761 docstring: Docstring, 

762 *, 

763 offset: int, 

764 warnings: bool = True, 

765 **options: Any, 

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

767 # NAME 

768 # TEXT? 

769 items, new_offset = _read_block_items(docstring, offset=offset, warnings=warnings, **options) 

770 

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

772 if warnings: 

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

774 return None, new_offset 

775 

776 modules = [] 

777 signature: str | Expr | None 

778 for item in items: 

779 name_signature = item[0] 

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

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

782 name = name.strip() 

783 signature = name_signature.strip() 

784 else: 

785 name = name_signature 

786 signature = None 

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

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

789 return DocstringSectionModules(modules), new_offset 

790 

791 

792def _read_examples_section( 

793 docstring: Docstring, 

794 *, 

795 offset: int, 

796 trim_doctest_flags: bool = True, 

797 warnings: bool = True, 

798 **options: Any, 

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

800 text, new_offset = _read_block(docstring, offset=offset, warnings=warnings, **options) 

801 

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

803 in_code_example = False 

804 in_code_block = False 

805 current_text: list[str] = [] 

806 current_example: list[str] = [] 

807 

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

809 if _is_empty_line(line): 

810 if in_code_example: 

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

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

813 current_example = [] 

814 in_code_example = False 

815 else: 

816 current_text.append(line) 

817 

818 elif in_code_example: 

819 if trim_doctest_flags: 

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

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

822 current_example.append(line) 

823 

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

825 in_code_block = not in_code_block 

826 current_text.append(line) 

827 

828 elif in_code_block: 

829 current_text.append(line) 

830 

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

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

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

834 current_text = [] 

835 in_code_example = True 

836 

837 if trim_doctest_flags: 

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

839 current_example.append(line) 

840 

841 else: 

842 current_text.append(line) 

843 

844 if current_text: 

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

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

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

848 

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

850 return DocstringSectionExamples(sub_sections), new_offset 

851 

852 if warnings: 

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

854 return None, new_offset 

855 

856 

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

858 if admonition_title: 

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

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

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

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

863 # as one would expect. 

864 kind = kind[:-1] 

865 sections.append( 

866 DocstringSectionAdmonition( 

867 kind=kind, 

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

869 title=admonition_title, 

870 ), 

871 ) 

872 elif current and any(current): 

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

874 

875 

876_section_reader = { 

877 DocstringSectionKind.parameters: _read_parameters_section, 

878 DocstringSectionKind.other_parameters: _read_other_parameters_section, 

879 DocstringSectionKind.type_parameters: _read_type_parameters_section, 

880 DocstringSectionKind.deprecated: _read_deprecated_section, 

881 DocstringSectionKind.raises: _read_raises_section, 

882 DocstringSectionKind.warns: _read_warns_section, 

883 DocstringSectionKind.examples: _read_examples_section, 

884 DocstringSectionKind.attributes: _read_attributes_section, 

885 DocstringSectionKind.functions: _read_functions_section, 

886 DocstringSectionKind.classes: _read_classes_section, 

887 DocstringSectionKind.type_aliases: _read_type_aliases_section, 

888 DocstringSectionKind.modules: _read_modules_section, 

889 DocstringSectionKind.returns: _read_returns_section, 

890 DocstringSectionKind.yields: _read_yields_section, 

891 DocstringSectionKind.receives: _read_receives_section, 

892} 

893 

894 

895def parse_numpy( 

896 docstring: Docstring, 

897 *, 

898 ignore_init_summary: bool = False, 

899 trim_doctest_flags: bool = True, 

900 warn_unknown_params: bool = True, 

901 warn_missing_types: bool = True, 

902 warnings: bool = True, 

903 **options: Any, 

904) -> list[DocstringSection]: 

905 """Parse a Numpydoc-style docstring. 

906 

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

908 It then returns this list of sections. 

909 

910 Parameters: 

911 docstring: The docstring to parse. 

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

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

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

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

916 warnings: Whether to log warnings at all. 

917 **options: Additional parsing options. 

918 

919 Returns: 

920 A list of docstring sections. 

921 """ 

922 sections: list[DocstringSection] = [] 

923 current_section = [] 

924 admonition_title = "" 

925 

926 in_code_block = False 

927 lines = docstring.lines 

928 

929 options = { 

930 "trim_doctest_flags": trim_doctest_flags, 

931 "ignore_init_summary": ignore_init_summary, 

932 "warn_unknown_params": warn_unknown_params, 

933 "warn_missing_types": warn_missing_types, 

934 "warnings": warnings, 

935 **options, 

936 } 

937 

938 ignore_summary = ( 

939 options["ignore_init_summary"] 

940 and docstring.parent is not None 

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

942 and docstring.parent.is_function 

943 and docstring.parent.parent is not None 

944 and docstring.parent.parent.is_class 

945 ) 

946 

947 offset = 2 if ignore_summary else 0 

948 

949 while offset < len(lines): 

950 line_lower = lines[offset].lower() 

951 

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

953 if in_code_block: 

954 # End of code block. 

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

956 in_code_block = False 

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

958 current_section.append(lines[offset]) 

959 

960 # Start of code block. 

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

962 in_code_block = True 

963 current_section.append(lines[offset]) 

964 

965 # Dash lines after empty lines lose their meaning. 

966 elif _is_empty_line(lines[offset]): 

967 current_section.append("") 

968 

969 # End of the docstring, wrap up. 

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

971 current_section.append(lines[offset]) 

972 _append_section(sections, current_section, admonition_title) 

973 admonition_title = "" 

974 current_section = [] 

975 

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

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

978 # Finish reading current section. 

979 _append_section(sections, current_section, admonition_title) 

980 current_section = [] 

981 

982 # Start parsing new (known) section. 

983 if line_lower in _section_kind: 

984 admonition_title = "" 

985 reader = _section_reader[_section_kind[line_lower]] 

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

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

988 sections.append(section) 

989 

990 # Start parsing admonition. 

991 else: 

992 admonition_title = lines[offset] 

993 offset += 1 # Skip next dash line. 

994 

995 # Regular line. 

996 else: 

997 current_section.append(lines[offset]) 

998 

999 offset += 1 

1000 

1001 # Finish current section. 

1002 _append_section(sections, current_section, admonition_title) 

1003 

1004 return sections