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