Coverage for tests/test_extension.py: 100.00%
90 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-18 01:26 +0100
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-18 01:26 +0100
1"""Tests for the Griffe extension."""
3import pytest
4from griffe import DocstringSectionKind, Extensions, GriffeLoader, temporary_visited_package
6from griffe_typingdoc import TypingDocExtension
8typing_imports = (
9 "from typing import Annotated, Doc, Generator, Iterator, Name, NotRequired, Raises, TypedDict, Unpack, Warns"
10)
11warning_imports = "from warnings import deprecated"
13# NOTE: Important! The value in calls to `Doc` will be parsed as a Name expression
14# if it is valid Python syntax for names. To make sure it is correctly parsed as a string,
15# it must contain invalid syntax for names, such as a dot at the end.
16# The alternative solution would be to add `from __future__ import annotations`
17# at the beginning of each temporary visited module.
20def test_extension_on_itself() -> None:
21 """Load our own package using the extension, assert a parameters section is added to the parsed docstring."""
22 loader = GriffeLoader(extensions=Extensions(TypingDocExtension()))
23 typingdoc = loader.load("griffe_typingdoc")
24 sections = typingdoc["TypingDocExtension.on_function_instance"].docstring.parsed
25 assert len(sections) == 2
26 assert sections[1].kind is DocstringSectionKind.parameters
27 assert sections[1].value[1].description == "The Griffe function just instantiated."
30def test_attribute_doc() -> None:
31 """Read documentation for attributes."""
32 with temporary_visited_package(
33 "package",
34 modules={"__init__.py": f"{typing_imports}\na: Annotated[str, Doc('Hello.')]"},
35 extensions=Extensions(TypingDocExtension()),
36 ) as package:
37 assert package["a"].docstring.value == "Hello."
40def test_parameter_doc() -> None:
41 """Read documentation for parameters."""
42 with temporary_visited_package(
43 "package",
44 modules={"__init__.py": f"{typing_imports}\ndef f(a: Annotated[str, Doc('Hello.')]): ..."},
45 extensions=Extensions(TypingDocExtension()),
46 ) as package:
47 assert package["f"].docstring.parsed[1].value[0].description == "Hello."
50def test_other_parameter_doc() -> None:
51 """Read documentation for other parameters, in unpack/typeddict annotations."""
52 with temporary_visited_package(
53 "package",
54 modules={
55 "__init__.py": f"""
56 {typing_imports}
57 class OtherParameters(TypedDict, total=False):
58 param1: Annotated[NotRequired[str], Doc("Hello.")]
60 def f(**kwargs: Annotated[Unpack[OtherParameters], Doc("See other parameters.")]):
61 ...
62 """,
63 },
64 extensions=Extensions(TypingDocExtension()),
65 ) as package:
66 assert package["f"].docstring.parsed[2].value[0].description == "Hello."
69def test_iterator_doc() -> None:
70 """Read documentation in iterator annotations."""
71 with temporary_visited_package(
72 "package",
73 modules={
74 "__init__.py": f"""
75 {typing_imports}
76 def f() -> Iterator[Annotated[int, Doc("Yielded hello.")]]:
77 ...
78 """,
79 },
80 extensions=Extensions(TypingDocExtension()),
81 ) as package:
82 assert package["f"].docstring.parsed[1].value[0].description == "Yielded hello."
85def test_generator_doc() -> None:
86 """Read documentation in generator annotations."""
87 with temporary_visited_package(
88 "package",
89 modules={
90 "__init__.py": f"""
91 {typing_imports}
92 def f() -> Generator[
93 Annotated[int, Doc("Yielded hello.")],
94 Annotated[int, Doc("Received hello.")],
95 Annotated[int, Doc("Returned hello.")],
96 ]:
97 ...
98 """,
99 },
100 extensions=Extensions(TypingDocExtension()),
101 ) as package:
102 sections = package["f"].docstring.parsed
103 assert sections[1].value[0].description == "Yielded hello."
104 assert sections[2].value[0].description == "Received hello."
105 assert sections[3].value[0].description == "Returned hello."
108def test_generator_tuples() -> None:
109 """Read documentation in generator annotations (in tuples)."""
110 with temporary_visited_package(
111 "package",
112 modules={
113 "__init__.py": f"""
114 {typing_imports}
115 def f() -> Generator[
116 tuple[
117 Annotated[int, Doc("First yielded.")],
118 Annotated[float, Doc("Second yielded.")],
119 ],
120 tuple[
121 Annotated[int, Doc("First received.")],
122 Annotated[float, Doc("Second received.")],
123 ],
124 tuple[
125 Annotated[int, Doc("First returned.")],
126 Annotated[float, Doc("Second returned.")],
127 ],
128 ]:
129 ...
130 """,
131 },
132 extensions=Extensions(TypingDocExtension()),
133 ) as package:
134 sections = package["f"].docstring.parsed
135 assert sections[1].value[0].description == "First yielded."
136 assert sections[1].value[1].description == "Second yielded."
137 assert sections[2].value[0].description == "First received."
138 assert sections[2].value[1].description == "Second received."
139 assert sections[3].value[0].description == "First returned."
140 assert sections[3].value[1].description == "Second returned."
143def test_return_doc() -> None:
144 """Read documentation for return value."""
145 with temporary_visited_package(
146 "package",
147 modules={"__init__.py": f"{typing_imports}\ndef f() -> Annotated[int, Doc('Hello.')]: ..."},
148 extensions=Extensions(TypingDocExtension()),
149 ) as package:
150 assert package["f"].docstring.parsed[1].value[0].description == "Hello."
153def test_unpacking_typed_dict() -> None:
154 """Unpack typed dicts, resolving them to their right location."""
155 with temporary_visited_package(
156 "package",
157 {
158 "__init__.py": """
159 from typing import TypedDict
160 from typing_extensions import Annotated, Doc, Unpack
162 from package import module
164 class Options(TypedDict):
165 foo: Annotated[int, Doc("Foo's description.")]
167 class A:
168 def __init__(self, **kwargs: Unpack[Options]) -> None:
169 '''Init.'''
170 self.options = kwargs
172 class B:
173 def __init__(self, **kwargs: Unpack[module.Options]) -> None:
174 '''Init.'''
175 self.options = kwargs
176 """,
177 "module.py": """
178 from typing import TypedDict
179 from typing_extensions import Annotated, Doc
181 class Options(TypedDict):
182 bar: Annotated[str, Doc("Bar's description.")]
183 """,
184 },
185 extensions=Extensions(TypingDocExtension()),
186 ) as package:
187 sections = package["A.__init__"].docstring.parsed
188 assert len(sections) == 2
189 assert sections[0].kind is DocstringSectionKind.text
190 assert sections[1].kind is DocstringSectionKind.other_parameters
191 foo = sections[1].value[0]
192 assert foo.name == "foo"
193 assert foo.description == "Foo's description."
194 assert str(foo.annotation).startswith("Annotated[int")
196 sections = package["B.__init__"].docstring.parsed
197 assert len(sections) == 2
198 assert sections[0].kind is DocstringSectionKind.text
199 assert sections[1].kind is DocstringSectionKind.other_parameters
200 bar = sections[1].value[0]
201 assert bar.name == "bar"
202 assert bar.description == "Bar's description."
203 assert str(bar.annotation).startswith("Annotated[str")
206@pytest.mark.parametrize(
207 "annotation",
208 ["int", "Annotated[int, '']"],
209)
210def test_ignore_unannotated_params(annotation: str) -> None:
211 """Ignore parameters that are not annotated with `Doc`."""
212 with temporary_visited_package(
213 "package",
214 {
215 "__init__.py": f"{typing_imports}\ndef f(a: {annotation}):\n '''Docstring.'''",
216 },
217 extensions=Extensions(TypingDocExtension()),
218 ) as package:
219 sections = package["f"].docstring.parsed
220 assert len(sections) == 1
221 assert sections[0].kind is DocstringSectionKind.text
224@pytest.mark.parametrize(
225 "annotation",
226 ["int", "Annotated[int, '']"],
227)
228def test_ignore_unannotated_other_params(annotation: str) -> None:
229 """Ignore other parameters that are not annotated with `Doc`."""
230 with temporary_visited_package(
231 "package",
232 {
233 "__init__.py": f"""
234 {typing_imports}
235 from typing import TypedDict
236 class Kwargs(TypedDict):
237 a: {annotation}
238 def f(**kwargs: Unpack[Kwargs]):
239 '''Docstring.'''
240 """,
241 },
242 extensions=Extensions(TypingDocExtension()),
243 ) as package:
244 sections = package["f"].docstring.parsed
245 assert len(sections) == 1
246 assert sections[0].kind is DocstringSectionKind.text
249@pytest.mark.parametrize(
250 "annotation",
251 ["int", "Annotated[int, '']"],
252)
253def test_ignore_unannotated_returns(annotation: str) -> None:
254 """Ignore return values that are not annotated with `Doc`."""
255 with temporary_visited_package(
256 "package",
257 {
258 "__init__.py": f"{typing_imports}\ndef f() -> {annotation}:\n '''Docstring.'''",
259 },
260 extensions=Extensions(TypingDocExtension()),
261 ) as package:
262 sections = package["f"].docstring.parsed
263 assert len(sections) == 1
264 assert sections[0].kind is DocstringSectionKind.text
267@pytest.mark.parametrize(
268 "annotation",
269 ["int", "Annotated[int, '']"],
270)
271def test_ignore_unannotated_yields(annotation: str) -> None:
272 """Ignore yields that are not annotated with `Doc`."""
273 with temporary_visited_package(
274 "package",
275 {
276 "__init__.py": f"{typing_imports}\ndef f() -> Iterator[{annotation}]:\n '''Docstring.'''",
277 },
278 extensions=Extensions(TypingDocExtension()),
279 ) as package:
280 sections = package["f"].docstring.parsed
281 assert len(sections) == 1
282 assert sections[0].kind is DocstringSectionKind.text
285@pytest.mark.parametrize(
286 "annotation",
287 ["int", "Annotated[int, '']"],
288)
289def test_ignore_unannotated_receives(annotation: str) -> None:
290 """Ignore receives that are not annotated with `Doc`."""
291 with temporary_visited_package(
292 "package",
293 {
294 "__init__.py": f"{typing_imports}\ndef f() -> Generator[int, {annotation}, None]:\n '''Docstring.'''",
295 },
296 extensions=Extensions(TypingDocExtension()),
297 ) as package:
298 sections = package["f"].docstring.parsed
299 assert len(sections) == 1
300 assert sections[0].kind is DocstringSectionKind.text