Coverage for tests/test_inspector.py: 97.73%
176 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 inspection mechanisms."""
3from __future__ import annotations
5import sys
7import pytest
9from griffe import (
10 Expr,
11 TypeParameterKind,
12 inspect,
13 temporary_inspected_module,
14 temporary_inspected_package,
15 temporary_pypackage,
16)
17from tests.helpers import clear_sys_modules
20def test_annotations_from_builtin_types() -> None:
21 """Assert builtin types are correctly transformed to annotations."""
22 with temporary_inspected_module("def func(a: int) -> str: pass") as module:
23 func = module["func"]
24 assert func.parameters[0].name == "a"
25 assert func.parameters[0].annotation.name == "int"
26 assert func.returns.name == "str"
29def test_annotations_from_classes() -> None:
30 """Assert custom classes are correctly transformed to annotations."""
31 with temporary_inspected_module("class A: pass\ndef func(a: A) -> A: pass") as module:
32 func = module["func"]
33 assert func.parameters[0].name == "a"
34 param = func.parameters[0].annotation
35 assert param.name == "A"
36 assert param.canonical_path == f"{module.name}.A"
37 returns = func.returns
38 assert returns.name == "A"
39 assert returns.canonical_path == f"{module.name}.A"
42# YORE: EOL 3.13: Remove block.
43# YORE: EOL 3.9: Remove line.
44@pytest.mark.skipif(sys.version_info < (3, 10), reason="Type unions not supported on 3.9")
45@pytest.mark.skipif(sys.version_info >= (3, 14), reason="3.14 changes type annotations, see test below")
46@pytest.mark.parametrize(
47 ("annotation", "expected"),
48 [
49 ("tuple[int, str]", "tuple[int, str]"),
50 ("Union[int, str]", "typing.Union[int, str]"),
51 ("int | str", "int | str"),
52 ("int | Literal[1]", "typing.Union[int, typing.Literal[1]]"),
53 ],
54)
55def test_annotations_from_types_before_314(annotation: str, expected: str) -> None:
56 """Assert annotations are correctly converted to string."""
57 with temporary_inspected_module(
58 f"""
59 from typing import Literal, Union
60 def func(param: {annotation}): ...
61 """,
62 ) as module:
63 param = module["func"].parameters["param"]
64 assert str(param.annotation) == expected
67# YORE: EOL 3.13: Remove line.
68@pytest.mark.skipif(sys.version_info < (3, 14), reason="3.14 modernizes type unions")
69@pytest.mark.parametrize(
70 ("annotation", "expected"),
71 [
72 ("tuple[int, str]", "tuple[int, str]"),
73 ("Union[int, str]", "int | str"),
74 ("int | str", "int | str"),
75 ("int | Literal[1]", "int | typing.Literal[1]"),
76 ],
77)
78def test_annotations_from_types(annotation: str, expected: str) -> None:
79 """Assert annotations are correctly converted to string."""
80 with temporary_inspected_module(
81 f"""
82 from typing import Literal, Union
83 def func(param: {annotation}): ...
84 """,
85 ) as module:
86 param = module["func"].parameters["param"]
87 assert str(param.annotation) == expected
90def test_class_level_imports() -> None:
91 """Assert annotations using class-level imports are resolved."""
92 with temporary_inspected_module(
93 """
94 class A:
95 from io import StringIO
96 def method(self, p: StringIO):
97 pass
98 """,
99 ) as module:
100 method = module["A.method"]
101 name = method.parameters["p"].annotation
102 assert name.name == "StringIO"
103 assert name.canonical_path == "io.StringIO"
106def test_missing_dependency() -> None:
107 """Assert missing dependencies are handled during dynamic imports."""
108 with (
109 pytest.raises(ImportError, match="ModuleNotFoundError: No module named 'missing'"),
110 temporary_inspected_module("import missing"),
111 ):
112 pass
115def test_inspect_properties_as_attributes() -> None:
116 """Assert properties are created as attributes and not functions."""
117 with temporary_inspected_module(
118 """
119 try:
120 from functools import cached_property
121 except ImportError:
122 from cached_property import cached_property
124 class C:
125 @property
126 def prop(self) -> bool:
127 return True
128 @cached_property
129 def cached_prop(self) -> int:
130 return 0
131 """,
132 ) as module:
133 assert module["C.prop"].is_attribute
134 assert "property" in module["C.prop"].labels
135 assert module["C.cached_prop"].is_attribute
136 assert "cached" in module["C.cached_prop"].labels
139def test_inspecting_module_importing_other_module() -> None:
140 """Assert aliases to modules are correctly inspected and aliased."""
141 with temporary_inspected_module("import itertools as it") as module:
142 assert module["it"].is_alias
143 assert module["it"].target_path == "itertools"
146def test_inspecting_parameters_with_functions_as_default_values() -> None:
147 """Assert functions as default parameter values are serialized with their name."""
148 with temporary_inspected_module("def func(): ...\ndef other_func(f=func): ...") as module:
149 default = module["other_func"].parameters["f"].default
150 assert default == "func"
153def test_inspecting_package_and_module_with_same_names() -> None:
154 """Package and module having same name shouldn't cause issues."""
155 with temporary_inspected_package("package", {"package.py": "a = 0"}):
156 pass
159def test_inspecting_module_with_submodules() -> None:
160 """Inspecting a module shouldn't register any of its submodules if they're not imported."""
161 with temporary_pypackage("pkg", ["mod.py"]) as tmp_package:
162 pkg = inspect("pkg", filepath=tmp_package.path / "__init__.py")
163 assert "mod" not in pkg.members
164 clear_sys_modules("pkg")
167def test_inspecting_module_with_imported_submodules() -> None:
168 """When inspecting a package on the disk, direct submodules should be skipped entirely."""
169 with temporary_pypackage(
170 "pkg",
171 {
172 "__init__.py": "from pkg import subpkg\nfrom pkg.subpkg import mod",
173 "subpkg/__init__.py": "a = 0",
174 "subpkg/mod.py": "b = 0",
175 },
176 ) as tmp_package:
177 pkg = inspect("pkg", filepath=tmp_package.path / "__init__.py")
178 assert "subpkg" not in pkg.members
179 assert "mod" in pkg.members
180 assert pkg["mod"].is_alias
181 assert pkg["mod"].target_path == "pkg.subpkg.mod"
182 clear_sys_modules("pkg")
185def test_inspecting_objects_from_private_builtin_stdlib_moduless() -> None:
186 """Inspect objects from private built-in modules in the standard library."""
187 ast = inspect("ast")
188 assert "Assign" in ast.members
189 assert not ast["Assign"].is_alias
191 ast = inspect("_ast")
192 assert "Assign" in ast.members
193 assert not ast["Assign"].is_alias
196def test_inspecting_partials_as_functions() -> None:
197 """Assert partials are correctly inspected as functions."""
198 with temporary_inspected_module(
199 """
200 from functools import partial
201 def func(a: int, b: int) -> int: pass
202 partial_func = partial(func, 1)
203 partial_func.__module__ = __name__
204 """,
205 ) as module:
206 partial_func = module["partial_func"]
207 assert partial_func.is_function
208 assert partial_func.parameters[0].name == "b"
209 assert partial_func.parameters[0].annotation.name == "int"
210 assert partial_func.returns.name == "int"
213def test_inspecting_class_instance() -> None:
214 """Assert class instances are correctly inspected."""
215 with temporary_inspected_package(
216 "pkg",
217 {
218 "__init__.py": "",
219 "foo.py": "from . import bar\nx = bar.X()",
220 "bar.py": "class X: pass",
221 },
222 ) as tmp_package:
223 assert not tmp_package["foo.x"].is_alias
226def test_inheriting_self_from_parent_class() -> None:
227 """Inspect self only once when inheriting it from parent class."""
228 with temporary_inspected_module(
229 """
230 class A: ...
231 class B(A): ...
233 A.B = B
234 """,
235 ) as module:
236 assert "B" in module["A"].members
237 assert "B" in module["B"].all_members
238 # Continue indefinitely.
239 assert "B" in module["A.B"].all_members
240 assert "B" in module["B.B"].all_members
241 assert "B" in module["A.B.B"].all_members
242 assert "B" in module["B.B.B"].all_members
243 # All resolve to A.B.
244 assert module["A.B.B"].final_target is module["A.B"]
245 assert module["B.B.B"].final_target is module["A.B"]
246 assert module["A.B.B.B"].final_target is module["A.B"]
247 assert module["B.B.B.B"].final_target is module["A.B"]
250# YORE: EOL 3.12: Remove block.
251# YORE: EOL 3.11: Remove line.
252@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics")
253def test_inspecting_pep695_generics_without_defaults() -> None:
254 """Assert PEP 695 generics are correctly inspected."""
255 with temporary_inspected_module(
256 """
257 class Class[X: Exception]: pass
258 def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
259 type TA[T: (int, str)] = dict[str, T]
260 """,
261 ) as module:
262 class_ = module["Class"]
263 assert class_.is_class
264 assert class_.type_parameters[0].name == "X"
265 assert class_.type_parameters[0].kind == TypeParameterKind.type_var
266 assert class_.type_parameters[0].bound.name == "Exception"
267 assert not class_.type_parameters[0].constraints
268 assert class_.type_parameters[0].default is None
270 func = module["func"]
271 assert func.is_function
272 assert func.type_parameters[0].name == "P"
273 assert func.type_parameters[0].kind == TypeParameterKind.param_spec
274 assert func.type_parameters[0].bound is None
275 assert not func.type_parameters[0].constraints
276 assert func.type_parameters[0].default is None
277 assert func.type_parameters[1].name == "T"
278 assert func.type_parameters[1].kind == TypeParameterKind.type_var
279 assert func.type_parameters[1].bound is None
280 assert not func.type_parameters[1].constraints
281 assert func.type_parameters[1].default is None
282 assert func.type_parameters[2].name == "R"
283 assert func.type_parameters[2].kind == TypeParameterKind.type_var_tuple
284 assert func.type_parameters[2].bound is None
285 assert not func.type_parameters[2].constraints
286 assert func.type_parameters[2].default is None
288 type_alias = module["TA"]
289 assert type_alias.is_type_alias
290 assert type_alias.type_parameters[0].name == "T"
291 assert type_alias.type_parameters[0].kind == TypeParameterKind.type_var
292 assert type_alias.type_parameters[0].bound is None
293 assert type_alias.type_parameters[0].constraints[0].name == "int"
294 assert type_alias.type_parameters[0].constraints[1].name == "str"
295 assert type_alias.type_parameters[0].default is None
296 assert isinstance(type_alias.value, Expr)
297 assert str(type_alias.value) == "dict[str, T]"
300# YORE: EOL 3.12: Remove line.
301@pytest.mark.skipif(sys.version_info < (3, 13), reason="Python less than 3.13 does not have defaults in PEP 695 generics") # fmt: skip
302def test_inspecting_pep695_generics() -> None:
303 """Assert PEP 695 generics are correctly inspected."""
304 with temporary_inspected_module(
305 """
306 class Class[X: Exception = OSError]: pass
307 def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
308 type TA[T: (int, str) = str] = dict[str, T]
309 """,
310 ) as module:
311 class_ = module["Class"]
312 assert class_.is_class
313 assert class_.type_parameters[0].name == "X"
314 assert class_.type_parameters[0].kind == TypeParameterKind.type_var
315 assert class_.type_parameters[0].bound.name == "Exception"
316 assert not class_.type_parameters[0].constraints
317 assert class_.type_parameters[0].default.name == "OSError"
319 func = module["func"]
320 assert func.is_function
321 assert func.type_parameters[0].name == "P"
322 assert func.type_parameters[0].kind == TypeParameterKind.param_spec
323 assert func.type_parameters[0].bound is None
324 assert not func.type_parameters[0].constraints
325 assert func.type_parameters[0].default is None
326 assert func.type_parameters[1].name == "T"
327 assert func.type_parameters[1].kind == TypeParameterKind.type_var
328 assert func.type_parameters[1].bound is None
329 assert not func.type_parameters[1].constraints
330 assert func.type_parameters[1].default is None
331 assert func.type_parameters[2].name == "R"
332 assert func.type_parameters[2].kind == TypeParameterKind.type_var_tuple
333 assert func.type_parameters[2].bound is None
334 assert not func.type_parameters[2].constraints
335 assert func.type_parameters[2].default is None
337 type_alias = module["TA"]
338 assert type_alias.is_type_alias
339 assert type_alias.type_parameters[0].name == "T"
340 assert type_alias.type_parameters[0].kind == TypeParameterKind.type_var
341 assert type_alias.type_parameters[0].bound is None
342 assert type_alias.type_parameters[0].constraints[0].name == "int"
343 assert type_alias.type_parameters[0].constraints[1].name == "str"
344 assert type_alias.type_parameters[0].default.name == "str"
345 assert isinstance(type_alias.value, Expr)
346 assert str(type_alias.value) == "dict[str, T]"