Coverage for tests/test_parsers/test_docstrings/test_numpy.py: 89.27%

167 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-09 17:28 +0100

1"""Tests for [the `parsers.docstrings.numpy` module][pytkdocs.parsers.docstrings.numpy].""" 

2 

3import inspect 

4from textwrap import dedent 

5from typing import Any, Optional 

6 

7import pytest 

8 

9from pytkdocs.loader import Loader 

10from pytkdocs.parsers.docstrings.base import Section 

11from pytkdocs.parsers.docstrings.numpy import Numpy 

12 

13 

14class DummyObject: 

15 path = "o" 

16 

17 

18def parse( 

19 docstring: str, 

20 signature: Optional[inspect.Signature] = None, 

21 return_type: Any = inspect.Signature.empty, 

22 trim_doctest: bool = False, # noqa: FBT002 

23) -> tuple[list[Section], list[str]]: 

24 """Helper to parse a doctring.""" 

25 parser = Numpy(trim_doctest_flags=trim_doctest) 

26 

27 return parser.parse( 

28 dedent(docstring).strip(), 

29 context={"obj": DummyObject(), "signature": signature, "type": return_type}, 

30 ) 

31 

32 

33def test_simple_docstring() -> None: 

34 """Parse a simple docstring.""" 

35 sections, errors = parse("A simple docstring.") 

36 assert len(sections) == 1 

37 assert not errors 

38 

39 

40def test_multi_line_docstring() -> None: 

41 """Parse a multi-line docstring.""" 

42 sections, errors = parse( 

43 """ 

44 A somewhat longer docstring. 

45 

46 Blablablabla. 

47 """, 

48 ) 

49 assert len(sections) == 1 

50 assert not errors 

51 

52 

53def test_sections_without_signature() -> None: 

54 """Parse a docstring without a signature.""" 

55 # type of return value always required 

56 sections, errors = parse( 

57 """ 

58 Sections without signature. 

59 

60 Parameters 

61 ---------- 

62 void : 

63 SEGFAULT. 

64 niet : 

65 SEGFAULT. 

66 nada : 

67 SEGFAULT. 

68 rien : 

69 SEGFAULT. 

70 

71 Raises 

72 ------ 

73 GlobalError 

74 when nothing works as expected. 

75 

76 Returns 

77 ------- 

78 bool 

79 Itself. 

80 """, 

81 ) 

82 assert len(sections) == 4 

83 assert len(errors) == 4 # missing annotations for params 

84 for error in errors: 

85 assert "param" in error 

86 

87 

88def test_sections_without_description() -> None: 

89 """Parse a docstring without descriptions.""" 

90 # type of return value always required 

91 sections, errors = parse( 

92 """ 

93 Sections without descriptions. 

94 

95 Parameters 

96 ---------- 

97 void : str 

98 niet : str 

99 

100 Raises 

101 ------ 

102 GlobalError 

103 

104 Returns 

105 ------- 

106 bool 

107 """, 

108 ) 

109 

110 # Assert that errors are as expected 

111 assert len(sections) == 4 

112 assert len(errors) == 6 

113 for error in errors[:4]: 

114 assert "param" in error 

115 assert "exception" in errors[4] 

116 assert "return description" in errors[5] 

117 

118 # Assert that no descriptions are ever None (can cause exceptions downstream) 

119 assert sections[1].type is Section.Type.PARAMETERS 

120 for p in sections[1].value: 

121 assert p.description is not None 

122 

123 assert sections[2].type is Section.Type.EXCEPTIONS 

124 for p in sections[2].value: 

125 assert p.description is not None 

126 

127 assert sections[3].type is Section.Type.RETURN 

128 assert sections[3].value.description is not None 

129 

130 

131def test_property_docstring() -> None: 

132 """Parse a property docstring.""" 

133 class_ = Loader().get_object_documentation("tests.fixtures.parsing.docstrings.NotDefinedYet") 

134 prop = class_.attributes[0] 

135 sections, errors = prop.docstring_sections, prop.docstring_errors 

136 assert len(sections) == 2 

137 assert not errors 

138 

139 

140def test_function_without_annotations() -> None: 

141 """Parse a function docstring without signature annotations.""" 

142 

143 def f(x, y): # noqa: ANN202, ANN001 

144 """ 

145 This function has no annotations. 

146 

147 Parameters 

148 ---------- 

149 x: 

150 X value. 

151 y: 

152 Y value. 

153 

154 Returns 

155 ------- 

156 float 

157 Sum X + Y. 

158 """ # noqa: D212, D416 

159 return x + y 

160 

161 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

162 assert len(sections) == 3 

163 assert not errors 

164 

165 

166def test_function_with_annotations() -> None: 

167 """Parse a function docstring with signature annotations.""" 

168 

169 def f(x: int, y: int) -> int: 

170 """ 

171 This function has annotations. 

172 

173 Parameters 

174 ---------- 

175 x: 

176 X value. 

177 y: 

178 Y value. 

179 

180 Returns 

181 ------- 

182 int 

183 Sum X + Y. 

184 """ # noqa: D212, D416 

185 return x + y 

186 

187 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

188 assert len(sections) == 3 

189 assert not errors 

190 

191 

192@pytest.mark.xfail(reason="Possible change in docstring-parser") 

193def test_function_with_examples_trim_doctest() -> None: 

194 """Parse example docstring with trim_doctest_flags option.""" 

195 

196 def f(x: int) -> int: 

197 r""" 

198 Test function. 

199 

200 Example 

201 ------- 

202 We want to skip the following test. 

203 >>> 1 + 1 == 3 # doctest: +SKIP 

204 True 

205 

206 And then a few more examples here: 

207 >>> print("a\n\nb") 

208 a 

209 <BLANKLINE> 

210 b 

211 >>> 1 + 1 == 2 # doctest: +SKIP 

212 >>> print(list(range(1, 100))) # doctest: +ELLIPSIS 

213 [1, 2, ..., 98, 99] 

214 """ # noqa: D416, D212 

215 return x 

216 

217 sections, errors = parse( 

218 inspect.getdoc(f), # type: ignore[arg-type] 

219 inspect.signature(f), 

220 trim_doctest=True, 

221 ) 

222 assert len(sections) == 2 

223 assert len(sections[1].value) == 4 

224 assert not errors 

225 

226 # Verify that doctest flags have indeed been trimmed 

227 example_str = sections[1].value[1][1] 

228 assert "# doctest: +SKIP" not in example_str 

229 example_str = sections[1].value[3][1] 

230 assert "<BLANKLINE>" not in example_str 

231 assert "\n>>> print(list(range(1, 100)))\n" in example_str 

232 

233 

234@pytest.mark.xfail(reason="Possible change in docstring-parser") 

235def test_function_with_examples() -> None: 

236 """Parse a function docstring with examples.""" 

237 

238 def f(x: int, y: int) -> int: 

239 """ 

240 This function has annotations. 

241 

242 Examples 

243 -------- 

244 Some examples that will create an unified code block: 

245 

246 >>> 2 + 2 == 5 

247 False 

248 >>> print("examples") 

249 "examples" 

250 

251 This is just a random comment in the examples section. 

252 

253 These examples will generate two different code blocks. Note the blank line. 

254 

255 >>> print("I'm in the first code block!") 

256 "I'm in the first code block!" 

257 

258 >>> print("I'm in other code block!") 

259 "I'm in other code block!" 

260 

261 We also can write multiline examples: 

262 

263 >>> x = 3 + 2 

264 >>> y = x + 10 

265 >>> y 

266 15 

267 

268 This is just a typical Python code block: 

269 

270 ```python 

271 print("examples") 

272 return 2 + 2 

273 ``` 

274 

275 Even if it contains doctests, the following block is still considered a normal code-block. 

276 

277 ```python 

278 >>> print("examples") 

279 "examples" 

280 >>> 2 + 2 

281 4 

282 ``` 

283 

284 The blank line before an example is optional. 

285 >>> x = 3 

286 >>> y = "apple" 

287 >>> z = False 

288 >>> l = [x, y, z] 

289 >>> my_print_list_function(l) 

290 3 

291 "apple" 

292 False 

293 """ # noqa: D416, D212 

294 return x + y 

295 

296 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

297 assert len(sections) == 2 

298 assert len(sections[1].value) == 9 

299 assert not errors 

300 

301 

302def test_types_in_docstring() -> None: 

303 """Parse types in docstring.""" 

304 

305 def f(x, y): # noqa: ANN001, ANN202 

306 """ 

307 The types are written in the docstring. 

308 

309 Parameters 

310 ---------- 

311 x : int 

312 X value. 

313 y : int 

314 Y value. 

315 

316 Returns 

317 ------- 

318 int 

319 Sum X + Y. 

320 """ # noqa: D212, D416 

321 return x + y 

322 

323 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

324 assert len(sections) == 3 

325 assert not errors 

326 

327 x, y = sections[1].value 

328 r = sections[2].value 

329 

330 assert x.name == "x" 

331 assert x.annotation == "int" 

332 assert x.description == "X value." 

333 assert x.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD 

334 assert x.default is inspect.Signature.empty 

335 

336 assert y.name == "y" 

337 assert y.annotation == "int" 

338 assert y.description == "Y value." 

339 assert y.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD 

340 assert y.default is inspect.Signature.empty 

341 

342 assert r.annotation == "int" 

343 assert r.description == "Sum X + Y." 

344 

345 

346def test_types_and_optional_in_docstring() -> None: 

347 """Parse optional types in docstring.""" 

348 

349 def f(x=1, y=None): # noqa: ANN001, ANN202 

350 """ 

351 The types are written in the docstring. 

352 

353 Parameters 

354 ---------- 

355 x : int 

356 X value. 

357 y : int, optional 

358 Y value. 

359 

360 Returns 

361 ------- 

362 int 

363 Sum X + Y. 

364 """ # noqa: D212, D416 

365 return x + (y or 1) 

366 

367 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

368 assert len(sections) == 3 

369 assert not errors 

370 

371 x, y = sections[1].value 

372 

373 assert x.name == "x" 

374 assert x.annotation == "int" 

375 assert x.description == "X value." 

376 assert x.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD 

377 assert x.default == 1 

378 

379 assert y.name == "y" 

380 assert y.annotation == "int" 

381 assert y.description == "Y value." 

382 assert y.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD 

383 assert y.default is None 

384 

385 

386def test_types_in_signature_and_docstring() -> None: 

387 """Parse types in both signature and docstring.""" 

388 

389 def f(x: int, y: int) -> int: 

390 """ 

391 The types are written both in the signature and in the docstring. 

392 

393 Parameters 

394 ---------- 

395 x : int 

396 X value. 

397 y : int 

398 Y value. 

399 

400 Returns 

401 ------- 

402 int 

403 Sum X + Y. 

404 """ # noqa: D212, D416 

405 return x + y 

406 

407 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

408 assert len(sections) == 3 

409 assert not errors 

410 

411 

412def test_close_sections() -> None: 

413 """Parse sections without blank lines in between.""" 

414 

415 def f(x, y, z): # noqa: ANN202, ANN001 

416 """ 

417 Parameters 

418 ---------- 

419 x : 

420 X 

421 y : 

422 Y 

423 z : 

424 Z 

425 

426 Raises 

427 ------ 

428 Error2 

429 error. 

430 Error1 

431 error. 

432 

433 Returns 

434 ------- 

435 str 

436 value 

437 """ # noqa: D205, D416, D212 

438 return x + y + z 

439 

440 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

441 assert len(sections) == 3 

442 assert not errors 

443 

444 

445# test_code_blocks was removed as docstrings within a code block 

446# are not applicable to numpy docstrings 

447 

448 

449def test_extra_parameter() -> None: 

450 """Warn on extra parameter in docstring.""" 

451 

452 def f(x): # noqa: ANN202, ANN001 

453 """Parameters 

454 ---------- 

455 x : 

456 Integer. 

457 y : 

458 Integer. 

459 """ # noqa: D205 

460 return x 

461 

462 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

463 assert len(sections) == 1 

464 assert len(errors) == 1 

465 assert "No type" in errors[0] 

466 

467 

468def test_missing_parameter() -> None: 

469 """Don't warn on missing parameter in docstring.""" 

470 

471 # FIXME: could warn 

472 def f(x, y): # noqa: ANN001, ANN202 

473 """Parameters 

474 ---------- 

475 x : 

476 Integer. 

477 """ # noqa: D205 

478 return x + y 

479 

480 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

481 assert len(sections) == 1 

482 assert not errors 

483 

484 

485def test_multiple_lines_in_sections_items() -> None: 

486 """Parse multi-line item description.""" 

487 

488 def f(p: str, q: str): # noqa: ANN202 

489 """Hi. 

490 

491 Parameters 

492 ---------- 

493 p : 

494 This argument 

495 has a description 

496 spawning on multiple lines. 

497 

498 It even has blank lines in it. 

499 Some of these lines 

500 are indented for no reason. 

501 q : 

502 What if the first line is blank? 

503 """ 

504 return p + q 

505 

506 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

507 assert len(sections) == 2 

508 assert len(sections[1].value) == 2 

509 # numpy docstrings parameter description can be parsed even if misindentated 

510 assert not errors 

511 

512 

513def test_parse_args_kwargs() -> None: 

514 """Parse args and kwargs.""" 

515 

516 def f(a, *args, **kwargs) -> int: # noqa: ANN001, ARG001, ANN002, ANN003 

517 """Parameters 

518 ---------- 

519 a : 

520 a parameter. 

521 *args : 

522 args parameters. 

523 **kwargs : 

524 kwargs parameters. 

525 """ # noqa: D205 

526 return 1 

527 

528 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

529 assert len(sections) == 1 

530 expected_parameters = { 

531 "a": "a parameter.", 

532 "*args": "args parameters.", 

533 "**kwargs": "kwargs parameters.", 

534 } 

535 for param in sections[0].value: 

536 assert param.name in expected_parameters 

537 assert expected_parameters[param.name] == param.description 

538 assert not errors 

539 

540 

541def test_different_indentation() -> None: 

542 """Parse different indentations, warn on confusing indentation.""" 

543 

544 def f() -> None: 

545 """ 

546 Hello. 

547 

548 Raises 

549 ------ 

550 StartAt5 

551 this section's items starts with x spaces of indentation. 

552 Well indented continuation line. 

553 Badly indented continuation line (will not trigger an error). 

554 

555 Empty lines are preserved, as well as extra-indentation (this line is a code block). 

556 AnyOtherLine 

557 ...starting with exactly 5 spaces is a new item. 

558 """ # noqa: D416, D212 

559 

560 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type] 

561 assert len(sections) == 2 

562 assert len(sections[1].value) == 2 

563 assert sections[1].value[0].description == ( 

564 "this section's items starts with x spaces of indentation.\n" 

565 "Well indented continuation line.\n" 

566 " Badly indented continuation line (will not trigger an error).\n" 

567 "\n" 

568 " Empty lines are preserved, as well as extra-indentation (this line is a code block)." 

569 ) 

570 assert not errors