Coverage for tests/test_visitor.py: 100.00%
178 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-11 13:44 +0200
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-11 13:44 +0200
1"""Test visit mechanisms."""
3from __future__ import annotations
5import sys
6from textwrap import dedent
8import pytest
10from griffe import (
11 Expr,
12 GriffeLoader,
13 TypeParameterKind,
14 temporary_pypackage,
15 temporary_visited_module,
16 temporary_visited_package,
17)
20def test_not_defined_at_runtime() -> None:
21 """Assert that objects not defined at runtime are not added to wildcards expansions."""
22 with temporary_pypackage("package", ["module_a.py", "module_b.py", "module_c.py"]) as tmp_package:
23 tmp_package.path.joinpath("__init__.py").write_text("from package.module_a import *")
24 tmp_package.path.joinpath("module_a.py").write_text(
25 dedent(
26 """
27 import typing
28 from typing import TYPE_CHECKING
30 from package.module_b import CONST_B
31 from package.module_c import CONST_C
33 if typing.TYPE_CHECKING: # Always false.
34 from package.module_b import TYPE_B
35 if TYPE_CHECKING: # Always false.
36 from package.module_c import TYPE_C
37 """,
38 ),
39 )
40 tmp_package.path.joinpath("module_b.py").write_text("CONST_B = 'hi'\nTYPE_B = str")
41 tmp_package.path.joinpath("module_c.py").write_text("CONST_C = 'ho'\nTYPE_C = str")
42 loader = GriffeLoader(search_paths=[tmp_package.tmpdir])
43 package = loader.load(tmp_package.name)
44 loader.resolve_aliases()
45 assert "CONST_B" in package.members
46 assert "CONST_C" in package.members
47 assert "TYPE_B" not in package.members
48 assert "TYPE_C" not in package.members
51@pytest.mark.parametrize(
52 ("decorator", "labels"),
53 [
54 ("property", ("property",)),
55 ("staticmethod", ("staticmethod",)),
56 ("classmethod", ("classmethod",)),
57 ("functools.cache", ("cached",)),
58 ("cache", ("cached",)),
59 ("functools.cached_property", ("cached", "property")),
60 ("cached_property", ("cached", "property")),
61 ("functools.lru_cache", ("cached",)),
62 ("functools.lru_cache(maxsize=8)", ("cached",)),
63 ("lru_cache", ("cached",)),
64 ("lru_cache(maxsize=8)", ("cached",)),
65 ("abc.abstractmethod", ("abstractmethod",)),
66 ("abstractmethod", ("abstractmethod",)),
67 ("dataclasses.dataclass", ("dataclass",)),
68 ("dataclass", ("dataclass",)),
69 ],
70)
71def test_set_function_labels_using_decorators(decorator: str, labels: tuple[str, ...]) -> None:
72 """Assert decorators are used to set labels on functions.
74 Parameters:
75 decorator: A parametrized decorator.
76 labels: The parametrized tuple of expected labels.
77 """
78 code = f"""
79 import abc
80 import dataclasses
81 import functools
82 from abc import abstractmethod
83 from dataclasses import dataclass
84 from functools import cache, cached_property, lru_cache
86 class A:
87 @{decorator}
88 def f(self):
89 return 0
90 """
91 with temporary_visited_module(code) as module:
92 assert module["A.f"].has_labels(*labels)
95@pytest.mark.parametrize(
96 ("decorator", "labels"),
97 [
98 ("dataclasses.dataclass", ("dataclass",)),
99 ("dataclass", ("dataclass",)),
100 ],
101)
102def test_set_class_labels_using_decorators(decorator: str, labels: tuple[str, ...]) -> None:
103 """Assert decorators are used to set labels on classes.
105 Parameters:
106 decorator: A parametrized decorator.
107 labels: The parametrized tuple of expected labels.
108 """
109 code = f"""
110 import dataclasses
111 from dataclasses import dataclass
113 @{decorator}
114 class A: ...
115 """
116 with temporary_visited_module(code) as module:
117 assert module["A"].has_labels(*labels)
120def test_handle_property_setter_and_deleter() -> None:
121 """Assert property setters and deleters are supported."""
122 code = """
123 class A:
124 def __init__(self): self._thing = 0
126 @property
127 def thing(self): return self._thing
129 @thing.setter
130 def thing(self, value): self._thing = value
132 @thing.deleter
133 def thing(self): del self._thing
134 """
135 with temporary_visited_module(code) as module:
136 assert module["A.thing"].has_labels("property", "writable", "deletable")
137 assert module["A.thing"].setter.is_function
138 assert module["A.thing"].deleter.is_function
141@pytest.mark.parametrize(
142 "decorator",
143 [
144 "overload",
145 "typing.overload",
146 ],
147)
148def test_handle_typing_overaload(decorator: str) -> None:
149 """Assert `typing.overload` is supported.
151 Parameters:
152 decorator: A parametrized overload decorator.
153 """
154 code = f"""
155 import typing
156 from typing import overload
157 from pathlib import Path
159 class A:
160 @{decorator}
161 def absolute(self, path: str) -> str:
162 ...
164 @{decorator}
165 def absolute(self, path: Path) -> Path:
166 ...
168 def absolute(self, path: str | Path) -> str | Path:
169 ...
170 """
171 with temporary_visited_module(code) as module:
172 overloads = module["A.absolute"].overloads
173 assert len(overloads) == 2
174 assert overloads[0].parameters["path"].annotation.name == "str"
175 assert overloads[1].parameters["path"].annotation.name == "Path"
176 assert overloads[0].returns.name == "str"
177 assert overloads[1].returns.name == "Path"
180@pytest.mark.parametrize(
181 "statements",
182 [
183 """__all__ = ["CONST_INIT"] + moda_all + modb_all + modc_all""",
184 """__all__ = ["CONST_INIT", *moda_all, *modb_all, *modc_all]""",
185 """
186 __all__ = ["CONST_INIT"]
187 __all__ += moda_all + modb_all + modc_all
188 """,
189 """
190 __all__ = ["CONST_INIT"] + moda_all + modb_all
191 __all__ += modc_all
192 """,
193 """
194 __all__ = ["CONST_INIT"] + moda_all + modb_all
195 __all__ += [*modc_all]
196 """,
197 """
198 __all__ = ["CONST_INIT"]
199 __all__ += moda_all
200 __all__ += modb_all + modc_all
201 """,
202 ],
203)
204def test_parse_complex__all__assignments(statements: str) -> None:
205 """Check our ability to expand exports based on `__all__` [augmented] assignments.
207 Parameters:
208 statements: Parametrized text containing `__all__` [augmented] assignments.
209 """
210 with temporary_pypackage("package", ["moda.py", "modb.py", "modc.py"]) as tmp_package:
211 tmp_package.path.joinpath("moda.py").write_text("CONST_A = 1\n\n__all__ = ['CONST_A']")
212 tmp_package.path.joinpath("modb.py").write_text("CONST_B = 1\n\n__all__ = ['CONST_B']")
213 tmp_package.path.joinpath("modc.py").write_text("CONST_C = 2\n\n__all__ = ['CONST_C']")
214 code = """
215 from package.moda import *
216 from package.moda import __all__ as moda_all
217 from package.modb import *
218 from package.modb import __all__ as modb_all
219 from package.modc import *
220 from package.modc import __all__ as modc_all
222 CONST_INIT = 0
223 """
224 tmp_package.path.joinpath("__init__.py").write_text(dedent(code) + dedent(statements))
226 loader = GriffeLoader(search_paths=[tmp_package.tmpdir])
227 package = loader.load(tmp_package.name)
228 loader.resolve_aliases()
230 assert package.exports == ["CONST_INIT", "CONST_A", "CONST_B", "CONST_C"]
233def test_dont_crash_on_nested_functions_in_init() -> None:
234 """Assert we don't crash when visiting a nested function in `__init__` methods."""
235 with temporary_visited_module(
236 """
237 class C:
238 def __init__(self):
239 def pl(i: int):
240 return i + 1
241 """,
242 ) as module:
243 assert module
246def test_get_correct_docstring_starting_line_number() -> None:
247 """Assert we get the correct line numbers for docstring."""
248 with temporary_visited_module(
249 """
250 '''
251 Module docstring.
252 '''
253 class C:
254 '''
255 Class docstring.
256 '''
257 def method(self):
258 '''
259 Method docstring.
260 '''
261 """,
262 ) as module:
263 assert module.docstring.lineno == 2 # type: ignore[union-attr]
264 assert module["C"].docstring.lineno == 6
265 assert module["C.method"].docstring.lineno == 10
268def test_visit_properties_as_attributes() -> None:
269 """Assert properties are created as attributes and not functions."""
270 with temporary_visited_module(
271 """
272 from functools import cached_property
274 class C:
275 @property
276 def prop(self) -> bool:
277 return True
278 @cached_property
279 def cached_prop(self) -> int:
280 return 0
281 """,
282 ) as module:
283 assert module["C.prop"].is_attribute
284 assert "property" in module["C.prop"].labels
285 assert module["C.cached_prop"].is_attribute
286 assert "cached" in module["C.cached_prop"].labels
289def test_forward_docstrings() -> None:
290 """Assert docstrings of class attributes are forwarded to instance assignments.
292 This is a regression test for https://github.com/mkdocstrings/griffe/issues/128.
293 """
294 with temporary_visited_module(
295 '''
296 class C:
297 attr: int
298 """This is a non-empty docstring."""
300 def __init__(self, attr: int) -> None:
301 self.attr = attr
302 ''',
303 ) as module:
304 assert module["C.attr"].docstring
307def test_classvar_annotations() -> None:
308 """Assert class variable and instance variable annotations are correctly parsed and merged."""
309 with temporary_visited_module(
310 """
311 from typing import ClassVar
313 class C:
314 w: ClassVar[str] = "foo"
315 x: ClassVar[int]
316 y: str
317 z: int = 5
319 def __init__(self) -> None:
320 self.a: ClassVar[float]
321 self.y = ""
322 self.b: bytes
323 """,
324 ) as module:
325 assert module["C.w"].annotation.canonical_path == "str"
326 assert module["C.w"].labels == {"class-attribute"}
327 assert module["C.w"].value == "'foo'"
329 assert module["C.x"].annotation.canonical_path == "int"
330 assert module["C.x"].labels == {"class-attribute"}
332 assert module["C.y"].annotation.canonical_path == "str"
333 assert module["C.y"].labels == {"instance-attribute"}
334 assert module["C.y"].value == "''"
336 assert module["C.z"].annotation.canonical_path == "int"
337 assert module["C.z"].labels == {"class-attribute", "instance-attribute"}
338 assert module["C.z"].value == "5"
340 # This is syntactically valid, but semantically invalid
341 assert module["C.a"].annotation.canonical_path == "typing.ClassVar"
342 assert module["C.a"].annotation.slice.canonical_path == "float"
343 assert module["C.a"].labels == {"instance-attribute"}
345 assert module["C.b"].annotation.canonical_path == "bytes"
346 assert module["C.b"].labels == {"instance-attribute"}
349def test_visiting_if_statement_in_class_for_type_guards() -> None:
350 """Don't fail on various if statements when checking for type-guards."""
351 with temporary_visited_module(
352 """
353 class A:
354 if something("string1 string2"):
355 class B:
356 pass
357 """,
358 ) as module:
359 assert module["A.B"].runtime
362def test_visiting_relative_imports_triggering_cyclic_aliases() -> None:
363 """Skip specific imports to avoid cyclic aliases."""
364 with temporary_visited_package(
365 "pkg",
366 {
367 "__init__.py": "from . import a",
368 "a.py": "from . import b",
369 "b.py": "",
370 },
371 ) as pkg:
372 assert "a" not in pkg.imports
373 assert "b" in pkg["a"].imports
374 assert pkg["a"].imports["b"] == "pkg.b"
377def test_parse_attributes_in__all__() -> None:
378 """Parse attributes in `__all__`."""
379 with temporary_visited_package(
380 "package",
381 {
382 "__init__.py": "from package import module\n__all__ = module.__all__",
383 "module.py": "def hello(): ...\n__all__ = ['hello']",
384 },
385 ) as package:
386 assert "hello" in package.exports # type: ignore[operator]
389def test_parse_deep_attributes_in__all__() -> None:
390 """Parse deep attributes in `__all__`."""
391 with temporary_visited_package(
392 "package",
393 {
394 "__init__.py": "from package import subpackage\n__all__ = subpackage.module.__all__",
395 "subpackage/__init__.py": "from package.subpackage import module",
396 "subpackage/module.py": "def hello(): ...\n__all__ = ['hello']",
397 },
398 ) as package:
399 assert "hello" in package.exports # type: ignore[operator]
402# YORE: EOL 3.12: Remove block.
403# YORE: EOL 3.11: Remove line.
404@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics")
405def test_parse_pep695_generics_without_defaults() -> None:
406 """Assert PEP 695 generics are correctly inspected."""
407 with temporary_visited_module(
408 """
409 class Class[X: Exception]: pass
410 def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
411 type TA[T: (int, str)] = dict[str, T]
412 """,
413 ) as module:
414 class_ = module["Class"]
415 assert class_.is_class
416 assert class_.type_parameters[0].name == "X"
417 assert class_.type_parameters[0].kind == TypeParameterKind.type_var
418 assert class_.type_parameters[0].bound.name == "Exception"
419 assert not class_.type_parameters[0].constraints
420 assert class_.type_parameters[0].default is None
422 func = module["func"]
423 assert func.is_function
424 assert func.type_parameters[0].name == "P"
425 assert func.type_parameters[0].kind == TypeParameterKind.param_spec
426 assert func.type_parameters[0].bound is None
427 assert not func.type_parameters[0].constraints
428 assert func.type_parameters[0].default is None
429 assert func.type_parameters[1].name == "T"
430 assert func.type_parameters[1].kind == TypeParameterKind.type_var
431 assert func.type_parameters[1].bound is None
432 assert not func.type_parameters[1].constraints
433 assert func.type_parameters[1].default is None
434 assert func.type_parameters[2].name == "R"
435 assert func.type_parameters[2].kind == TypeParameterKind.type_var_tuple
436 assert func.type_parameters[2].bound is None
437 assert not func.type_parameters[2].constraints
438 assert func.type_parameters[2].default is None
440 type_alias = module["TA"]
441 assert type_alias.is_type_alias
442 assert type_alias.type_parameters[0].name == "T"
443 assert type_alias.type_parameters[0].kind == TypeParameterKind.type_var
444 assert type_alias.type_parameters[0].bound is None
445 assert type_alias.type_parameters[0].constraints[0].name == "int"
446 assert type_alias.type_parameters[0].constraints[1].name == "str"
447 assert type_alias.type_parameters[0].default is None
448 assert isinstance(type_alias.value, Expr)
449 assert str(type_alias.value) == "dict[str, T]"
452# YORE: EOL 3.12: Remove line.
453@pytest.mark.skipif(sys.version_info < (3, 13), reason="Python less than 3.13 does not have defaults in PEP 695 generics") # fmt: skip
454def test_parse_pep695_generics() -> None:
455 """Assert PEP 695 generics are correctly parsed."""
456 with temporary_visited_module(
457 """
458 class Class[X: Exception = OSError]: pass
459 def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
460 type TA[T: (int, str) = str] = dict[str, T]
461 """,
462 ) as module:
463 class_ = module["Class"]
464 assert class_.is_class
465 assert class_.type_parameters[0].name == "X"
466 assert class_.type_parameters[0].kind == TypeParameterKind.type_var
467 assert class_.type_parameters[0].bound.name == "Exception"
468 assert not class_.type_parameters[0].constraints
469 assert class_.type_parameters[0].default.name == "OSError"
471 func = module["func"]
472 assert func.is_function
473 assert func.type_parameters[0].name == "P"
474 assert func.type_parameters[0].kind == TypeParameterKind.param_spec
475 assert func.type_parameters[0].bound is None
476 assert not func.type_parameters[0].constraints
477 assert func.type_parameters[0].default is None
478 assert func.type_parameters[1].name == "T"
479 assert func.type_parameters[1].kind == TypeParameterKind.type_var
480 assert func.type_parameters[1].bound is None
481 assert not func.type_parameters[1].constraints
482 assert func.type_parameters[1].default is None
483 assert func.type_parameters[2].name == "R"
484 assert func.type_parameters[2].kind == TypeParameterKind.type_var_tuple
485 assert func.type_parameters[2].bound is None
486 assert not func.type_parameters[2].constraints
487 assert func.type_parameters[2].default is None
489 type_alias = module["TA"]
490 assert type_alias.is_type_alias
491 assert type_alias.type_parameters[0].name == "T"
492 assert type_alias.type_parameters[0].kind == TypeParameterKind.type_var
493 assert type_alias.type_parameters[0].bound is None
494 assert type_alias.type_parameters[0].constraints[0].name == "int"
495 assert type_alias.type_parameters[0].constraints[1].name == "str"
496 assert type_alias.type_parameters[0].default.name == "str"
497 assert isinstance(type_alias.value, Expr)
498 assert str(type_alias.value) == "dict[str, T]"