Coverage for tests / test_docstrings / test_sphinx.py: 100.00%

443 statements  

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

1"""Tests for the [Sphinx-style parser][griffe.docstrings.sphinx].""" 

2 

3from __future__ import annotations 

4 

5import inspect 

6from typing import TYPE_CHECKING, Any 

7 

8import pytest 

9 

10from griffe import ( 

11 Attribute, 

12 Class, 

13 DocstringAttribute, 

14 DocstringParameter, 

15 DocstringRaise, 

16 DocstringReturn, 

17 DocstringSectionKind, 

18 Expr, 

19 ExprAttribute, 

20 ExprBinOp, 

21 ExprName, 

22 ExprSubscript, 

23 ExprTuple, 

24 Function, 

25 Module, 

26 Parameter, 

27 Parameters, 

28) 

29 

30if TYPE_CHECKING: 

31 from tests.test_docstrings.helpers import ParserType 

32 

33SOME_NAME = "foo" 

34SOME_TEXT = "descriptive test text" 

35SOME_EXTRA_TEXT = "more test text" 

36SOME_EXCEPTION_NAME = "SomeException" 

37SOME_OTHER_EXCEPTION_NAME = "SomeOtherException" 

38 

39 

40@pytest.mark.parametrize( 

41 "docstring", 

42 [ 

43 "One line docstring description", 

44 """ 

45 Multiple line docstring description. 

46 

47 With more text. 

48 """, 

49 ], 

50) 

51def test_parse__description_only_docstring__single_markdown_section(parse_sphinx: ParserType, docstring: str) -> None: 

52 """Parse a single or multiline docstring. 

53 

54 Parameters: 

55 parse_sphinx: Fixture parser. 

56 docstring: A parametrized docstring. 

57 """ 

58 sections, warnings = parse_sphinx(docstring) 

59 

60 assert len(sections) == 1 

61 assert sections[0].kind is DocstringSectionKind.text 

62 assert sections[0].value == inspect.cleandoc(docstring) 

63 assert not warnings 

64 

65 

66def test_parse__no_description__single_markdown_section(parse_sphinx: ParserType) -> None: 

67 """Parse an empty docstring. 

68 

69 Parameters: 

70 parse_sphinx: Fixture parser. 

71 """ 

72 sections, warnings = parse_sphinx("") 

73 

74 assert len(sections) == 1 

75 assert sections[0].kind is DocstringSectionKind.text 

76 assert sections[0].value == "" 

77 assert not warnings 

78 

79 

80def test_parse__multiple_blank_lines_before_description__single_markdown_section(parse_sphinx: ParserType) -> None: 

81 """Parse a docstring with initial blank lines. 

82 

83 Parameters: 

84 parse_sphinx: Fixture parser. 

85 """ 

86 sections, warnings = parse_sphinx( 

87 """ 

88 

89 

90 Now text""", 

91 ) 

92 

93 assert len(sections) == 1 

94 assert sections[0].kind is DocstringSectionKind.text 

95 assert sections[0].value == "Now text" 

96 assert not warnings 

97 

98 

99def test_parse__param_field__param_section(parse_sphinx: ParserType) -> None: 

100 """Parse a parameter section. 

101 

102 Parameters: 

103 parse_sphinx: Fixture parser. 

104 """ 

105 sections, _ = parse_sphinx( 

106 f""" 

107 Docstring with one line param. 

108 

109 :param {SOME_NAME}: {SOME_TEXT} 

110 """, 

111 ) 

112 assert len(sections) == 2 

113 assert sections[1].kind is DocstringSectionKind.parameters 

114 actual = sections[1].value[0] 

115 expected = DocstringParameter(SOME_NAME, description=SOME_TEXT) 

116 assert isinstance(actual, type(expected)) 

117 assert actual.as_dict() == expected.as_dict() 

118 

119 

120def test_parse__only_param_field__empty_markdown(parse_sphinx: ParserType) -> None: 

121 """Parse only a parameter section. 

122 

123 Parameters: 

124 parse_sphinx: Fixture parser. 

125 """ 

126 sections, _ = parse_sphinx(":param foo: text") 

127 assert len(sections) == 2 

128 assert sections[0].kind is DocstringSectionKind.text 

129 assert sections[0].value == "" 

130 

131 

132@pytest.mark.parametrize( 

133 "param_directive_name", 

134 [ 

135 "param", 

136 "parameter", 

137 "arg", 

138 "arguments", 

139 "key", 

140 "keyword", 

141 ], 

142) 

143def test_parse__all_param_names__param_section(parse_sphinx: ParserType, param_directive_name: str) -> None: 

144 """Parse all parameters directives. 

145 

146 Parameters: 

147 parse_sphinx: Fixture parser. 

148 param_directive_name: A parametrized directive name. 

149 """ 

150 sections, _ = parse_sphinx( 

151 f""" 

152 Docstring with one line param. 

153 

154 :{param_directive_name} {SOME_NAME}: {SOME_TEXT} 

155 """, 

156 ) 

157 assert len(sections) == 2 

158 assert sections[1].kind is DocstringSectionKind.parameters 

159 actual = sections[1].value[0] 

160 expected = DocstringParameter(SOME_NAME, description=SOME_TEXT) 

161 assert isinstance(actual, type(expected)) 

162 assert actual.as_dict() == expected.as_dict() 

163 

164 

165@pytest.mark.parametrize( 

166 "docstring", 

167 [ 

168 f""" 

169 Docstring with param with continuation, no indent. 

170 

171 :param {SOME_NAME}: {SOME_TEXT} 

172 {SOME_EXTRA_TEXT} 

173 """, 

174 f""" 

175 Docstring with param with continuation, with indent. 

176 

177 :param {SOME_NAME}: {SOME_TEXT} 

178 {SOME_EXTRA_TEXT} 

179 """, 

180 ], 

181) 

182def test_parse__param_field_multi_line__param_section(parse_sphinx: ParserType, docstring: str) -> None: 

183 """Parse multiline directives. 

184 

185 Parameters: 

186 parse_sphinx: Fixture parser. 

187 docstring: A parametrized docstring. 

188 """ 

189 sections, _ = parse_sphinx(docstring) 

190 assert len(sections) == 2 

191 assert sections[1].kind is DocstringSectionKind.parameters 

192 actual = sections[1].value[0] 

193 expected = DocstringParameter(SOME_NAME, description=f"{SOME_TEXT}\n{SOME_EXTRA_TEXT}") 

194 assert isinstance(actual, type(expected)) 

195 assert actual.as_dict() == expected.as_dict() 

196 

197 

198def test_parse__param_field_for_function__param_section_with_kind(parse_sphinx: ParserType) -> None: 

199 """Parse parameters. 

200 

201 Parameters: 

202 parse_sphinx: Fixture parser. 

203 """ 

204 docstring = f""" 

205 Docstring with line continuation. 

206 

207 :param foo: {SOME_TEXT} 

208 """ 

209 

210 sections, _ = parse_sphinx(docstring) 

211 assert len(sections) == 2 

212 assert sections[1].kind is DocstringSectionKind.parameters 

213 actual = sections[1].value[0] 

214 expected = DocstringParameter(SOME_NAME, description=SOME_TEXT) 

215 assert isinstance(actual, type(expected)) 

216 assert actual.as_dict() == expected.as_dict() 

217 

218 

219def test_parse__param_field_docs_type__param_section_with_type(parse_sphinx: ParserType) -> None: 

220 """Parse parameters with types. 

221 

222 Parameters: 

223 parse_sphinx: Fixture parser. 

224 """ 

225 docstring = f""" 

226 Docstring with line continuation. 

227 

228 :param str foo: {SOME_TEXT} 

229 """ 

230 

231 sections, _ = parse_sphinx(docstring) 

232 assert len(sections) == 2 

233 assert sections[1].kind is DocstringSectionKind.parameters 

234 actual = sections[1].value[0] 

235 expected = DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT) 

236 assert isinstance(actual, type(expected)) 

237 assert actual.as_dict() == expected.as_dict() 

238 

239 

240@pytest.mark.parametrize("type_", ["str", "int"]) 

241def test_parse__param_field_type_field__param_section_with_type(parse_sphinx: ParserType, type_: str) -> None: 

242 """Parse parameters with separated types. 

243 

244 Parameters: 

245 parse_sphinx: Fixture parser. 

246 type_: The type to use in the type directive. 

247 """ 

248 docstring = f""" 

249 Docstring with line continuation. 

250 

251 :param {SOME_NAME}: {SOME_TEXT} 

252 :type {SOME_NAME}: {type_} 

253 """ 

254 

255 sections, _ = parse_sphinx(docstring) 

256 assert len(sections) == 2 

257 assert sections[1].kind is DocstringSectionKind.parameters 

258 actual = sections[1].value[0] 

259 expected = DocstringParameter(SOME_NAME, annotation=f"{type_}", description=SOME_TEXT) 

260 assert isinstance(actual, type(expected)) 

261 assert actual.as_dict() == expected.as_dict() 

262 

263 

264@pytest.mark.parametrize("type_", ["str", "int"]) 

265def test_parse__param_field_type_field__param_section_with_type_with_parent( 

266 parse_sphinx: ParserType, 

267 type_: str, 

268) -> None: 

269 """Parse parameters with separated types. 

270 

271 Parameters: 

272 parse_sphinx: Fixture parser. 

273 type_: The type to use in the type directive. 

274 """ 

275 docstring = f""" 

276 Docstring with line continuation. 

277 

278 :param {SOME_NAME}: {SOME_TEXT} 

279 :type {SOME_NAME}: {type_} 

280 """ 

281 parent_fn = Function("func", parameters=Parameters(Parameter(SOME_NAME))) 

282 sections, _ = parse_sphinx(docstring, parent=parent_fn) 

283 assert len(sections) == 2 

284 assert sections[1].kind is DocstringSectionKind.parameters 

285 actual = sections[1].value[0] 

286 expected_annotation = ExprName(name=f"{type_}") 

287 expected = DocstringParameter(SOME_NAME, annotation=expected_annotation, description=SOME_TEXT) 

288 assert isinstance(actual, type(expected)) 

289 assert actual.as_dict() == expected.as_dict() 

290 assert isinstance(actual.annotation, type(expected.annotation)) 

291 assert isinstance(actual.annotation, ExprName) 

292 assert isinstance(actual.annotation, Expr) 

293 assert actual.annotation.as_dict() == expected_annotation.as_dict() 

294 

295 

296def test_parse__param_field_type_field_first__param_section_with_type(parse_sphinx: ParserType) -> None: 

297 """Parse parameters with separated types first. 

298 

299 Parameters: 

300 parse_sphinx: Fixture parser. 

301 """ 

302 docstring = f""" 

303 Docstring with line continuation. 

304 

305 :type foo: str 

306 :param foo: {SOME_TEXT} 

307 """ 

308 

309 sections, _ = parse_sphinx(docstring) 

310 assert len(sections) == 2 

311 assert sections[1].kind is DocstringSectionKind.parameters 

312 actual = sections[1].value[0] 

313 expected = DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT) 

314 assert isinstance(actual, type(expected)) 

315 assert actual.as_dict() == expected.as_dict() 

316 

317 

318def test_parse__param_field_type_field_first__param_section_with_type_with_parent(parse_sphinx: ParserType) -> None: 

319 """Parse parameters with separated types first. 

320 

321 Parameters: 

322 parse_sphinx: Fixture parser. 

323 """ 

324 docstring = f""" 

325 Docstring with line continuation. 

326 

327 :type {SOME_NAME}: str 

328 :param {SOME_NAME}: {SOME_TEXT} 

329 """ 

330 parent_fn = Function("func", parameters=Parameters(Parameter(SOME_NAME))) 

331 sections, _ = parse_sphinx(docstring, parent=parent_fn) 

332 assert len(sections) == 2 

333 assert sections[1].kind is DocstringSectionKind.parameters 

334 actual = sections[1].value[0] 

335 expected_annotation = ExprName("str", parent=Class("C")) 

336 expected = DocstringParameter(SOME_NAME, annotation=expected_annotation, description=SOME_TEXT) 

337 assert isinstance(actual, type(expected)) 

338 assert actual.as_dict() == expected.as_dict() 

339 assert isinstance(actual.annotation, type(expected.annotation)) 

340 assert isinstance(actual.annotation, ExprName) 

341 assert isinstance(actual.annotation, Expr) 

342 assert actual.annotation.as_dict() == expected_annotation.as_dict() 

343 

344 

345@pytest.mark.parametrize("union", ["str or None", "None or str", "str or int", "str or int or float"]) 

346def test_parse__param_field_type_field_or_none__param_section_with_optional( 

347 parse_sphinx: ParserType, 

348 union: str, 

349) -> None: 

350 """Parse parameters with separated union types. 

351 

352 Parameters: 

353 parse_sphinx: Fixture parser. 

354 union: A parametrized union type. 

355 """ 

356 docstring = f""" 

357 Docstring with line continuation. 

358 

359 :param foo: {SOME_TEXT} 

360 :type foo: {union} 

361 """ 

362 

363 sections, _ = parse_sphinx(docstring) 

364 assert len(sections) == 2 

365 assert sections[1].kind is DocstringSectionKind.parameters 

366 actual = sections[1].value[0] 

367 expected = DocstringParameter(SOME_NAME, annotation=union.replace(" or ", " | "), description=SOME_TEXT) 

368 assert isinstance(actual, type(expected)) 

369 assert actual.as_dict() == expected.as_dict() 

370 

371 

372@pytest.mark.parametrize( 

373 ("union", "expected_annotation"), 

374 [ 

375 ("str or None", ExprBinOp(ExprName("str"), "|", "None")), 

376 ("None or str", ExprBinOp("None", "|", ExprName("str"))), 

377 ("str or int", ExprBinOp(ExprName("str"), "|", ExprName("int"))), 

378 ("str or int or float", ExprBinOp(ExprBinOp(ExprName("str"), "|", ExprName("int")), "|", ExprName("float"))), 

379 ], 

380) 

381def test_parse__param_field_type_field_or_none__param_section_with_optional_with_parent( 

382 parse_sphinx: ParserType, 

383 union: str, 

384 expected_annotation: Expr, 

385) -> None: 

386 """Parse parameters with separated union types. 

387 

388 Parameters: 

389 parse_sphinx: Fixture parser. 

390 union: A parametrized union type. 

391 expected_annotation: The expected annotation as an expression 

392 """ 

393 docstring = f""" 

394 Docstring with line continuation. 

395 

396 :param {SOME_NAME}: {SOME_TEXT} 

397 :type {SOME_NAME}: {union} 

398 """ 

399 

400 parent_fn = Function("func", parameters=Parameters(Parameter(SOME_NAME))) 

401 sections, _ = parse_sphinx(docstring, parent=parent_fn) 

402 assert len(sections) == 2 

403 assert sections[1].kind is DocstringSectionKind.parameters 

404 actual = sections[1].value[0] 

405 expected = DocstringParameter(SOME_NAME, annotation=expected_annotation, description=SOME_TEXT) 

406 assert isinstance(actual, type(expected)) 

407 assert actual.as_dict() == expected.as_dict() 

408 assert isinstance(actual.annotation, type(expected.annotation)) 

409 assert isinstance(actual.annotation, Expr) 

410 assert actual.annotation.as_dict() == expected_annotation.as_dict() 

411 

412 

413def test_parse__param_field_annotate_type__param_section_with_type(parse_sphinx: ParserType) -> None: 

414 """Parse a simple docstring. 

415 

416 Parameters: 

417 parse_sphinx: Fixture parser. 

418 """ 

419 docstring = f""" 

420 Docstring with line continuation. 

421 

422 :param foo: {SOME_TEXT} 

423 """ 

424 

425 sections, warnings = parse_sphinx( 

426 docstring, 

427 parent=Function("func", parameters=Parameters(Parameter("foo", annotation="str", kind=None))), 

428 ) 

429 assert len(sections) == 2 

430 assert sections[1].kind is DocstringSectionKind.parameters 

431 actual = sections[1].value[0] 

432 expected = DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT) 

433 assert isinstance(actual, type(expected)) 

434 assert actual.as_dict() == expected.as_dict() 

435 assert not warnings 

436 

437 

438def test_parse__param_field_no_matching_param__result_from_docstring(parse_sphinx: ParserType) -> None: 

439 """Parse a simple docstring. 

440 

441 Parameters: 

442 parse_sphinx: Fixture parser. 

443 """ 

444 docstring = f""" 

445 Docstring with line continuation. 

446 

447 :param other: {SOME_TEXT} 

448 """ 

449 

450 sections, _ = parse_sphinx(docstring) 

451 assert len(sections) == 2 

452 assert sections[1].kind is DocstringSectionKind.parameters 

453 actual = sections[1].value[0] 

454 expected = DocstringParameter("other", description=SOME_TEXT) 

455 assert isinstance(actual, type(expected)) 

456 assert actual.as_dict() == expected.as_dict() 

457 

458 

459def test_parse__param_field_with_default__result_from_docstring(parse_sphinx: ParserType) -> None: 

460 """Parse a simple docstring. 

461 

462 Parameters: 

463 parse_sphinx: Fixture parser. 

464 """ 

465 docstring = f""" 

466 Docstring with line continuation. 

467 

468 :param foo: {SOME_TEXT} 

469 """ 

470 

471 sections, warnings = parse_sphinx( 

472 docstring, 

473 parent=Function("func", parameters=Parameters(Parameter("foo", kind=None, default=repr("")))), 

474 ) 

475 assert len(sections) == 2 

476 assert sections[1].kind is DocstringSectionKind.parameters 

477 actual = sections[1].value[0] 

478 expected = DocstringParameter("foo", description=SOME_TEXT, value=repr("")) 

479 assert isinstance(actual, type(expected)) 

480 assert actual.as_dict() == expected.as_dict() 

481 assert not warnings 

482 

483 

484def test_parse__param_field_no_matching_param__error_message(parse_sphinx: ParserType) -> None: 

485 """Parse a simple docstring. 

486 

487 Parameters: 

488 parse_sphinx: Fixture parser. 

489 """ 

490 docstring = f""" 

491 Docstring with line continuation. 

492 

493 :param other: {SOME_TEXT} 

494 """ 

495 

496 _, warnings = parse_sphinx(docstring) 

497 assert "No matching parameter for 'other'" in warnings[0] 

498 

499 

500def test_parse__invalid_param_field_only_initial_marker__error_message(parse_sphinx: ParserType) -> None: 

501 """Parse a simple docstring. 

502 

503 Parameters: 

504 parse_sphinx: Fixture parser. 

505 """ 

506 docstring = f""" 

507 Docstring with line continuation. 

508 

509 :param foo {SOME_TEXT} 

510 """ 

511 

512 _, warnings = parse_sphinx(docstring) 

513 assert "Failed to get ':directive: value' pair" in warnings[0] 

514 

515 

516def test_parse__invalid_param_field_wrong_part_count__error_message(parse_sphinx: ParserType) -> None: 

517 """Parse a simple docstring. 

518 

519 Parameters: 

520 parse_sphinx: Fixture parser. 

521 """ 

522 docstring = f""" 

523 Docstring with line continuation. 

524 

525 :param: {SOME_TEXT} 

526 """ 

527 

528 _, warnings = parse_sphinx(docstring) 

529 assert "Failed to parse field directive" in warnings[0] 

530 

531 

532def test_parse__invalid_param_field_wrong_part_count_spaces_4__error_message(parse_sphinx: ParserType) -> None: 

533 """Parse a simple docstring. 

534 

535 Parameters: 

536 parse_sphinx: Fixture parser. 

537 """ 

538 docstring = f""" 

539 Docstring with line continuation. 

540 

541 :param typing.Union[str, int] {SOME_NAME}: {SOME_TEXT} 

542 """ 

543 

544 sections, warnings = parse_sphinx(docstring) 

545 

546 # Assert that the warning is shown 

547 assert "Failed to parse field directive" in warnings[0] 

548 

549 # Assert that the parameter is still collected, but ignores the invalid type 

550 assert len(sections) == 2 

551 assert sections[1].kind is DocstringSectionKind.parameters 

552 actual = sections[1].value[0] 

553 expected = DocstringParameter(SOME_NAME, annotation=None, description=SOME_TEXT) 

554 assert isinstance(actual, type(expected)) 

555 assert actual.as_dict() == expected.as_dict() 

556 

557 

558def test_parse__valid_param_field_part_count_3(parse_sphinx: ParserType) -> None: 

559 """Parse a simple docstring. 

560 

561 Parameters: 

562 parse_sphinx: Fixture parser. 

563 """ 

564 docstring = f""" 

565 Docstring with line continuation. 

566 

567 :param typing.Union[str,int] {SOME_NAME}: {SOME_TEXT} 

568 """ 

569 

570 sections, _ = parse_sphinx(docstring) 

571 assert len(sections) == 2 

572 assert sections[1].kind is DocstringSectionKind.parameters 

573 actual = sections[1].value[0] 

574 expected = DocstringParameter(SOME_NAME, annotation="typing.Union[str,int]", description=SOME_TEXT) 

575 assert isinstance(actual, type(expected)) 

576 assert actual.as_dict() == expected.as_dict() 

577 

578 

579def test_parse__valid_param_field_part_count_3_with_parent(parse_sphinx: ParserType) -> None: 

580 """Parse a simple docstring. 

581 

582 Parameters: 

583 parse_sphinx: Fixture parser. 

584 """ 

585 docstring = f""" 

586 Docstring with line continuation. 

587 

588 :param typing.Union[str,int] {SOME_NAME}: {SOME_TEXT} 

589 """ 

590 

591 parent_fn = Function("func3", parameters=Parameters(Parameter(name=SOME_NAME))) 

592 sections, _ = parse_sphinx(docstring, parent=parent_fn) 

593 assert len(sections) == 2 

594 assert sections[1].kind is DocstringSectionKind.parameters 

595 actual = sections[1].value[0] 

596 typing_expr = ExprName("typing", parent=parent_fn) 

597 expected_annotation = ExprSubscript( 

598 left=ExprAttribute(values=[typing_expr, ExprName("Union", parent=typing_expr)]), 

599 slice=ExprTuple([ExprName("str", parent=parent_fn), ExprName("int", parent=parent_fn)], implicit=True), 

600 ) 

601 expected = DocstringParameter(SOME_NAME, annotation=expected_annotation, description=SOME_TEXT) 

602 assert isinstance(actual, type(expected)) 

603 assert actual.as_dict() == expected.as_dict() 

604 

605 

606def test_parse__param_twice__error_message(parse_sphinx: ParserType) -> None: 

607 """Parse a simple docstring. 

608 

609 Parameters: 

610 parse_sphinx: Fixture parser. 

611 """ 

612 docstring = f""" 

613 Docstring with line continuation. 

614 

615 :param foo: {SOME_TEXT} 

616 :param foo: {SOME_TEXT} again 

617 """ 

618 

619 _, warnings = parse_sphinx( 

620 docstring, 

621 parent=Function("func", parameters=Parameters(Parameter("foo", kind=None))), 

622 ) 

623 assert "Duplicate parameter entry for 'foo'" in warnings[0] 

624 

625 

626def test_parse__param_type_twice_doc__error_message(parse_sphinx: ParserType) -> None: 

627 """Parse a simple docstring. 

628 

629 Parameters: 

630 parse_sphinx: Fixture parser. 

631 """ 

632 docstring = f""" 

633 Docstring with line continuation. 

634 

635 :param str foo: {SOME_TEXT} 

636 :type foo: str 

637 """ 

638 

639 _, warnings = parse_sphinx( 

640 docstring, 

641 parent=Function("func", parameters=Parameters(Parameter("foo", kind=None))), 

642 ) 

643 assert "Duplicate parameter information for 'foo'" in warnings[0] 

644 

645 

646def test_parse__param_type_twice_type_directive_first__error_message(parse_sphinx: ParserType) -> None: 

647 """Parse a simple docstring. 

648 

649 Parameters: 

650 parse_sphinx: Fixture parser. 

651 """ 

652 docstring = f""" 

653 Docstring with line continuation. 

654 

655 :type foo: str 

656 :param str foo: {SOME_TEXT} 

657 """ 

658 

659 _, warnings = parse_sphinx( 

660 docstring, 

661 parent=Function("func", parameters=Parameters(Parameter("foo", kind=None))), 

662 ) 

663 assert "Duplicate parameter information for 'foo'" in warnings[0] 

664 

665 

666def test_parse__param_type_twice_annotated__error_message(parse_sphinx: ParserType) -> None: 

667 """Parse a simple docstring. 

668 

669 Parameters: 

670 parse_sphinx: Fixture parser. 

671 """ 

672 docstring = f""" 

673 Docstring with line continuation. 

674 

675 :param str foo: {SOME_TEXT} 

676 :type foo: str 

677 """ 

678 

679 _, warnings = parse_sphinx( 

680 docstring, 

681 parent=Function("func", parameters=Parameters(Parameter("foo", annotation="str", kind=None))), 

682 ) 

683 assert "Duplicate parameter information for 'foo'" in warnings[0] 

684 

685 

686def test_warn_about_unknown_parameters(parse_sphinx: ParserType) -> None: 

687 """Warn about unknown parameters in "Parameters" sections. 

688 

689 Parameters: 

690 parse_sphinx: Fixture parser. 

691 """ 

692 docstring = """ 

693 

694 :param str a: {SOME_TEXT} 

695 """ 

696 

697 _, warnings = parse_sphinx( 

698 docstring, 

699 parent=Function( 

700 "func", 

701 parameters=Parameters( 

702 Parameter("b"), 

703 ), 

704 ), 

705 ) 

706 assert len(warnings) == 1 

707 assert "Parameter 'a' does not appear in the function signature" in warnings[0] 

708 

709 

710def test_parse__param_type_no_type__error_message(parse_sphinx: ParserType) -> None: 

711 """Parse a simple docstring. 

712 

713 Parameters: 

714 parse_sphinx: Fixture parser. 

715 """ 

716 docstring = f""" 

717 Docstring with line continuation. 

718 

719 :param str foo: {SOME_TEXT} 

720 :type str 

721 """ 

722 

723 _, warnings = parse_sphinx( 

724 docstring, 

725 parent=Function("func", parameters=Parameters(Parameter("foo", annotation="str", kind=None))), 

726 ) 

727 assert "Failed to get ':directive: value' pair from" in warnings[0] 

728 

729 

730def test_parse__param_type_no_name__error_message(parse_sphinx: ParserType) -> None: 

731 """Parse a simple docstring. 

732 

733 Parameters: 

734 parse_sphinx: Fixture parser. 

735 """ 

736 docstring = f""" 

737 Docstring with line continuation. 

738 

739 :param str foo: {SOME_TEXT} 

740 :type: str 

741 """ 

742 

743 _, warnings = parse_sphinx( 

744 docstring, 

745 parent=Function("func", parameters=Parameters(Parameter("foo", annotation="str", kind=None))), 

746 ) 

747 assert "Failed to get parameter name from" in warnings[0] 

748 

749 

750def test_parse__param_multiline(parse_sphinx: ParserType) -> None: 

751 """Parse multiline parameter descriptions. 

752 

753 Parameters: 

754 parse_sphinx: Fixture parser. 

755 """ 

756 docstring = """Do something. 

757 

758 :param foo: This is a docstring that is long enough to run onto a second line, 

759 because it is quite long. 

760 

761 A second paragraph is also required. 

762 :param bar: This is an example that is quite long, and also requires bullet 

763 points to be clear about intent: 

764 

765 * First thing 

766 

767 ``` 

768 a code block 

769 ``` 

770 

771 * Second thing 

772 * Third thing 

773 """ 

774 

775 sections, _ = parse_sphinx(docstring) 

776 param_section = sections[1] 

777 

778 param_foo = param_section.value[0] 

779 assert param_foo.description == ( 

780 "This is a docstring that is long enough to run onto a second line,\n" 

781 "because it is quite long.\n\n" 

782 "A second paragraph is also required." 

783 ) 

784 

785 param_bar = param_section.value[1] 

786 assert param_bar.description == ( 

787 "This is an example that is quite long, and also requires bullet\n" 

788 "points to be clear about intent:\n\n" 

789 "* First thing\n\n" 

790 " ```\n" 

791 " a code block\n" 

792 " ```\n\n" 

793 "* Second thing\n" 

794 "* Third thing" 

795 ) 

796 

797 

798@pytest.mark.parametrize( 

799 "docstring", 

800 [ 

801 f""" 

802 Docstring with param with continuation, no indent. 

803 

804 :var {SOME_NAME}: {SOME_TEXT} 

805 {SOME_EXTRA_TEXT} 

806 """, 

807 f""" 

808 Docstring with param with continuation, with indent. 

809 

810 :var {SOME_NAME}: {SOME_TEXT} 

811 {SOME_EXTRA_TEXT} 

812 """, 

813 ], 

814) 

815def test_parse__attribute_field_multi_line__param_section(parse_sphinx: ParserType, docstring: str) -> None: 

816 """Parse multiline attributes. 

817 

818 Parameters: 

819 parse_sphinx: Fixture parser. 

820 docstring: A parametrized docstring. 

821 """ 

822 sections, warnings = parse_sphinx(docstring) 

823 assert len(sections) == 2 

824 assert sections[1].kind is DocstringSectionKind.attributes 

825 actual = sections[1].value[0] 

826 expected = DocstringAttribute(SOME_NAME, description=f"{SOME_TEXT}\n{SOME_EXTRA_TEXT}") 

827 assert isinstance(actual, type(expected)) 

828 assert actual.as_dict() == expected.as_dict() 

829 assert not warnings 

830 

831 

832@pytest.mark.parametrize( 

833 "attribute_directive_name", 

834 [ 

835 "var", 

836 "ivar", 

837 "cvar", 

838 ], 

839) 

840def test_parse__all_attribute_names__param_section(parse_sphinx: ParserType, attribute_directive_name: str) -> None: 

841 """Parse all attributes directives. 

842 

843 Parameters: 

844 parse_sphinx: Fixture parser. 

845 attribute_directive_name: A parametrized directive name. 

846 """ 

847 sections, warnings = parse_sphinx( 

848 f""" 

849 Docstring with one line attribute. 

850 

851 :{attribute_directive_name} {SOME_NAME}: {SOME_TEXT} 

852 """, 

853 ) 

854 assert len(sections) == 2 

855 assert sections[1].kind is DocstringSectionKind.attributes 

856 actual = sections[1].value[0] 

857 expected = DocstringAttribute(SOME_NAME, description=SOME_TEXT) 

858 assert isinstance(actual, type(expected)) 

859 assert actual.as_dict() == expected.as_dict() 

860 assert not warnings 

861 

862 

863def test_parse__class_attributes__attributes_section(parse_sphinx: ParserType) -> None: 

864 """Parse class attributes. 

865 

866 Parameters: 

867 parse_sphinx: Fixture parser. 

868 """ 

869 docstring = f""" 

870 Class docstring with attributes 

871 

872 :var foo: {SOME_TEXT} 

873 """ 

874 

875 sections, _ = parse_sphinx(docstring, parent=Class("klass")) 

876 assert len(sections) == 2 

877 assert sections[1].kind is DocstringSectionKind.attributes 

878 actual = sections[1].value[0] 

879 expected = DocstringAttribute(SOME_NAME, description=SOME_TEXT) 

880 assert isinstance(actual, type(expected)) 

881 assert actual.as_dict() == expected.as_dict() 

882 

883 

884def test_parse__class_attributes_with_type__annotation_in_attributes_section(parse_sphinx: ParserType) -> None: 

885 """Parse typed class attributes. 

886 

887 Parameters: 

888 parse_sphinx: Fixture parser. 

889 """ 

890 docstring = f""" 

891 Class docstring with attributes 

892 

893 :vartype foo: str 

894 :var foo: {SOME_TEXT} 

895 """ 

896 

897 sections, _ = parse_sphinx(docstring, parent=Class("klass")) 

898 assert len(sections) == 2 

899 assert sections[1].kind is DocstringSectionKind.attributes 

900 actual = sections[1].value[0] 

901 expected = DocstringAttribute(SOME_NAME, annotation="str", description=SOME_TEXT) 

902 assert isinstance(actual, type(expected)) 

903 assert actual.as_dict() == expected.as_dict() 

904 

905 

906def test_parse__attribute_invalid_directive___error(parse_sphinx: ParserType) -> None: 

907 """Warn on invalid attribute directive. 

908 

909 Parameters: 

910 parse_sphinx: Fixture parser. 

911 """ 

912 docstring = f""" 

913 Class docstring with attributes 

914 

915 :var {SOME_TEXT} 

916 """ 

917 

918 _, warnings = parse_sphinx(docstring) 

919 assert "Failed to get ':directive: value' pair from" in warnings[0] 

920 

921 

922def test_parse__attribute_no_name__error(parse_sphinx: ParserType) -> None: 

923 """Warn on invalid attribute directive. 

924 

925 Parameters: 

926 parse_sphinx: Fixture parser. 

927 """ 

928 docstring = f""" 

929 Class docstring with attributes 

930 

931 :var: {SOME_TEXT} 

932 """ 

933 

934 _, warnings = parse_sphinx(docstring) 

935 assert "Failed to parse field directive from" in warnings[0] 

936 

937 

938def test_parse__attribute_duplicate__error(parse_sphinx: ParserType) -> None: 

939 """Warn on duplicate attribute directive. 

940 

941 Parameters: 

942 parse_sphinx: Fixture parser. 

943 """ 

944 docstring = f""" 

945 Class docstring with attributes 

946 

947 :var foo: {SOME_TEXT} 

948 :var foo: {SOME_TEXT} 

949 """ 

950 

951 _, warnings = parse_sphinx(docstring) 

952 assert "Duplicate attribute entry for 'foo'" in warnings[0] 

953 

954 

955def test_parse__class_attributes_type_invalid__error(parse_sphinx: ParserType) -> None: 

956 """Warn on invalid attribute type directive. 

957 

958 Parameters: 

959 parse_sphinx: Fixture parser. 

960 """ 

961 docstring = f""" 

962 Class docstring with attributes 

963 

964 :vartype str 

965 :var foo: {SOME_TEXT} 

966 """ 

967 

968 _, warnings = parse_sphinx(docstring) 

969 assert "Failed to get ':directive: value' pair from " in warnings[0] 

970 

971 

972def test_parse__class_attributes_type_no_name__error(parse_sphinx: ParserType) -> None: 

973 """Warn on invalid attribute directive. 

974 

975 Parameters: 

976 parse_sphinx: Fixture parser. 

977 """ 

978 docstring = f""" 

979 Class docstring with attributes 

980 

981 :vartype: str 

982 :var foo: {SOME_TEXT} 

983 """ 

984 

985 _, warnings = parse_sphinx(docstring) 

986 assert "Failed to get attribute name from" in warnings[0] 

987 

988 

989def test_parse__return_directive__return_section_no_type(parse_sphinx: ParserType) -> None: 

990 """Parse return directives. 

991 

992 Parameters: 

993 parse_sphinx: Fixture parser. 

994 """ 

995 docstring = f""" 

996 Function with only return directive 

997 

998 :return: {SOME_TEXT} 

999 """ 

1000 

1001 sections, _ = parse_sphinx(docstring) 

1002 assert len(sections) == 2 

1003 assert sections[1].kind is DocstringSectionKind.returns 

1004 actual = sections[1].value[0] 

1005 expected = DocstringReturn(name="", annotation=None, description=SOME_TEXT) 

1006 assert isinstance(actual, type(expected)) 

1007 assert actual.as_dict() == expected.as_dict() 

1008 

1009 

1010def test_parse__return_directive_rtype__return_section_with_type(parse_sphinx: ParserType) -> None: 

1011 """Parse typed return directives. 

1012 

1013 Parameters: 

1014 parse_sphinx: Fixture parser. 

1015 """ 

1016 docstring = f""" 

1017 Function with only return & rtype directive 

1018 

1019 :return: {SOME_TEXT} 

1020 :rtype: str 

1021 """ 

1022 

1023 sections, _ = parse_sphinx(docstring) 

1024 assert len(sections) == 2 

1025 assert sections[1].kind is DocstringSectionKind.returns 

1026 actual = sections[1].value[0] 

1027 expected = DocstringReturn(name="", annotation="str", description=SOME_TEXT) 

1028 assert isinstance(actual, type(expected)) 

1029 assert actual.as_dict() == expected.as_dict() 

1030 

1031 

1032def test_parse__return_directive_rtype_first__return_section_with_type(parse_sphinx: ParserType) -> None: 

1033 """Parse typed-first return directives. 

1034 

1035 Parameters: 

1036 parse_sphinx: Fixture parser. 

1037 """ 

1038 docstring = f""" 

1039 Function with only return & rtype directive 

1040 

1041 :rtype: str 

1042 :return: {SOME_TEXT} 

1043 """ 

1044 

1045 sections, _ = parse_sphinx(docstring) 

1046 assert len(sections) == 2 

1047 assert sections[1].kind is DocstringSectionKind.returns 

1048 actual = sections[1].value[0] 

1049 expected = DocstringReturn(name="", annotation="str", description=SOME_TEXT) 

1050 assert isinstance(actual, type(expected)) 

1051 assert actual.as_dict() == expected.as_dict() 

1052 

1053 

1054def test_parse__return_directive_annotation__return_section_with_type(parse_sphinx: ParserType) -> None: 

1055 """Parse return directives with return annotation. 

1056 

1057 Parameters: 

1058 parse_sphinx: Fixture parser. 

1059 """ 

1060 docstring = f""" 

1061 Function with return directive, rtype directive, & annotation 

1062 

1063 :return: {SOME_TEXT} 

1064 """ 

1065 

1066 sections, _ = parse_sphinx(docstring, parent=Function("func", returns="str")) 

1067 assert len(sections) == 2 

1068 assert sections[1].kind is DocstringSectionKind.returns 

1069 actual = sections[1].value[0] 

1070 expected = DocstringReturn(name="", annotation="str", description=SOME_TEXT) 

1071 assert isinstance(actual, type(expected)) 

1072 assert actual.as_dict() == expected.as_dict() 

1073 

1074 

1075def test_parse__return_directive_annotation__prefer_return_directive(parse_sphinx: ParserType) -> None: 

1076 """Prefer docstring type over return annotation. 

1077 

1078 Parameters: 

1079 parse_sphinx: Fixture parser. 

1080 """ 

1081 docstring = f""" 

1082 Function with return directive, rtype directive, & annotation 

1083 

1084 :return: {SOME_TEXT} 

1085 :rtype: str 

1086 """ 

1087 

1088 sections, _ = parse_sphinx(docstring, parent=Function("func", returns="int")) 

1089 assert len(sections) == 2 

1090 assert sections[1].kind is DocstringSectionKind.returns 

1091 actual = sections[1].value[0] 

1092 expected = DocstringReturn(name="", annotation="str", description=SOME_TEXT) 

1093 assert isinstance(actual, type(expected)) 

1094 assert actual.as_dict() == expected.as_dict() 

1095 

1096 

1097def test_parse__return_invalid__error(parse_sphinx: ParserType) -> None: 

1098 """Warn on invalid return directive. 

1099 

1100 Parameters: 

1101 parse_sphinx: Fixture parser. 

1102 """ 

1103 docstring = f""" 

1104 Function with only return directive 

1105 

1106 :return {SOME_TEXT} 

1107 """ 

1108 

1109 _, warnings = parse_sphinx(docstring) 

1110 assert "Failed to get ':directive: value' pair from " in warnings[0] 

1111 

1112 

1113def test_parse__rtype_invalid__error(parse_sphinx: ParserType) -> None: 

1114 """Warn on invalid typed return directive. 

1115 

1116 Parameters: 

1117 parse_sphinx: Fixture parser. 

1118 """ 

1119 docstring = """ 

1120 Function with only return directive 

1121 

1122 :rtype str 

1123 """ 

1124 

1125 _, warnings = parse_sphinx(docstring) 

1126 assert "Failed to get ':directive: value' pair from " in warnings[0] 

1127 

1128 

1129def test_parse__raises_directive__exception_section(parse_sphinx: ParserType) -> None: 

1130 """Parse raise directives. 

1131 

1132 Parameters: 

1133 parse_sphinx: Fixture parser. 

1134 """ 

1135 docstring = f""" 

1136 Function with only return directive 

1137 

1138 :raise SomeException: {SOME_TEXT} 

1139 """ 

1140 

1141 sections, _ = parse_sphinx(docstring) 

1142 assert len(sections) == 2 

1143 assert sections[1].kind is DocstringSectionKind.raises 

1144 actual = sections[1].value[0] 

1145 expected = DocstringRaise(annotation=SOME_EXCEPTION_NAME, description=SOME_TEXT) 

1146 assert isinstance(actual, type(expected)) 

1147 assert actual.as_dict() == expected.as_dict() 

1148 

1149 

1150def test_parse__multiple_raises_directive__exception_section_with_two(parse_sphinx: ParserType) -> None: 

1151 """Parse multiple raise directives. 

1152 

1153 Parameters: 

1154 parse_sphinx: Fixture parser. 

1155 """ 

1156 docstring = f""" 

1157 Function with only return directive 

1158 

1159 :raise SomeException: {SOME_TEXT} 

1160 :raise SomeOtherException: {SOME_TEXT} 

1161 """ 

1162 

1163 sections, _ = parse_sphinx(docstring) 

1164 assert len(sections) == 2 

1165 assert sections[1].kind is DocstringSectionKind.raises 

1166 actual = sections[1].value[0] 

1167 expected = DocstringRaise(annotation=SOME_EXCEPTION_NAME, description=SOME_TEXT) 

1168 assert isinstance(actual, type(expected)) 

1169 assert actual.as_dict() == expected.as_dict() 

1170 actual = sections[1].value[1] 

1171 expected = DocstringRaise(annotation=SOME_OTHER_EXCEPTION_NAME, description=SOME_TEXT) 

1172 assert isinstance(actual, type(expected)) 

1173 assert actual.as_dict() == expected.as_dict() 

1174 

1175 

1176@pytest.mark.parametrize( 

1177 "raise_directive_name", 

1178 [ 

1179 "raises", 

1180 "raise", 

1181 "except", 

1182 "exception", 

1183 ], 

1184) 

1185def test_parse__all_exception_names__param_section(parse_sphinx: ParserType, raise_directive_name: str) -> None: 

1186 """Parse all raise directives. 

1187 

1188 Parameters: 

1189 parse_sphinx: Fixture parser. 

1190 raise_directive_name: A parametrized directive name. 

1191 """ 

1192 sections, _ = parse_sphinx( 

1193 f""" 

1194 Docstring with one line attribute. 

1195 

1196 :{raise_directive_name} {SOME_EXCEPTION_NAME}: {SOME_TEXT} 

1197 """, 

1198 ) 

1199 assert len(sections) == 2 

1200 assert sections[1].kind is DocstringSectionKind.raises 

1201 actual = sections[1].value[0] 

1202 expected = DocstringRaise(annotation=SOME_EXCEPTION_NAME, description=SOME_TEXT) 

1203 assert isinstance(actual, type(expected)) 

1204 assert actual.as_dict() == expected.as_dict() 

1205 

1206 

1207def test_parse__raise_invalid__error(parse_sphinx: ParserType) -> None: 

1208 """Warn on invalid raise directives. 

1209 

1210 Parameters: 

1211 parse_sphinx: Fixture parser. 

1212 """ 

1213 docstring = f""" 

1214 Function with only return directive 

1215 

1216 :raise {SOME_TEXT} 

1217 """ 

1218 

1219 _, warnings = parse_sphinx(docstring) 

1220 assert "Failed to get ':directive: value' pair from " in warnings[0] 

1221 

1222 

1223def test_parse__raise_no_name__error(parse_sphinx: ParserType) -> None: 

1224 """Warn on invalid raise directives. 

1225 

1226 Parameters: 

1227 parse_sphinx: Fixture parser. 

1228 """ 

1229 docstring = f""" 

1230 Function with only return directive 

1231 

1232 :raise: {SOME_TEXT} 

1233 """ 

1234 

1235 _, warnings = parse_sphinx(docstring) 

1236 assert "Failed to parse exception directive from" in warnings[0] 

1237 

1238 

1239def test_parse__module_attributes_section__expected_attributes_section(parse_sphinx: ParserType) -> None: 

1240 """Parse attributes section in modules. 

1241 

1242 Parameters: 

1243 parse_sphinx: Fixture parser. 

1244 """ 

1245 docstring = """ 

1246 Let's describe some attributes. 

1247 

1248 :var A: Alpha. 

1249 :vartype B: bytes 

1250 :var B: Beta. 

1251 :var C: Gamma. 

1252 :var D: Delta. 

1253 :var E: Epsilon. 

1254 :vartype E: float 

1255 """ 

1256 

1257 module = Module("mod", filepath=None) 

1258 module["A"] = Attribute("A", annotation="int", value="0") 

1259 module["B"] = Attribute("B", annotation="str", value=repr("ŧ")) 

1260 module["C"] = Attribute("C", annotation="bool", value="True") 

1261 module["D"] = Attribute("D", annotation=None, value="3.0") 

1262 module["E"] = Attribute("E", annotation=None, value="None") 

1263 sections, warnings = parse_sphinx(docstring, parent=module) 

1264 

1265 attr_section = sections[1] 

1266 assert attr_section.kind is DocstringSectionKind.attributes 

1267 assert len(attr_section.value) == 5 

1268 expected_data: list[dict[str, Any]] = [ 

1269 {"name": "A", "annotation": "int", "description": "Alpha."}, 

1270 {"name": "B", "annotation": "bytes", "description": "Beta."}, 

1271 {"name": "C", "annotation": "bool", "description": "Gamma."}, 

1272 {"name": "D", "annotation": None, "description": "Delta."}, 

1273 {"name": "E", "annotation": "float", "description": "Epsilon."}, 

1274 ] 

1275 for index, expected_kwargs in enumerate(expected_data): 

1276 actual = attr_section.value[index] 

1277 expected = DocstringAttribute(**expected_kwargs) 

1278 assert isinstance(actual, type(expected)) 

1279 assert actual.name == expected.name 

1280 assert actual.as_dict() == expected.as_dict() 

1281 assert not warnings 

1282 

1283 

1284def test_parse__properties_return_type(parse_sphinx: ParserType) -> None: 

1285 """Parse attributes section in modules. 

1286 

1287 Parameters: 

1288 parse_sphinx: Fixture parser. 

1289 """ 

1290 docstring = """ 

1291 Property that returns True for explaining the issue. 

1292 

1293 :return: True 

1294 """ 

1295 prop = Attribute("example", annotation="bool") 

1296 sections, warnings = parse_sphinx(docstring, parent=prop) 

1297 assert not warnings 

1298 assert sections[1].value[0].annotation == "bool" 

1299 

1300 

1301# ============================================================================================= 

1302# Warnings 

1303def test_disabled_warnings(parse_sphinx: ParserType) -> None: 

1304 """Assert warnings are disabled. 

1305 

1306 Parameters: 

1307 parse_sphinx: Fixture parser. 

1308 """ 

1309 docstring = ":param x: X value." 

1310 _, warnings = parse_sphinx(docstring, warnings=True) 

1311 assert warnings 

1312 _, warnings = parse_sphinx(docstring, warnings=False) 

1313 assert not warnings