Coverage for tests/test_nodes.py: 98.96%

60 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-15 16:47 +0200

1"""Test nodes utilities.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6from ast import PyCF_ONLY_AST 

7 

8import pytest 

9 

10from griffe import Expr, ExprName, module_vtree, relative_to_absolute, temporary_visited_module 

11 

12syntax_examples = [ 

13 # operations 

14 "b + c", 

15 "b - c", 

16 "b * c", 

17 "b / c", 

18 "b // c", 

19 "b ** c", 

20 "b ^ c", 

21 "b & c", 

22 "b | c", 

23 "b @ c", 

24 "b % c", 

25 "b >> c", 

26 "b << c", 

27 # unary operations 

28 "+b", 

29 "-b", 

30 "~b", 

31 # comparisons 

32 "b == c", 

33 "b >= c", 

34 "b > c", 

35 "b <= c", 

36 "b < c", 

37 "b != c", 

38 # boolean logic 

39 "b and c", 

40 "b or c", 

41 "not b", 

42 # identify 

43 "b is c", 

44 "b is not c", 

45 # membership 

46 "b in c", 

47 "b not in c", 

48 # calls 

49 "call()", 

50 "call(something)", 

51 "call(something=something)", 

52 # strings 

53 "f'a {round(key, 2)} {z}'", 

54 # slices 

55 "o[x]", 

56 "o[x, y]", 

57 "o[x:y]", 

58 "o[x:y, z]", 

59 "o[x, y(z)]", 

60 # walrus operator 

61 "a if (a := b) else c", 

62 # starred 

63 "a(*b, **c)", 

64 # structs 

65 "(a, b, c)", 

66 "{a, b, c}", 

67 "{a: b, c: d}", 

68 "[a, b, c]", 

69 # yields 

70 "yield", 

71 "yield a", 

72 "yield from a", 

73 # lambdas 

74 "lambda a: a", 

75 "lambda a, b: a", 

76 "lambda *a, **b: a", 

77 "lambda a, b=0: a", 

78 "lambda a, /, b, c: a", 

79 "lambda a, *, b, c: a", 

80 "lambda a, /, b, *, c: a", 

81] 

82 

83 

84@pytest.mark.parametrize( 

85 ("code", "path", "is_package", "expected"), 

86 [ 

87 ("from . import b", "a", False, "a.b"), 

88 ("from . import b", "a", True, "a.b"), 

89 ("from . import c", "a.b", False, "a.c"), 

90 ("from . import c", "a.b", True, "a.b.c"), 

91 ("from . import d", "a.b.c", False, "a.b.d"), 

92 ("from .c import d", "a", False, "a.c.d"), 

93 ("from .c import d", "a.b", False, "a.c.d"), 

94 ("from .b import c", "a.b", True, "a.b.b.c"), 

95 ("from .. import e", "a.c.d.i", False, "a.c.e"), 

96 ("from ..d import e", "a.c.d.i", False, "a.c.d.e"), 

97 ("from ... import f", "a.c.d.i", False, "a.f"), 

98 ("from ...b import f", "a.c.d.i", False, "a.b.f"), 

99 ("from ...c.d import e", "a.c.d.i", False, "a.c.d.e"), 

100 ("from .c import *", "a", False, "a.c.*"), 

101 ("from .c import *", "a.b", False, "a.c.*"), 

102 ("from .b import *", "a.b", True, "a.b.b.*"), 

103 ("from .. import *", "a.c.d.i", False, "a.c.*"), 

104 ("from ..d import *", "a.c.d.i", False, "a.c.d.*"), 

105 ("from ... import *", "a.c.d.i", False, "a.*"), 

106 ("from ...b import *", "a.c.d.i", False, "a.b.*"), 

107 ("from ...c.d import *", "a.c.d.i", False, "a.c.d.*"), 

108 ], 

109) 

110def test_relative_to_absolute_imports(code: str, path: str, is_package: bool, expected: str) -> None: 

111 """Check if relative imports are correctly converted to absolute ones. 

112 

113 Parameters: 

114 code: The parametrized module code. 

115 path: The parametrized module path. 

116 is_package: Whether the module is a package (or subpackage) (parametrized). 

117 expected: The parametrized expected absolute path. 

118 """ 

119 node = compile(code, mode="exec", filename="<>", flags=PyCF_ONLY_AST).body[0] # type: ignore[attr-defined] 

120 module = module_vtree(path, leaf_package=is_package, return_leaf=True) 

121 for name in node.names: 

122 assert relative_to_absolute(node, name, module) == expected 

123 

124 

125def test_multipart_imports() -> None: 

126 """Assert that a multipart path like `a.b.c` imported as `x` points to the right target.""" 

127 with temporary_visited_module( 

128 """ 

129 import pkg.b.c 

130 import pkg.b.c as alias 

131 """, 

132 ) as module: 

133 pkg = module["pkg"] 

134 alias = module["alias"] 

135 assert pkg.target_path == "pkg" 

136 assert alias.target_path == "pkg.b.c" 

137 

138 

139@pytest.mark.parametrize( 

140 "expression", 

141 [ 

142 "A", 

143 "A.B", 

144 "A[B]", 

145 "A.B[C.D]", 

146 "~A", 

147 "A | B", 

148 "A[[B, C], D]", 

149 "A(b=c, d=1)", 

150 "A[-1, +2.3]", 

151 "A[B, C.D(e='syntax error')]", 

152 ], 

153) 

154def test_building_annotations_from_nodes(expression: str) -> None: 

155 """Test building annotations from AST nodes. 

156 

157 Parameters: 

158 expression: An expression (parametrized). 

159 """ 

160 class_defs = "\n\n".join(f"class {letter}: ..." for letter in "ABCD") 

161 with temporary_visited_module(f"{class_defs}\n\nx: {expression}\ny: {expression} = 0") as module: 

162 assert "x" in module.members 

163 assert "y" in module.members 

164 assert str(module["x"].annotation) == expression 

165 assert str(module["y"].annotation) == expression 

166 

167 

168@pytest.mark.parametrize("code", syntax_examples) 

169def test_building_expressions_from_nodes(code: str) -> None: 

170 """Test building annotations from AST nodes. 

171 

172 Parameters: 

173 code: An expression (parametrized). 

174 """ 

175 with temporary_visited_module(f"__z__ = {code}") as module: 

176 assert "__z__" in module.members 

177 

178 # make space after comma non-significant 

179 value = str(module["__z__"].value).replace(", ", ",") 

180 assert value == code.replace(", ", ",") 

181 

182 

183@pytest.mark.parametrize( 

184 ("code", "has_name"), 

185 [ 

186 ("import typing\nclass A: ...\na: typing.Literal['A']", False), 

187 ("from typing import Literal\nclass A: ...\na: Literal['A']", False), 

188 ("import typing_extensions\nclass A: ...\na: typing.Literal['A']", False), 

189 ("from typing_extensions import Literal\nclass A: ...\na: Literal['A']", False), 

190 ("from mod import A\na: 'A'", True), 

191 ("from mod import A\na: list['A']", True), 

192 ], 

193) 

194def test_forward_references(code: str, has_name: bool) -> None: 

195 """Check that we support forward references (type names as strings). 

196 

197 Parameters: 

198 code: Parametrized code. 

199 has_name: Whether the annotation should contain a Name rather than a string. 

200 """ 

201 with temporary_visited_module(code) as module: 

202 annotation = list(module["a"].annotation.iterate(flat=True)) 

203 if has_name: 

204 assert any(isinstance(item, ExprName) and item.name == "A" for item in annotation) 204 ↛ exitline 204 didn't finish the generator expression on line 204

205 assert all(not (isinstance(item, str) and item == "A") for item in annotation) 

206 else: 

207 assert "'A'" in annotation 

208 assert all(not (isinstance(item, ExprName) and item.name == "A") for item in annotation) 

209 

210 

211@pytest.mark.parametrize( 

212 "default", 

213 [ 

214 "1", 

215 "'test_string'", 

216 "dict(key=1)", 

217 "{'key': 1}", 

218 "DEFAULT_VALUE", 

219 "None", 

220 ], 

221) 

222def test_default_value_from_nodes(default: str) -> None: 

223 """Test getting default value from AST nodes. 

224 

225 Parameters: 

226 default: A default value (parametrized). 

227 """ 

228 module_defs = f"def f(x={default}):\n return x" 

229 with temporary_visited_module(module_defs) as module: 

230 assert "f" in module.members 

231 params = module.members["f"].parameters # type: ignore[union-attr] 

232 assert len(params) == 1 

233 assert str(params[0].default) == default 

234 

235 

236# https://github.com/mkdocstrings/griffe/issues/159 

237def test_parsing_complex_string_annotations() -> None: 

238 """Test parsing of complex, stringified annotations.""" 

239 with temporary_visited_module( 

240 """ 

241 class ArgsKwargs: 

242 def __init__(self, args: 'tuple[Any, ...]', kwargs: 'dict[str, Any] | None' = None) -> None: 

243 ... 

244 

245 @property 

246 def args(self) -> 'tuple[Any, ...]': 

247 ... 

248 

249 @property 

250 def kwargs(self) -> 'dict[str, Any] | None': 

251 ... 

252 """, 

253 ) as module: 

254 init_args_annotation = module["ArgsKwargs.__init__"].parameters["args"].annotation 

255 assert isinstance(init_args_annotation, Expr) 

256 assert init_args_annotation.is_tuple 

257 kwargs_return_annotation = module["ArgsKwargs.kwargs"].annotation 

258 assert isinstance(kwargs_return_annotation, Expr) 

259 

260 

261def test_parsing_dynamic_base_classes(caplog: pytest.LogCaptureFixture) -> None: 

262 """Assert parsing dynamic base classes does not trigger errors. 

263 

264 Parameters: 

265 caplog: Pytest fixture to capture logs. 

266 """ 

267 with caplog.at_level(logging.ERROR), temporary_visited_module( 

268 """ 

269 from collections import namedtuple 

270 class Thing(namedtuple('Thing', 'attr1 attr2')): 

271 ... 

272 """, 

273 ): 

274 pass 

275 assert not caplog.records