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