Coverage for tests/test_expressions.py: 100.00%

64 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-11 13:44 +0200

1"""Test names and expressions methods.""" 

2 

3from __future__ import annotations 

4 

5import ast 

6import sys 

7 

8import pytest 

9 

10from griffe import Module, Parser, get_expression, temporary_visited_module 

11from tests.test_nodes import syntax_examples 

12 

13 

14@pytest.mark.parametrize( 

15 ("annotation", "items"), 

16 [ 

17 ("tuple[int, float] | None", 2), 

18 ("None | tuple[int, float]", 2), 

19 ("Optional[tuple[int, float]]", 2), 

20 ("typing.Optional[tuple[int, float]]", 2), 

21 ], 

22) 

23def test_explode_return_annotations(annotation: str, items: int) -> None: 

24 """Check that we correctly split items from return annotations. 

25 

26 Parameters: 

27 annotation: The return annotation. 

28 items: The number of items to write in the docstring returns section. 

29 """ 

30 newline = "\n " 

31 returns = newline.join(f"x{_}: Some value." for _ in range(items)) 

32 code = f""" 

33 import typing 

34 from typing import Optional 

35 

36 def function() -> {annotation}: 

37 '''This function returns either two ints or None 

38 

39 Returns: 

40 {returns} 

41 ''' 

42 """ 

43 with temporary_visited_module(code) as module: 

44 sections = module["function"].docstring.parse(Parser.google) 

45 assert sections[1].value 

46 

47 

48@pytest.mark.parametrize( 

49 "annotation", 

50 [ 

51 "int", 

52 "tuple[int]", 

53 "dict[str, str]", 

54 "Optional[tuple[int, float]]", 

55 ], 

56) 

57def test_full_expressions(annotation: str) -> None: 

58 """Assert we can transform expressions to their full form without errors.""" 

59 code = f"x: {annotation}" 

60 with temporary_visited_module(code) as module: 

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

62 

63 

64def test_resolving_full_names() -> None: 

65 """Assert expressions are correctly transformed to their fully-resolved form.""" 

66 with temporary_visited_module( 

67 """ 

68 from package import module 

69 attribute1: module.Class 

70 

71 from package import module as mod 

72 attribute2: mod.Class 

73 """, 

74 ) as module: 

75 assert module["attribute1"].annotation.canonical_path == "package.module.Class" 

76 assert module["attribute2"].annotation.canonical_path == "package.module.Class" 

77 

78 

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

80def test_expressions(code: str) -> None: 

81 """Test building annotations from AST nodes. 

82 

83 Parameters: 

84 code: An expression (parametrized). 

85 """ 

86 top_node = compile(code, filename="<>", mode="exec", flags=ast.PyCF_ONLY_AST, optimize=2) 

87 expression = get_expression(top_node.body[0].value, parent=Module("module")) # type: ignore[attr-defined] 

88 assert str(expression) == code 

89 

90 

91def test_length_one_tuple_as_string() -> None: 

92 """Length-1 tuples must have a trailing comma.""" 

93 code = "x = ('a',)" 

94 with temporary_visited_module(code) as module: 

95 assert str(module["x"].value) == "('a',)" 

96 

97 

98@pytest.mark.parametrize( 

99 ("annotation", "modernized"), 

100 [ 

101 ("Union[str, int, float]", "str | int | float"), 

102 ("typing.Union[str, int, float]", "str | int | float"), 

103 ("Union[Tuple[str, ...], Dict[str, int]]", "tuple[str, ...] | dict[str, int]"), 

104 ("typing.Union[typing.Tuple[str, ...], typing.Dict[str, int]]", "tuple[str, ...] | dict[str, int]"), 

105 ("Tuple[List[Dict[str, Set[str]]]]", "tuple[list[dict[str, set[str]]]]"), 

106 ("typing.Tuple[typing.List[typing.Dict[str, typing.Set[str]]]]", "tuple[list[dict[str, set[str]]]]"), 

107 ("Optional[Tuple[List[bool]]]", "tuple[list[bool]] | None"), 

108 ("typing.Optional[typing.Tuple[typing.List[bool]]]", "tuple[list[bool]] | None"), 

109 ], 

110) 

111def test_modernizing_specific_expressions(annotation: str, modernized: str) -> None: 

112 """Modernize expressions correctly. 

113 

114 Parameters: 

115 annotation: Original annotation (parametrized). 

116 modernized: Expected modernized annotation (parametrized). 

117 """ 

118 with temporary_visited_module( 

119 f""" 

120 import typing 

121 from typing import Union, Optional, Tuple, Dict, List, Set, Literal 

122 a: {annotation} 

123 """, 

124 ) as module: 

125 expression = module["a"].annotation 

126 assert str(expression.modernize()) == modernized 

127 

128 

129@pytest.mark.parametrize( 

130 "annotation", 

131 [ 

132 "typing.Literal['s']", 

133 "Literal['s']", 

134 ], 

135) 

136def test_handling_modernization_without_crashing(annotation: str) -> None: 

137 """Modernizing expressions never crashes. 

138 

139 Parameters: 

140 annotation: Original annotation (parametrized). 

141 """ 

142 with temporary_visited_module( 

143 f""" 

144 import typing 

145 from typing import Union, Optional, Tuple, Dict, List, Set, Literal 

146 a: {annotation} 

147 """, 

148 ) as module: 

149 module["a"].annotation.modernize() 

150 

151 

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

153def test_modernizing_idempotence(code: str) -> None: 

154 """Modernize expressions that can't be modernized. 

155 

156 Parameters: 

157 code: An expression (parametrized). 

158 """ 

159 top_node = compile(code, filename="<>", mode="exec", flags=ast.PyCF_ONLY_AST, optimize=2) 

160 expression = get_expression(top_node.body[0].value, parent=Module("module")) # type: ignore[attr-defined] 

161 modernized = expression.modernize() # type: ignore[union-attr] 

162 assert expression == modernized 

163 assert str(expression) == str(modernized) 

164 

165 

166def test_resolving_init_parameter() -> None: 

167 """Instance attribute values should resolve to matching parameters. 

168 

169 They must not resolve to the member of the same name in the same class, 

170 or to objects with the same name in higher scopes. 

171 """ 

172 with temporary_visited_module( 

173 """ 

174 x = 1 

175 

176 class Class: 

177 def __init__(self, x: int): 

178 self.x: int = x 

179 """, 

180 ) as module: 

181 assert module["Class.x"].value.canonical_path == "module.Class(x)" 

182 

183 

184@pytest.mark.parametrize( 

185 "code", 

186 [ 

187 # Core. 

188 "a * (b + c)", # Lower precedence as a sub-expression of one that has higher precedence. 

189 "(a and b) == c", 

190 "((a | b) + c).d", 

191 "a - (b - c)", # Left-association. 

192 "(a ** b) ** c", # Right-association. 

193 # Unary operator and edge cases: 

194 # > The power operator `**` binds less tightly than an arithmetic 

195 # > or bitwise unary operator on its right, that is, `2**-1` is `0.5`. 

196 "a ** -b", 

197 "-a ** b", 

198 "(-a) ** b", 

199 # Misc: conditionals, lambdas, comprehensions and generators. 

200 "(lambda: 0).a", 

201 "(lambda x: a + x if b else c)(d).e", 

202 "a if (b if c else d) else e", # Right-association. 

203 "(a if b else c) if d else e", # Forced left-association. 

204 "(a for a in b).c", 

205 ], 

206) 

207def test_parentheses_preserved(code: str) -> None: 

208 """Parentheses used to enforce an order of operations should not be removed.""" 

209 with temporary_visited_module(f"val = {code}") as module: 

210 value_expr = module["val"].value 

211 assert str(value_expr) == code 

212 

213 

214# YORE: EOL 3.11: Remove line. 

215@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics") 

216def test_resolving_type_parameters() -> None: 

217 """Assert type parameters are correctly transformed to their fully-resolved form.""" 

218 with temporary_visited_module( 

219 """ 

220 class C[T]: 

221 class D[T]: 

222 def func[Y](self, arg1: T, arg2: Y): pass 

223 attr: T 

224 def func[Z](arg1: T, arg2: Y): pass 

225 """, 

226 ) as module: 

227 assert module["C.D.func"].parameters["arg1"].annotation.canonical_path == "module.C.D[T]" 

228 assert module["C.D.func"].parameters["arg2"].annotation.canonical_path == "module.C.D.func[Y]" 

229 

230 assert module["C.D.attr"].annotation.canonical_path == "module.C.D[T]" 

231 

232 assert module["C.func"].parameters["arg1"].annotation.canonical_path == "module.C[T]" 

233 assert module["C.func"].parameters["arg2"].annotation.canonical_path == "Y"