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

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# 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 

65 

66 

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 

88 

89 

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" 

104 

105 

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 

113 

114 

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 

123 

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 

137 

138 

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" 

144 

145 

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" 

151 

152 

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 

157 

158 

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

165 

166 

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

183 

184 

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 

190 

191 ast = inspect("_ast") 

192 assert "Assign" in ast.members 

193 assert not ast["Assign"].is_alias 

194 

195 

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" 

211 

212 

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 

224 

225 

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

232 

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

248 

249 

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 

269 

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 

287 

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

298 

299 

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" 

318 

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 

336 

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