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
« 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]."""
3import inspect
4from textwrap import dedent
5from typing import Any, Optional
7import pytest
9from pytkdocs.loader import Loader
10from pytkdocs.parsers.docstrings.base import Section
11from pytkdocs.parsers.docstrings.numpy import Numpy
14class DummyObject:
15 path = "o"
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)
27 return parser.parse(
28 dedent(docstring).strip(),
29 context={"obj": DummyObject(), "signature": signature, "type": return_type},
30 )
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
40def test_multi_line_docstring() -> None:
41 """Parse a multi-line docstring."""
42 sections, errors = parse(
43 """
44 A somewhat longer docstring.
46 Blablablabla.
47 """,
48 )
49 assert len(sections) == 1
50 assert not errors
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.
60 Parameters
61 ----------
62 void :
63 SEGFAULT.
64 niet :
65 SEGFAULT.
66 nada :
67 SEGFAULT.
68 rien :
69 SEGFAULT.
71 Raises
72 ------
73 GlobalError
74 when nothing works as expected.
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
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.
95 Parameters
96 ----------
97 void : str
98 niet : str
100 Raises
101 ------
102 GlobalError
104 Returns
105 -------
106 bool
107 """,
108 )
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]
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
123 assert sections[2].type is Section.Type.EXCEPTIONS
124 for p in sections[2].value:
125 assert p.description is not None
127 assert sections[3].type is Section.Type.RETURN
128 assert sections[3].value.description is not None
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
140def test_function_without_annotations() -> None:
141 """Parse a function docstring without signature annotations."""
143 def f(x, y): # noqa: ANN202, ANN001
144 """
145 This function has no annotations.
147 Parameters
148 ----------
149 x:
150 X value.
151 y:
152 Y value.
154 Returns
155 -------
156 float
157 Sum X + Y.
158 """ # noqa: D212, D416
159 return x + y
161 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
162 assert len(sections) == 3
163 assert not errors
166def test_function_with_annotations() -> None:
167 """Parse a function docstring with signature annotations."""
169 def f(x: int, y: int) -> int:
170 """
171 This function has annotations.
173 Parameters
174 ----------
175 x:
176 X value.
177 y:
178 Y value.
180 Returns
181 -------
182 int
183 Sum X + Y.
184 """ # noqa: D212, D416
185 return x + y
187 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
188 assert len(sections) == 3
189 assert not errors
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."""
196 def f(x: int) -> int:
197 r"""
198 Test function.
200 Example
201 -------
202 We want to skip the following test.
203 >>> 1 + 1 == 3 # doctest: +SKIP
204 True
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
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
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
234@pytest.mark.xfail(reason="Possible change in docstring-parser")
235def test_function_with_examples() -> None:
236 """Parse a function docstring with examples."""
238 def f(x: int, y: int) -> int:
239 """
240 This function has annotations.
242 Examples
243 --------
244 Some examples that will create an unified code block:
246 >>> 2 + 2 == 5
247 False
248 >>> print("examples")
249 "examples"
251 This is just a random comment in the examples section.
253 These examples will generate two different code blocks. Note the blank line.
255 >>> print("I'm in the first code block!")
256 "I'm in the first code block!"
258 >>> print("I'm in other code block!")
259 "I'm in other code block!"
261 We also can write multiline examples:
263 >>> x = 3 + 2
264 >>> y = x + 10
265 >>> y
266 15
268 This is just a typical Python code block:
270 ```python
271 print("examples")
272 return 2 + 2
273 ```
275 Even if it contains doctests, the following block is still considered a normal code-block.
277 ```python
278 >>> print("examples")
279 "examples"
280 >>> 2 + 2
281 4
282 ```
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
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
302def test_types_in_docstring() -> None:
303 """Parse types in docstring."""
305 def f(x, y): # noqa: ANN001, ANN202
306 """
307 The types are written in the docstring.
309 Parameters
310 ----------
311 x : int
312 X value.
313 y : int
314 Y value.
316 Returns
317 -------
318 int
319 Sum X + Y.
320 """ # noqa: D212, D416
321 return x + y
323 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
324 assert len(sections) == 3
325 assert not errors
327 x, y = sections[1].value
328 r = sections[2].value
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
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
342 assert r.annotation == "int"
343 assert r.description == "Sum X + Y."
346def test_types_and_optional_in_docstring() -> None:
347 """Parse optional types in docstring."""
349 def f(x=1, y=None): # noqa: ANN001, ANN202
350 """
351 The types are written in the docstring.
353 Parameters
354 ----------
355 x : int
356 X value.
357 y : int, optional
358 Y value.
360 Returns
361 -------
362 int
363 Sum X + Y.
364 """ # noqa: D212, D416
365 return x + (y or 1)
367 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
368 assert len(sections) == 3
369 assert not errors
371 x, y = sections[1].value
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
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
386def test_types_in_signature_and_docstring() -> None:
387 """Parse types in both signature and docstring."""
389 def f(x: int, y: int) -> int:
390 """
391 The types are written both in the signature and in the docstring.
393 Parameters
394 ----------
395 x : int
396 X value.
397 y : int
398 Y value.
400 Returns
401 -------
402 int
403 Sum X + Y.
404 """ # noqa: D212, D416
405 return x + y
407 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
408 assert len(sections) == 3
409 assert not errors
412def test_close_sections() -> None:
413 """Parse sections without blank lines in between."""
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
426 Raises
427 ------
428 Error2
429 error.
430 Error1
431 error.
433 Returns
434 -------
435 str
436 value
437 """ # noqa: D205, D416, D212
438 return x + y + z
440 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
441 assert len(sections) == 3
442 assert not errors
445# test_code_blocks was removed as docstrings within a code block
446# are not applicable to numpy docstrings
449def test_extra_parameter() -> None:
450 """Warn on extra parameter in docstring."""
452 def f(x): # noqa: ANN202, ANN001
453 """Parameters
454 ----------
455 x :
456 Integer.
457 y :
458 Integer.
459 """ # noqa: D205
460 return x
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]
468def test_missing_parameter() -> None:
469 """Don't warn on missing parameter in docstring."""
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
480 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
481 assert len(sections) == 1
482 assert not errors
485def test_multiple_lines_in_sections_items() -> None:
486 """Parse multi-line item description."""
488 def f(p: str, q: str): # noqa: ANN202
489 """Hi.
491 Parameters
492 ----------
493 p :
494 This argument
495 has a description
496 spawning on multiple lines.
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
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
513def test_parse_args_kwargs() -> None:
514 """Parse args and kwargs."""
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
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
541def test_different_indentation() -> None:
542 """Parse different indentations, warn on confusing indentation."""
544 def f() -> None:
545 """
546 Hello.
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).
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
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