Coverage for tests/test_visitor.py: 100.00%
103 statements
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
1"""Test visit mechanisms."""
3from __future__ import annotations
5from textwrap import dedent
7import pytest
9from griffe import GriffeLoader, temporary_pypackage, temporary_visited_module, temporary_visited_package
12def test_not_defined_at_runtime() -> None:
13 """Assert that objects not defined at runtime are not added to wildcards expansions."""
14 with temporary_pypackage("package", ["module_a.py", "module_b.py", "module_c.py"]) as tmp_package:
15 tmp_package.path.joinpath("__init__.py").write_text("from package.module_a import *")
16 tmp_package.path.joinpath("module_a.py").write_text(
17 dedent(
18 """
19 import typing
20 from typing import TYPE_CHECKING
22 from package.module_b import CONST_B
23 from package.module_c import CONST_C
25 if typing.TYPE_CHECKING: # always false
26 from package.module_b import TYPE_B
27 if TYPE_CHECKING: # always false
28 from package.module_c import TYPE_C
29 """,
30 ),
31 )
32 tmp_package.path.joinpath("module_b.py").write_text("CONST_B = 'hi'\nTYPE_B = str")
33 tmp_package.path.joinpath("module_c.py").write_text("CONST_C = 'ho'\nTYPE_C = str")
34 loader = GriffeLoader(search_paths=[tmp_package.tmpdir])
35 package = loader.load(tmp_package.name)
36 loader.resolve_aliases()
37 assert "CONST_B" in package.members
38 assert "CONST_C" in package.members
39 assert "TYPE_B" not in package.members
40 assert "TYPE_C" not in package.members
43@pytest.mark.parametrize(
44 ("decorator", "labels"),
45 [
46 ("property", ("property",)),
47 ("staticmethod", ("staticmethod",)),
48 ("classmethod", ("classmethod",)),
49 ("functools.cache", ("cached",)),
50 ("cache", ("cached",)),
51 ("functools.cached_property", ("cached", "property")),
52 ("cached_property", ("cached", "property")),
53 ("functools.lru_cache", ("cached",)),
54 ("functools.lru_cache(maxsize=8)", ("cached",)),
55 ("lru_cache", ("cached",)),
56 ("lru_cache(maxsize=8)", ("cached",)),
57 ("abc.abstractmethod", ("abstractmethod",)),
58 ("abstractmethod", ("abstractmethod",)),
59 ("dataclasses.dataclass", ("dataclass",)),
60 ("dataclass", ("dataclass",)),
61 ],
62)
63def test_set_function_labels_using_decorators(decorator: str, labels: tuple[str, ...]) -> None:
64 """Assert decorators are used to set labels on functions.
66 Parameters:
67 decorator: A parametrized decorator.
68 labels: The parametrized tuple of expected labels.
69 """
70 code = f"""
71 import abc
72 import dataclasses
73 import functools
74 from abc import abstractmethod
75 from dataclasses import dataclass
76 from functools import cache, cached_property, lru_cache
78 class A:
79 @{decorator}
80 def f(self):
81 return 0
82 """
83 with temporary_visited_module(code) as module:
84 assert module["A.f"].has_labels(*labels)
87@pytest.mark.parametrize(
88 ("decorator", "labels"),
89 [
90 ("dataclasses.dataclass", ("dataclass",)),
91 ("dataclass", ("dataclass",)),
92 ],
93)
94def test_set_class_labels_using_decorators(decorator: str, labels: tuple[str, ...]) -> None:
95 """Assert decorators are used to set labels on classes.
97 Parameters:
98 decorator: A parametrized decorator.
99 labels: The parametrized tuple of expected labels.
100 """
101 code = f"""
102 import dataclasses
103 from dataclasses import dataclass
105 @{decorator}
106 class A: ...
107 """
108 with temporary_visited_module(code) as module:
109 assert module["A"].has_labels(*labels)
112def test_handle_property_setter_and_deleter() -> None:
113 """Assert property setters and deleters are supported."""
114 code = """
115 class A:
116 def __init__(self): self._thing = 0
118 @property
119 def thing(self): return self._thing
121 @thing.setter
122 def thing(self, value): self._thing = value
124 @thing.deleter
125 def thing(self): del self._thing
126 """
127 with temporary_visited_module(code) as module:
128 assert module["A.thing"].has_labels("property", "writable", "deletable")
129 assert module["A.thing"].setter.is_function
130 assert module["A.thing"].deleter.is_function
133@pytest.mark.parametrize(
134 "decorator",
135 [
136 "overload",
137 "typing.overload",
138 ],
139)
140def test_handle_typing_overaload(decorator: str) -> None:
141 """Assert `typing.overload` is supported.
143 Parameters:
144 decorator: A parametrized overload decorator.
145 """
146 code = f"""
147 import typing
148 from typing import overload
149 from pathlib import Path
151 class A:
152 @{decorator}
153 def absolute(self, path: str) -> str:
154 ...
156 @{decorator}
157 def absolute(self, path: Path) -> Path:
158 ...
160 def absolute(self, path: str | Path) -> str | Path:
161 ...
162 """
163 with temporary_visited_module(code) as module:
164 overloads = module["A.absolute"].overloads
165 assert len(overloads) == 2
166 assert overloads[0].parameters["path"].annotation.name == "str"
167 assert overloads[1].parameters["path"].annotation.name == "Path"
168 assert overloads[0].returns.name == "str"
169 assert overloads[1].returns.name == "Path"
172@pytest.mark.parametrize(
173 "statements",
174 [
175 """__all__ = moda_all + modb_all + modc_all + ["CONST_INIT"]""",
176 """__all__ = ["CONST_INIT", *moda_all, *modb_all, *modc_all]""",
177 """
178 __all__ = ["CONST_INIT"]
179 __all__ += moda_all + modb_all + modc_all
180 """,
181 """
182 __all__ = moda_all + modb_all + modc_all
183 __all__ += ["CONST_INIT"]
184 """,
185 """
186 __all__ = ["CONST_INIT"]
187 __all__ += moda_all
188 __all__ += modb_all + modc_all
189 """,
190 ],
191)
192def test_parse_complex__all__assignments(statements: str) -> None:
193 """Check our ability to expand exports based on `__all__` [augmented] assignments.
195 Parameters:
196 statements: Parametrized text containing `__all__` [augmented] assignments.
197 """
198 with temporary_pypackage("package", ["moda.py", "modb.py", "modc.py"]) as tmp_package:
199 tmp_package.path.joinpath("moda.py").write_text("CONST_A = 1\n\n__all__ = ['CONST_A']")
200 tmp_package.path.joinpath("modb.py").write_text("CONST_B = 1\n\n__all__ = ['CONST_B']")
201 tmp_package.path.joinpath("modc.py").write_text("CONST_C = 2\n\n__all__ = ['CONST_C']")
202 code = """
203 from package.moda import *
204 from package.moda import __all__ as moda_all
205 from package.modb import *
206 from package.modb import __all__ as modb_all
207 from package.modc import *
208 from package.modc import __all__ as modc_all
210 CONST_INIT = 0
211 """
212 tmp_package.path.joinpath("__init__.py").write_text(dedent(code) + dedent(statements))
214 loader = GriffeLoader(search_paths=[tmp_package.tmpdir])
215 package = loader.load(tmp_package.name)
216 loader.resolve_aliases()
218 assert package.exports == {"CONST_INIT", "CONST_A", "CONST_B", "CONST_C"}
221def test_dont_crash_on_nested_functions_in_init() -> None:
222 """Assert we don't crash when visiting a nested function in `__init__` methods."""
223 with temporary_visited_module(
224 """
225 class C:
226 def __init__(self):
227 def pl(i: int):
228 return i + 1
229 """,
230 ) as module:
231 assert module
234def test_get_correct_docstring_starting_line_number() -> None:
235 """Assert we get the correct line numbers for docstring."""
236 with temporary_visited_module(
237 """
238 '''
239 Module docstring.
240 '''
241 class C:
242 '''
243 Class docstring.
244 '''
245 def method(self):
246 '''
247 Method docstring.
248 '''
249 """,
250 ) as module:
251 assert module.docstring.lineno == 2 # type: ignore[union-attr]
252 assert module["C"].docstring.lineno == 6
253 assert module["C.method"].docstring.lineno == 10
256def test_visit_properties_as_attributes() -> None:
257 """Assert properties are created as attributes and not functions."""
258 with temporary_visited_module(
259 """
260 from functools import cached_property
262 class C:
263 @property
264 def prop(self) -> bool:
265 return True
266 @cached_property
267 def cached_prop(self) -> int:
268 return 0
269 """,
270 ) as module:
271 assert module["C.prop"].is_attribute
272 assert "property" in module["C.prop"].labels
273 assert module["C.cached_prop"].is_attribute
274 assert "cached" in module["C.cached_prop"].labels
277def test_forward_docstrings() -> None:
278 """Assert docstrings of class attributes are forwarded to instance assignments.
280 This is a regression test for https://github.com/mkdocstrings/griffe/issues/128.
281 """
282 with temporary_visited_module(
283 '''
284 class C:
285 attr: int
286 """This is a non-empty docstring."""
288 def __init__(self, attr: int) -> None:
289 self.attr = attr
290 ''',
291 ) as module:
292 assert module["C.attr"].docstring
295def test_classvar_annotations() -> None:
296 """Assert class variable and instance variable annotations are correctly parsed and merged."""
297 with temporary_visited_module(
298 """
299 from typing import ClassVar
301 class C:
302 w: ClassVar[str] = "foo"
303 x: ClassVar[int]
304 y: str
305 z: int = 5
307 def __init__(self) -> None:
308 self.a: ClassVar[float]
309 self.y = ""
310 self.b: bytes
311 """,
312 ) as module:
313 assert module["C.w"].annotation.canonical_path == "str"
314 assert module["C.w"].labels == {"class-attribute"}
315 assert module["C.w"].value == "'foo'"
317 assert module["C.x"].annotation.canonical_path == "int"
318 assert module["C.x"].labels == {"class-attribute"}
320 assert module["C.y"].annotation.canonical_path == "str"
321 assert module["C.y"].labels == {"instance-attribute"}
322 assert module["C.y"].value == "''"
324 assert module["C.z"].annotation.canonical_path == "int"
325 assert module["C.z"].labels == {"class-attribute", "instance-attribute"}
326 assert module["C.z"].value == "5"
328 # This is syntactically valid, but semantically invalid
329 assert module["C.a"].annotation.canonical_path == "typing.ClassVar"
330 assert module["C.a"].annotation.slice.canonical_path == "float"
331 assert module["C.a"].labels == {"instance-attribute"}
333 assert module["C.b"].annotation.canonical_path == "bytes"
334 assert module["C.b"].labels == {"instance-attribute"}
337def test_visiting_if_statement_in_class_for_type_guards() -> None:
338 """Don't fail on various if statements when checking for type-guards."""
339 with temporary_visited_module(
340 """
341 class A:
342 if something("string1 string2"):
343 class B:
344 pass
345 """,
346 ) as module:
347 assert module["A.B"].runtime
350def test_visiting_relative_imports_triggering_cyclic_aliases() -> None:
351 """Skip specific imports to avoid cyclic aliases."""
352 with temporary_visited_package(
353 "pkg",
354 {
355 "__init__.py": "from . import a",
356 "a.py": "from . import b",
357 "b.py": "",
358 },
359 ) as pkg:
360 assert "a" not in pkg.imports
361 assert "b" in pkg["a"].imports
362 assert pkg["a"].imports["b"] == "pkg.b"
365def test_parse_attributes_in__all__() -> None:
366 """Parse attributes in `__all__`."""
367 with temporary_visited_package(
368 "package",
369 {
370 "__init__.py": "from package import module\n__all__ = module.__all__",
371 "module.py": "def hello(): ...\n__all__ = ['hello']",
372 },
373 ) as package:
374 assert "hello" in package.exports # type: ignore[operator]
377def test_parse_deep_attributes_in__all__() -> None:
378 """Parse deep attributes in `__all__`."""
379 with temporary_visited_package(
380 "package",
381 {
382 "__init__.py": "from package import subpackage\n__all__ = subpackage.module.__all__",
383 "subpackage/__init__.py": "from package.subpackage import module",
384 "subpackage/module.py": "def hello(): ...\n__all__ = ['hello']",
385 },
386 ) as package:
387 assert "hello" in package.exports # type: ignore[operator]