Coverage for tests / test_nodes.py: 100.00%
61 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-11 11:48 +0100
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-11 11:48 +0100
1"""Test nodes utilities."""
3from __future__ import annotations
5import logging
6import sys
7from ast import PyCF_ONLY_AST
9import pytest
11from griffe import Expr, ExprName, module_vtree, relative_to_absolute, temporary_visited_module
13syntax_examples = [
14 # Operations.
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 "b << c",
28 # Unary operations.
29 "+b",
30 "-b",
31 "~b",
32 # Comparisons.
33 "b == c",
34 "b >= c",
35 "b > c",
36 "b <= c",
37 "b < c",
38 "b != c",
39 # Boolean logic.
40 "b and c",
41 "b or c",
42 "not b",
43 # Identify.
44 "b is c",
45 "b is not c",
46 # Membership.
47 "b in c",
48 "b not in c",
49 # Calls.
50 "call()",
51 "call(something)",
52 "call(something=something)",
53 # Strings.
54 "f'a {round(key, 2)} {z}'",
55 # YORE: EOL 3.13: Replace line with `"t'a {round(key, 2)} {z}'",`.
56 *(["t'a {round(key, 2)} {z}'"] if sys.version_info >= (3, 14) else []),
57 # Slices.
58 "o[x]",
59 "o[x, y]",
60 "o[x:y]",
61 "o[x:y, z]",
62 "o[x, y(z)]",
63 # Walrus operator.
64 "a if (a := b) else c",
65 # Starred.
66 "a(*b, **c)",
67 # Structs.
68 "(a, b, c)",
69 "{a, b, c}",
70 "{a: b, c: d}",
71 "[a, b, c]",
72 # Yields.
73 "yield",
74 "yield a",
75 "yield from a",
76 # Lambdas.
77 "lambda a: a",
78 "lambda a, b: a",
79 "lambda *a, **b: a",
80 "lambda a, b=0: a",
81 "lambda a, /, b, c: a",
82 "lambda a, *, b, c: a",
83 "lambda a, /, b, *, c: a",
84]
87@pytest.mark.parametrize(
88 ("code", "path", "is_package", "expected"),
89 [
90 ("from . import b", "a", False, "a.b"),
91 ("from . import b", "a", True, "a.b"),
92 ("from . import c", "a.b", False, "a.c"),
93 ("from . import c", "a.b", True, "a.b.c"),
94 ("from . import d", "a.b.c", False, "a.b.d"),
95 ("from .c import d", "a", False, "a.c.d"),
96 ("from .c import d", "a.b", False, "a.c.d"),
97 ("from .b import c", "a.b", True, "a.b.b.c"),
98 ("from .. import e", "a.c.d.i", False, "a.c.e"),
99 ("from ..d import e", "a.c.d.i", False, "a.c.d.e"),
100 ("from ... import f", "a.c.d.i", False, "a.f"),
101 ("from ...b import f", "a.c.d.i", False, "a.b.f"),
102 ("from ...c.d import e", "a.c.d.i", False, "a.c.d.e"),
103 ("from .c import *", "a", False, "a.c.*"),
104 ("from .c import *", "a.b", False, "a.c.*"),
105 ("from .b import *", "a.b", True, "a.b.b.*"),
106 ("from .. import *", "a.c.d.i", False, "a.c.*"),
107 ("from ..d import *", "a.c.d.i", False, "a.c.d.*"),
108 ("from ... import *", "a.c.d.i", False, "a.*"),
109 ("from ...b import *", "a.c.d.i", False, "a.b.*"),
110 ("from ...c.d import *", "a.c.d.i", False, "a.c.d.*"),
111 ],
112)
113def test_relative_to_absolute_imports(code: str, path: str, is_package: bool, expected: str) -> None:
114 """Check if relative imports are correctly converted to absolute ones.
116 Parameters:
117 code: The parametrized module code.
118 path: The parametrized module path.
119 is_package: Whether the module is a package (or subpackage) (parametrized).
120 expected: The parametrized expected absolute path.
121 """
122 node = compile(code, mode="exec", filename="<>", flags=PyCF_ONLY_AST).body[0] # ty:ignore[unresolved-attribute]
123 module = module_vtree(path, leaf_package=is_package, return_leaf=True)
124 for name in node.names:
125 assert relative_to_absolute(node, name, module) == expected
128def test_multipart_imports() -> None:
129 """Assert that a multipart path like `a.b.c` imported as `x` points to the right target."""
130 with temporary_visited_module(
131 """
132 import pkg.b.c
133 import pkg.b.c as alias
134 """,
135 ) as module:
136 pkg = module["pkg"]
137 alias = module["alias"]
138 assert pkg.target_path == "pkg"
139 assert alias.target_path == "pkg.b.c"
142@pytest.mark.parametrize(
143 "expression",
144 [
145 "A",
146 "A.B",
147 "A[B]",
148 "A.B[C.D]",
149 "~A",
150 "A | B",
151 "A[[B, C], D]",
152 "A(b=c, d=1)",
153 "A[-1, +2.3]",
154 "A[B, C.D(e='syntax error')]",
155 ],
156)
157def test_building_annotations_from_nodes(expression: str) -> None:
158 """Test building annotations from AST nodes.
160 Parameters:
161 expression: An expression (parametrized).
162 """
163 class_defs = "\n\n".join(f"class {letter}: ..." for letter in "ABCD")
164 with temporary_visited_module(f"{class_defs}\n\nx: {expression}\ny: {expression} = 0") as module:
165 assert "x" in module.members
166 assert "y" in module.members
167 assert str(module["x"].annotation) == expression
168 assert str(module["y"].annotation) == expression
171@pytest.mark.parametrize("code", syntax_examples)
172def test_building_expressions_from_nodes(code: str) -> None:
173 """Test building annotations from AST nodes.
175 Parameters:
176 code: An expression (parametrized).
177 """
178 with temporary_visited_module(f"__z__ = {code}") as module:
179 assert "__z__" in module.members
181 # Make space after comma non-significant.
182 value = str(module["__z__"].value).replace(", ", ",")
183 assert value == code.replace(", ", ",")
186@pytest.mark.parametrize(
187 ("code", "has_name"),
188 [
189 ("import typing\nclass A: ...\na: typing.Literal['A']", False),
190 ("from typing import Literal\nclass A: ...\na: Literal['A']", False),
191 ("import typing_extensions\nclass A: ...\na: typing.Literal['A']", False),
192 ("from typing_extensions import Literal\nclass A: ...\na: Literal['A']", False),
193 ("from mod import A\na: 'A'", True),
194 ("from mod import A\na: list['A']", True),
195 ],
196)
197def test_forward_references(code: str, has_name: bool) -> None:
198 """Check that we support forward references (type names as strings).
200 Parameters:
201 code: Parametrized code.
202 has_name: Whether the annotation should contain a Name rather than a string.
203 """
204 with temporary_visited_module(code) as module:
205 annotation = list(module["a"].annotation.iterate(flat=True))
206 if has_name:
207 assert any(isinstance(item, ExprName) and item.name == "A" for item in annotation)
208 assert all(not (isinstance(item, str) and item == "A") for item in annotation)
209 else:
210 assert "'A'" in annotation
211 assert all(not (isinstance(item, ExprName) and item.name == "A") for item in annotation)
214@pytest.mark.parametrize(
215 "default",
216 [
217 "1",
218 "'test_string'",
219 "dict(key=1)",
220 "{'key': 1}",
221 "DEFAULT_VALUE",
222 "None",
223 ],
224)
225def test_default_value_from_nodes(default: str) -> None:
226 """Test getting default value from AST nodes.
228 Parameters:
229 default: A default value (parametrized).
230 """
231 module_defs = f"def f(x={default}):\n return x"
232 with temporary_visited_module(module_defs) as module:
233 assert "f" in module.members
234 params = module.members["f"].parameters # ty:ignore[possibly-missing-attribute]
235 assert len(params) == 1
236 assert str(params[0].default) == default
239# https://github.com/mkdocstrings/griffe/issues/159
240def test_parsing_complex_string_annotations() -> None:
241 """Test parsing of complex, stringified annotations."""
242 with temporary_visited_module(
243 """
244 class ArgsKwargs:
245 def __init__(self, args: 'tuple[Any, ...]', kwargs: 'dict[str, Any] | None' = None) -> None:
246 ...
248 @property
249 def args(self) -> 'tuple[Any, ...]':
250 ...
252 @property
253 def kwargs(self) -> 'dict[str, Any] | None':
254 ...
255 """,
256 ) as module:
257 init_args_annotation = module["ArgsKwargs.__init__"].parameters["args"].annotation
258 assert isinstance(init_args_annotation, Expr)
259 assert init_args_annotation.is_tuple
260 kwargs_return_annotation = module["ArgsKwargs.kwargs"].annotation
261 assert isinstance(kwargs_return_annotation, Expr)
264def test_parsing_dynamic_base_classes(caplog: pytest.LogCaptureFixture) -> None:
265 """Assert parsing dynamic base classes does not trigger errors.
267 Parameters:
268 caplog: Pytest fixture to capture logs.
269 """
270 with (
271 caplog.at_level(logging.ERROR),
272 temporary_visited_module(
273 """
274 from collections import namedtuple
275 class Thing(namedtuple('Thing', 'attr1 attr2')):
276 ...
277 """,
278 ),
279 ):
280 pass
281 assert not caplog.records