Coverage for tests/test_parsers/test_docstrings/test_google.py: 94.01%
305 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-09 18:24 +0100
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-09 18:24 +0100
1"""Tests for [the `parsers.docstrings.google` module][pytkdocs.parsers.docstrings.google]."""
3import inspect
4from collections.abc import Iterator
5from textwrap import dedent
6from typing import Any, Optional
8from pytkdocs.loader import Loader
9from pytkdocs.parsers.docstrings.base import Section
10from pytkdocs.parsers.docstrings.google import Google
11from pytkdocs.serializer import serialize_attribute
14class DummyObject:
15 path = "o"
18def parse(
19 docstring: str,
20 signature: Optional[inspect.Signature] = None,
21 return_type: Any = inspect.Signature.empty,
22 *,
23 admonitions: bool = True,
24 trim_doctest: bool = False,
25) -> tuple[list[Section], list[str]]:
26 """Helper to parse a doctring."""
27 parser = Google(replace_admonitions=admonitions, trim_doctest_flags=trim_doctest)
29 return parser.parse(
30 dedent(docstring).strip(),
31 context={"obj": DummyObject(), "signature": signature, "type": return_type},
32 )
35def test_simple_docstring() -> None:
36 """Parse a simple docstring."""
37 sections, errors = parse("A simple docstring.")
38 assert len(sections) == 1
39 assert not errors
42def test_multi_line_docstring() -> None:
43 """Parse a multi-line docstring."""
44 sections, errors = parse(
45 """
46 A somewhat longer docstring.
48 Blablablabla.
49 """,
50 )
51 assert len(sections) == 1
52 assert not errors
55def test_sections_without_signature() -> None:
56 """Parse a docstring without a signature."""
57 sections, errors = parse(
58 """
59 Sections without signature.
61 Parameters:
62 void: SEGFAULT.
63 niet: SEGFAULT.
64 nada: SEGFAULT.
65 rien: SEGFAULT.
67 Keyword Args:
68 keywd: SEGFAULT.
70 Exceptions:
71 GlobalError: when nothing works as expected.
73 Returns:
74 Itself.
75 """,
76 )
78 assert len(sections) == 5
79 assert len(errors) == 6 # missing annotations for params and return
80 for error in errors[:-1]:
81 assert "param" in error
82 assert "return" in errors[-1]
85def test_property_docstring() -> None:
86 """Parse a property docstring."""
87 class_ = Loader().get_object_documentation("tests.fixtures.parsing.docstrings.NotDefinedYet")
88 prop = class_.attributes[0]
89 sections, errors = prop.docstring_sections, prop.docstring_errors
90 assert len(sections) == 2
91 assert not errors
94def test_function_without_annotations() -> None:
95 """Parse a function docstring without signature annotations."""
97 def f(x, y, *, z): # noqa: ANN001, ANN202
98 """This function has no annotations.
100 Parameters:
101 x: X value.
102 y: Y value.
104 Keyword Args:
105 z: Z value.
107 Returns:
108 Sum X + Y + Z.
109 """
110 return x + y + z
112 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
113 assert len(sections) == 4
114 assert len(errors) == 1
115 assert "No return type/annotation in" in errors[0]
118def test_function_with_annotations() -> None:
119 """Parse a function docstring with signature annotations."""
121 def f(x: int, y: int, *, z: int) -> int: # noqa: ARG001
122 """This function has annotations.
124 Parameters:
125 x: X value.
126 y: Y value.
128 Keyword Arguments:
129 z: Z value.
131 Returns:
132 Sum X + Y.
133 """
134 return x + y
136 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
137 assert len(sections) == 4
138 assert not errors
141def test_function_with_examples_trim_doctest() -> None:
142 """Parse example docstring with trim_doctest_flags option."""
144 def f(x: int) -> int:
145 r"""Test function.
147 Example:
148 We want to skip the following test.
149 >>> 1 + 1 == 3 # doctest: +SKIP
150 True
152 And then a few more examples here:
153 >>> print("a\n\nb")
154 a
155 <BLANKLINE>
156 b
157 >>> 1 + 1 == 2 # doctest: +SKIP
158 >>> print(list(range(1, 100))) # doctest: +ELLIPSIS
159 [1, 2, ..., 98, 99]
160 """
161 return x
163 sections, errors = parse(
164 inspect.getdoc(f), # type: ignore[arg-type]
165 inspect.signature(f),
166 trim_doctest=True,
167 )
168 assert len(sections) == 2
169 assert len(sections[1].value) == 4
170 assert not errors
172 # Verify that doctest flags have indeed been trimmed
173 example_str = sections[1].value[1][1]
174 assert "# doctest: +SKIP" not in example_str
175 example_str = sections[1].value[3][1]
176 assert "<BLANKLINE>" not in example_str
177 assert "\n>>> print(list(range(1, 100)))\n" in example_str
180def test_function_with_examples() -> None:
181 """Parse a function docstring with examples."""
183 def f(x: int, y: int) -> int:
184 """This function has annotations.
186 Examples:
187 Some examples that will create an unified code block:
189 >>> 2 + 2 == 5
190 False
191 >>> print("examples")
192 "examples"
194 This is just a random comment in the examples section.
196 These examples will generate two different code blocks. Note the blank line.
198 >>> print("I'm in the first code block!")
199 "I'm in the first code block!"
201 >>> print("I'm in other code block!")
202 "I'm in other code block!"
204 We also can write multiline examples:
206 >>> x = 3 + 2
207 >>> y = x + 10
208 >>> y
209 15
211 This is just a typical Python code block:
213 ```python
214 print("examples")
215 return 2 + 2
216 ```
218 Even if it contains doctests, the following block is still considered a normal code-block.
220 ```python
221 >>> print("examples")
222 "examples"
223 >>> 2 + 2
224 4
225 ```
227 The blank line before an example is optional.
228 >>> x = 3
229 >>> y = "apple"
230 >>> z = False
231 >>> l = [x, y, z]
232 >>> my_print_list_function(l)
233 3
234 "apple"
235 False
236 """
237 return x + y
239 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
240 assert len(sections) == 2
241 assert len(sections[1].value) == 9
242 assert not errors
245def test_types_in_docstring() -> None:
246 """Parse types in docstring."""
248 def f(x, y, *, z): # noqa: ANN001, ANN202
249 """The types are written in the docstring.
251 Parameters:
252 x (int): X value.
253 y (int): Y value.
255 Keyword Args:
256 z (int): Z value.
258 Returns:
259 int: Sum X + Y + Z.
260 """
261 return x + y + z
263 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
264 assert len(sections) == 4
265 assert not errors
267 assert sections[0].type == Section.Type.MARKDOWN
268 assert sections[1].type == Section.Type.PARAMETERS
269 assert sections[2].type == Section.Type.KEYWORD_ARGS
270 assert sections[3].type == Section.Type.RETURN
272 x, y = sections[1].value
273 (z,) = sections[2].value
274 r = sections[3].value
276 assert x.name == "x"
277 assert x.annotation == "int"
278 assert x.description == "X value."
279 assert x.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
280 assert x.default is inspect.Signature.empty
282 assert y.name == "y"
283 assert y.annotation == "int"
284 assert y.description == "Y value."
285 assert y.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
286 assert y.default is inspect.Signature.empty
288 assert z.name == "z"
289 assert z.annotation == "int"
290 assert z.description == "Z value."
291 assert z.kind is inspect.Parameter.KEYWORD_ONLY
292 assert z.default is inspect.Signature.empty
294 assert r.annotation == "int"
295 assert r.description == "Sum X + Y + Z."
298def test_types_and_optional_in_docstring() -> None:
299 """Parse optional types in docstring."""
301 def f(x=1, y=None, *, z=None): # noqa: ANN001, ANN202
302 """The types are written in the docstring.
304 Parameters:
305 x (int): X value.
306 y (int, optional): Y value.
308 Keyword Args:
309 z (int, optional): Z value.
311 Returns:
312 int: Sum X + Y + Z.
313 """
314 return x + (y or 1) + (z or 1)
316 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
317 assert len(sections) == 4
318 assert not errors
320 assert sections[0].type == Section.Type.MARKDOWN
321 assert sections[1].type == Section.Type.PARAMETERS
322 assert sections[2].type == Section.Type.KEYWORD_ARGS
324 x, y = sections[1].value
325 (z,) = sections[2].value
327 assert x.name == "x"
328 assert x.annotation == "int"
329 assert x.description == "X value."
330 assert x.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
331 assert x.default == 1
333 assert y.name == "y"
334 assert y.annotation == "int"
335 assert y.description == "Y value."
336 assert y.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
337 assert y.default is None
339 assert z.name == "z"
340 assert z.annotation == "int"
341 assert z.description == "Z value."
342 assert z.kind is inspect.Parameter.KEYWORD_ONLY
343 assert z.default is None
346def test_types_in_signature_and_docstring() -> None:
347 """Parse types in both signature and docstring. Should prefer the docstring type."""
349 def f(x: int, y: int, *, z: int) -> int:
350 """The types are written both in the signature and in the docstring.
352 Parameters:
353 x (str): X value.
354 y (str): Y value.
356 Keyword Args:
357 z (str): Z value.
359 Returns:
360 str: Sum X + Y + Z.
361 """
362 return x + y + z
364 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
365 assert len(sections) == 4
366 assert not errors
368 assert sections[0].type == Section.Type.MARKDOWN
369 assert sections[1].type == Section.Type.PARAMETERS
370 assert sections[2].type == Section.Type.KEYWORD_ARGS
371 assert sections[3].type == Section.Type.RETURN
373 x, y = sections[1].value
374 (z,) = sections[2].value
375 r = sections[3].value
377 assert x.name == "x"
378 assert x.annotation == "str"
379 assert x.description == "X value."
380 assert x.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
381 assert x.default is inspect.Signature.empty
383 assert y.name == "y"
384 assert y.annotation == "str"
385 assert y.description == "Y value."
386 assert y.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
387 assert y.default is inspect.Signature.empty
389 assert z.name == "z"
390 assert z.annotation == "str"
391 assert z.description == "Z value."
392 assert z.kind is inspect.Parameter.KEYWORD_ONLY
393 assert z.default is inspect.Signature.empty
395 assert r.annotation == "str"
396 assert r.description == "Sum X + Y + Z."
399def test_close_sections() -> None:
400 """Parse sections without blank lines in between."""
402 def f(x, y, z): # noqa: ANN202, ANN001
403 """Parameters:
404 x: X.
405 Parameters:
406 y: Y.
408 Parameters:
409 z: Z.
410 Exceptions:
411 Error2: error.
412 Exceptions:
413 Error1: error.
415 Returns:
416 1.
418 Returns:
419 2.
420 """ # noqa: D205
421 return x + y + z
423 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
424 assert len(sections) == 7
425 assert len(errors) == 2 # no return type annotations
428def test_code_blocks() -> None:
429 """Parse code blocks."""
431 def f(s): # noqa: ANN001, ANN202
432 """This docstring contains a docstring in a code block o_O!
434 ```python
435 '''
436 This docstring is contained in another docstring O_o!
438 Parameters:
439 s: A string.
440 '''
441 ```
442 """
443 return s
445 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
446 assert len(sections) == 1
447 assert not errors
450def test_indented_code_block() -> None:
451 """Parse indented code blocks."""
453 def f(s): # noqa: ANN001, ANN202
454 '''
455 This docstring contains a docstring in a code block o_O!
457 """
458 This docstring is contained in another docstring O_o!
460 Parameters:
461 s: A string.
462 """
463 ''' # noqa: D212
464 return s
466 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
467 assert len(sections) == 1
468 assert not errors
471def test_extra_parameter() -> None:
472 """Warn on extra parameter in docstring."""
474 def f(x): # noqa: ANN202, ANN001
475 """
476 Parameters:
477 x: Integer.
478 y: Integer.
479 """ # noqa: D205, D212
480 return x
482 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
483 assert len(sections) == 1
484 assert len(errors) == 1
485 assert "No type" in errors[0]
488def test_missing_parameter() -> None:
489 """Don't warn on missing parameter in docstring."""
491 # FIXME: could warn
492 def f(x, y): # noqa: ANN202, ANN001
493 """
494 Parameters:
495 x: Integer.
496 """ # noqa: D205, D212
497 return x + y
499 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
500 assert len(sections) == 1
501 assert not errors
504def test_param_line_without_colon() -> None:
505 """Warn when missing colon."""
507 def f(x: int): # noqa: ANN202
508 """
509 Parameters:
510 x is an integer.
511 """ # noqa: D205, D212
512 return x
514 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
515 assert not sections # getting x fails, so the section is empty and discarded
516 assert len(errors) == 2
517 assert "pair" in errors[0]
518 assert "Empty" in errors[1]
521def test_param_line_without_colon_keyword_only() -> None:
522 """Warn when missing colon."""
524 def f(*, x: int): # noqa: ANN202
525 """
526 Keyword Args:
527 x is an integer.
528 """ # noqa: D205, D212
529 return x
531 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
532 assert not sections # getting x fails, so the section is empty and discarded
533 assert len(errors) == 2
534 assert "pair" in errors[0]
535 assert "Empty" in errors[1]
538def test_admonitions() -> None:
539 """Parse admonitions."""
541 def f() -> None:
542 """Note:
543 Hello.
545 Note: With title.
546 Hello again.
548 Something:
549 Something.
550 """ # noqa: D205
552 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
553 assert len(sections) == 1
554 assert not errors
557def test_invalid_sections() -> None:
558 """Warn on invalid (empty) sections."""
560 def f() -> None:
561 """Parameters:
562 Exceptions:
563 Exceptions:
565 Returns:
566 Note:
568 Important:
569 """ # noqa: D205, D415, D414
571 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
572 assert len(sections) == 1
573 for error in errors[:3]:
574 assert "Empty" in error
575 assert "Empty return section at line" in errors[3]
576 assert "Empty" in errors[-1]
579def test_multiple_lines_in_sections_items() -> None:
580 """Parse multi-line item description."""
582 def f(p: str, q: str): # noqa: ANN202
583 """Hi.
585 Arguments:
586 p: This argument
587 has a description
588 spawning on multiple lines.
590 It even has blank lines in it.
591 Some of these lines
592 are indented for no reason.
593 q:
594 What if the first line is blank?
595 """
596 return p + q
598 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
599 assert len(sections) == 2
600 assert len(sections[1].value) == 2
601 assert errors
602 for error in errors:
603 assert "should be 4 * 2 = 8 spaces, not" in error
606def test_parse_args_kwargs() -> None:
607 """Parse args and kwargs."""
609 def f(a, *args, **kwargs): # noqa: ANN202, ANN001, ANN002, ARG001, ANN003
610 """
611 Arguments:
612 a: a parameter.
613 *args: args parameters.
614 **kwargs: kwargs parameters.
615 """ # noqa: D205, D212
616 return 1
618 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
619 assert len(sections) == 1
620 expected_parameters = {"a": "a parameter.", "*args": "args parameters.", "**kwargs": "kwargs parameters."}
621 for param in sections[0].value:
622 assert param.name in expected_parameters
623 assert expected_parameters[param.name] == param.description
624 assert not errors
627def test_parse_args_kwargs_keyword_only() -> None:
628 """Parse args and kwargs."""
630 def f(a, *args, **kwargs) -> int: # noqa: ANN002, ANN001, ARG001, ANN003
631 """Arguments:
632 a: a parameter.
633 *args: args parameters.
635 Keyword Args:
636 **kwargs: kwargs parameters.
637 """ # noqa: D205
638 return 1
640 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
641 assert len(sections) == 2
642 expected_parameters = {"a": "a parameter.", "*args": "args parameters."}
643 for param in sections[0].value:
644 assert param.name in expected_parameters
645 assert expected_parameters[param.name] == param.description
647 expected_parameters = {"**kwargs": "kwargs parameters."}
648 for param in sections[1].value:
649 assert param.name in expected_parameters
650 assert expected_parameters[param.name] == param.description
652 assert not errors
655def test_different_indentation() -> None:
656 """Parse different indentations, warn on confusing indentation."""
658 def f() -> None:
659 """Hello.
661 Raises:
662 StartAt5: this section's items starts with 5 spaces of indentation.
663 Well indented continuation line.
664 Badly indented continuation line (will trigger an error).
666 Empty lines are preserved, as well as extra-indentation (this line is a code block).
667 AnyOtherLine: ...starting with exactly 5 spaces is a new item.
668 AnyLine: ...indented with less than 5 spaces signifies the end of the section.
669 """
671 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
672 assert len(sections) == 3
673 assert len(sections[1].value) == 2
674 assert sections[1].value[0].description == (
675 "this section's items starts with 5 spaces of indentation.\n"
676 "Well indented continuation line.\n"
677 "Badly indented continuation line (will trigger an error).\n"
678 "\n"
679 " Empty lines are preserved, as well as extra-indentation (this line is a code block)."
680 )
681 assert sections[2].value == " AnyLine: ...indented with less than 5 spaces signifies the end of the section."
682 assert len(errors) == 1
683 assert "should be 5 * 2 = 10 spaces, not 6" in errors[0]
686def test_parse_module_attributes_section() -> None:
687 """Parse attributes section in modules."""
688 loader = Loader()
689 obj = loader.get_object_documentation("tests.fixtures.docstring_attributes_section")
690 assert len(obj.docstring_sections) == 2
691 assert not obj.docstring_errors
692 attr_section = obj.docstring_sections[1]
693 assert attr_section.type == Section.Type.ATTRIBUTES
694 assert len(attr_section.value) == 5
695 expected = [
696 {"name": "A", "annotation": "int", "description": "Alpha."},
697 {"name": "B", "annotation": "bytes", "description": "Beta."},
698 {"name": "C", "annotation": "bool", "description": "Gamma."},
699 {"name": "D", "annotation": "", "description": "Delta."},
700 {"name": "E", "annotation": "float", "description": "Epsilon."},
701 ]
702 assert [serialize_attribute(attr) for attr in attr_section.value] == expected
705def test_docstring_with_yield_section() -> None:
706 """Parse Yields section."""
708 def f(): # noqa: ANN202
709 """A useless range wrapper.
711 Yields:
712 int: Integers.
713 """
714 yield from range(10)
716 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
717 assert len(sections) == 2
718 annotated = sections[1].value
719 assert annotated.annotation == "int"
720 assert annotated.description == "Integers."
721 assert not errors
724def test_docstring_with_yield_section_and_return_annotation() -> None:
725 """Parse Yields section."""
727 def f() -> Iterator[int]:
728 """A useless range wrapper.
730 Yields:
731 Integers.
732 """
733 yield from range(10)
735 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
736 assert len(sections) == 2
737 annotated = sections[1].value
738 assert annotated.annotation == Iterator[int]
739 assert annotated.description == "Integers."
740 assert not errors
743def test_keyword_args_no_type() -> None:
744 """Parse types for keyword arguments."""
746 def f(**kwargs) -> None: # noqa: ANN003
747 """Do nothing.
749 Keyword Arguments:
750 a: No type.
751 """
753 sections, errors = parse(inspect.getdoc(f), inspect.signature(f)) # type: ignore[arg-type]
754 assert len(sections) == 2
755 kwargs = sections[1].value
756 assert kwargs[0].name == "a"
757 assert kwargs[0].annotation is inspect.Parameter.empty
758 assert kwargs[0].description == "No type."
759 assert kwargs[0].kind is inspect.Parameter.KEYWORD_ONLY
760 assert kwargs[0].default is inspect.Parameter.empty
761 assert len(errors) == 1
762 assert "No type annotation for parameter" in errors[0]
765def test_keyword_args_type() -> None:
766 """Parse types for keyword arguments."""
768 def f(**kwargs) -> None: # noqa: ANN003
769 """Do nothing.
771 Keyword Arguments:
772 a (int): Typed.
773 """
775 sections, errors = parse(inspect.getdoc(f) or "", inspect.signature(f))
776 assert len(sections) == 2
777 kwargs = sections[1].value
778 assert kwargs[0].name == "a"
779 assert kwargs[0].annotation == "int"
780 assert kwargs[0].description == "Typed."
781 assert kwargs[0].kind is inspect.Parameter.KEYWORD_ONLY
782 assert kwargs[0].default is inspect.Parameter.empty
783 assert not errors