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

1"""Test inspection mechanisms.""" 

2 

3from __future__ import annotations 

4 

5import sys 

6 

7import pytest 

8 

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 

18 

19 

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" 

27 

28 

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" 

40 

41 

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 

63 

64 

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 

86 

87 

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" 

102 

103 

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 

111 

112 

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 

121 

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 

135 

136 

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" 

142 

143 

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" 

149 

150 

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 

155 

156 

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") 

163 

164 

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") 

181 

182 

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 

188 

189 ast = inspect("_ast") 

190 assert "Assign" in ast.members 

191 assert not ast["Assign"].is_alias 

192 

193 

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" 

209 

210 

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 

222 

223 

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): ... 

230 

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"] 

246 

247 

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 

267 

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 

285 

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]" 

296 

297 

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" 

316 

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 

334 

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]"