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
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-11 13:44 +0200
1"""Test names and expressions methods."""
3from __future__ import annotations
5import ast
6import sys
8import pytest
10from griffe import Module, Parser, get_expression, temporary_visited_module
11from tests.test_nodes import syntax_examples
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.
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
36 def function() -> {annotation}:
37 '''This function returns either two ints or None
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
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
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
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"
79@pytest.mark.parametrize("code", syntax_examples)
80def test_expressions(code: str) -> None:
81 """Test building annotations from AST nodes.
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
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',)"
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.
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
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.
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()
152@pytest.mark.parametrize("code", syntax_examples)
153def test_modernizing_idempotence(code: str) -> None:
154 """Modernize expressions that can't be modernized.
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)
166def test_resolving_init_parameter() -> None:
167 """Instance attribute values should resolve to matching parameters.
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
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)"
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
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]"
230 assert module["C.D.attr"].annotation.canonical_path == "module.C.D[T]"
232 assert module["C.func"].parameters["arg1"].annotation.canonical_path == "module.C[T]"
233 assert module["C.func"].parameters["arg2"].annotation.canonical_path == "Y"