Coverage for tests/test_nodes.py: 100.00%
60 statements
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
1"""Test nodes utilities."""
3from __future__ import annotations
5import logging
6from ast import PyCF_ONLY_AST
8import pytest
10from griffe import Expr, ExprName, module_vtree, relative_to_absolute, temporary_visited_module
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]
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.
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
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"
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.
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
168@pytest.mark.parametrize("code", syntax_examples)
169def test_building_expressions_from_nodes(code: str) -> None:
170 """Test building annotations from AST nodes.
172 Parameters:
173 code: An expression (parametrized).
174 """
175 with temporary_visited_module(f"__z__ = {code}") as module:
176 assert "__z__" in module.members
178 # make space after comma non-significant
179 value = str(module["__z__"].value).replace(", ", ",")
180 assert value == code.replace(", ", ",")
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).
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)
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)
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.
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
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 ...
245 @property
246 def args(self) -> 'tuple[Any, ...]':
247 ...
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)
261def test_parsing_dynamic_base_classes(caplog: pytest.LogCaptureFixture) -> None:
262 """Assert parsing dynamic base classes does not trigger errors.
264 Parameters:
265 caplog: Pytest fixture to capture logs.
266 """
267 with (
268 caplog.at_level(logging.ERROR),
269 temporary_visited_module(
270 """
271 from collections import namedtuple
272 class Thing(namedtuple('Thing', 'attr1 attr2')):
273 ...
274 """,
275 ),
276 ):
277 pass
278 assert not caplog.records