Coverage for packages / griffelib / src / griffe / _internal / expressions.py: 92.71%
752 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# This module contains the data classes that represent resolvable names and expressions.
2# First we declare data classes for each kind of expression, mostly corresponding to Python's AST nodes.
3# Then we declare builder methods, that iterate AST nodes and build the corresponding data classes,
4# and two utilities `_yield` and `_join` to help iterate on expressions.
5# Finally we declare a few public helpers to safely get expressions from AST nodes in different scenarios.
7from __future__ import annotations
9import ast
10import sys
11from dataclasses import dataclass
12from dataclasses import fields as getfields
13from enum import IntEnum, auto
14from functools import partial
15from typing import TYPE_CHECKING, Any, Protocol
17from griffe._internal.agents.nodes.parameters import get_parameters
18from griffe._internal.enumerations import LogLevel, ParameterKind
19from griffe._internal.exceptions import NameResolutionError
20from griffe._internal.logger import logger
22if TYPE_CHECKING:
23 from collections.abc import Iterable, Iterator, Sequence
24 from pathlib import Path
26 from griffe._internal.models import Class, Function, Module
29class _OperatorPrecedence(IntEnum):
30 # Adapted from:
31 #
32 # - https://docs.python.org/3/reference/expressions.html#operator-precedence
33 # - https://github.com/python/cpython/blob/main/Lib/_ast_unparse.py
34 # - https://github.com/astral-sh/ruff/blob/6abafcb56575454f2caeaa174efcb9fd0a8362b1/crates/ruff_python_ast/src/operator_precedence.rs
36 # The enum members are declared in ascending order of precedence.
38 # A virtual precedence level for contexts that provide their own grouping, like list brackets or
39 # function call parentheses. This ensures parentheses will never be added for the direct children of these nodes.
40 # NOTE: `ruff_python_formatter::expression::parentheses`'s state machine would be more robust
41 # but would introduce significant complexity.
42 NONE = auto()
44 YIELD = auto() # `yield`, `yield from`
45 ASSIGN = auto() # `target := expr`
46 STARRED = auto() # `*expr` (omitted by Python docs, see ruff impl)
47 LAMBDA = auto()
48 IF_ELSE = auto() # `expr if cond else expr`
49 OR = auto()
50 AND = auto()
51 NOT = auto()
52 COMP_MEMB_ID = auto() # `<`, `<=`, `>`, `>=`, `!=`, `==`, `in`, `not in`, `is`, `is not`
53 BIT_OR = auto() # `|`
54 BIT_XOR = auto() # `^`
55 BIT_AND = auto() # `&`
56 LEFT_RIGHT_SHIFT = auto() # `<<`, `>>`
57 ADD_SUB = auto() # `+`, `-`
58 MUL_DIV_REMAIN = auto() # `*`, `@`, `/`, `//`, `%`
59 POS_NEG_BIT_NOT = auto() # `+x`, `-x`, `~x`
60 EXPONENT = auto() # `**`
61 AWAIT = auto()
62 CALL_ATTRIBUTE = auto() # `x[index]`, `x[index:index]`, `x(arguments...)`, `x.attribute`
63 ATOMIC = auto() # `(expressions...)`, `[expressions...]`, `{key: value...}`, `{expressions...}`
66def _yield(
67 element: str | Expr | tuple[str | Expr, ...],
68 *,
69 flat: bool = True,
70 is_left: bool = False,
71 outer_precedence: _OperatorPrecedence = _OperatorPrecedence.ATOMIC,
72) -> Iterator[str | Expr]:
73 if isinstance(element, Expr):
74 element_precedence = _get_precedence(element)
75 needs_parens = False
76 # Lower inner precedence, e.g. `(a + b) * c`, `+(10) < *(11)`.
77 if element_precedence < outer_precedence:
78 needs_parens = True
79 elif element_precedence == outer_precedence:
80 # Right-association, e.g. parenthesize left-hand side in `(a ** b) ** c`, (a if b else c) if d else e
81 is_right_assoc = isinstance(element, ExprIfExp) or (
82 isinstance(element, ExprBinOp) and element.operator == "**"
83 )
84 if is_right_assoc:
85 if is_left: 85 ↛ 91line 85 didn't jump to line 91 because the condition on line 85 was always true
86 needs_parens = True
87 # Left-association, e.g. parenthesize right-hand side in `a - (b - c)`.
88 elif isinstance(element, (ExprBinOp, ExprBoolOp)) and not is_left:
89 needs_parens = True
91 if needs_parens:
92 yield "("
93 if flat:
94 yield from element.iterate(flat=True)
95 else:
96 yield element
97 yield ")"
98 elif flat:
99 yield from element.iterate(flat=True)
100 else:
101 yield element
102 elif isinstance(element, tuple):
103 for elem in element:
104 yield from _yield(elem, flat=flat, outer_precedence=outer_precedence, is_left=is_left)
105 else:
106 yield element
109def _join(
110 elements: Iterable[str | Expr | tuple[str | Expr, ...]],
111 joint: str | Expr,
112 *,
113 flat: bool = True,
114) -> Iterator[str | Expr]:
115 """Apply a separator between elements.
117 The caller is assumed to provide their own grouping
118 (e.g. lists, tuples, slice) and will prevent parentheses from being added.
119 """
120 it = iter(elements)
121 try:
122 # Since we are in a sequence, don't parenthesize items.
123 # Avoids [a + b, c + d] being serialized as [(a + b), (c + d)]
124 yield from _yield(next(it), flat=flat, outer_precedence=_OperatorPrecedence.NONE)
125 except StopIteration:
126 return
127 for element in it:
128 yield from _yield(joint, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
129 yield from _yield(element, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
132def _field_as_dict(
133 element: str | bool | Expr | list[str | Expr] | None, # noqa: FBT001
134 **kwargs: Any,
135) -> str | bool | None | list | dict:
136 if isinstance(element, Expr):
137 return _expr_as_dict(element, **kwargs)
138 if isinstance(element, list):
139 return [_field_as_dict(elem, **kwargs) for elem in element]
140 return element
143def _expr_as_dict(expression: Expr, **kwargs: Any) -> dict[str, Any]:
144 fields = {
145 field.name: _field_as_dict(getattr(expression, field.name), **kwargs)
146 for field in sorted(getfields(expression), key=lambda f: f.name)
147 if field.name != "parent"
148 }
149 fields["cls"] = expression.classname
150 return fields
153_modern_types = {
154 "typing.Tuple": "tuple",
155 "typing.Dict": "dict",
156 "typing.List": "list",
157 "typing.Set": "set",
158}
161@dataclass
162class Expr:
163 """Base class for expressions."""
165 def __str__(self) -> str:
166 return "".join(elem if isinstance(elem, str) else elem.name for elem in self.iterate(flat=True)) # ty:ignore[unresolved-attribute]
168 def __iter__(self) -> Iterator[str | Expr]:
169 """Iterate on the expression syntax and elements."""
170 yield from self.iterate(flat=False)
172 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002
173 """Iterate on the expression elements.
175 Parameters:
176 flat: Expressions are trees.
178 When flat is false, this method iterates only on the first layer of the tree.
179 To iterate on all the subparts of the expression, you have to do so recursively.
180 It allows to handle each subpart specifically (for example subscripts, attribute, etc.),
181 without them getting rendered as strings.
183 On the contrary, when flat is true, the whole tree is flattened as a sequence
184 of strings and instances of [Names][griffe.ExprName].
186 Yields:
187 Strings and names when flat, strings and expressions otherwise.
188 """
189 yield from ()
191 def modernize(self) -> Expr:
192 """Modernize the expression.
194 For example, use PEP 604 type unions `|` instead of `typing.Union`.
196 Returns:
197 A modernized expression.
198 """
199 return self
201 def as_dict(self, **kwargs: Any) -> dict[str, Any]:
202 """Return the expression as a dictionary.
204 Parameters:
205 **kwargs: Configuration options (none available yet).
208 Returns:
209 A dictionary.
210 """
211 return _expr_as_dict(self, **kwargs)
213 @property
214 def classname(self) -> str:
215 """The expression class name."""
216 return self.__class__.__name__
218 @property
219 def path(self) -> str:
220 """Path of the expressed name/attribute."""
221 return str(self)
223 @property
224 def canonical_path(self) -> str:
225 """Path of the expressed name/attribute."""
226 return str(self)
228 @property
229 def canonical_name(self) -> str:
230 """Name of the expressed name/attribute/parameter."""
231 # We must handle things like `griffe.Visitor` and `griffe.Visitor(code)`.
232 return self.canonical_path.rsplit(".", 1)[-1].split("(", 1)[-1].removesuffix(")")
234 @property
235 def is_classvar(self) -> bool:
236 """Whether this attribute is annotated with `ClassVar`."""
237 return isinstance(self, ExprSubscript) and self.canonical_name == "ClassVar"
239 @property
240 def is_tuple(self) -> bool:
241 """Whether this expression is a tuple."""
242 return isinstance(self, ExprSubscript) and self.canonical_name.lower() == "tuple"
244 @property
245 def is_iterator(self) -> bool:
246 """Whether this expression is an iterator."""
247 return isinstance(self, ExprSubscript) and self.canonical_name == "Iterator"
249 @property
250 def is_generator(self) -> bool:
251 """Whether this expression is a generator."""
252 return isinstance(self, ExprSubscript) and self.canonical_name == "Generator"
254 @staticmethod
255 def _to_binop(elements: Sequence[Expr], op: str) -> ExprBinOp:
256 if len(elements) == 2: # noqa: PLR2004
257 left, right = elements
258 if isinstance(left, Expr): 258 ↛ 260line 258 didn't jump to line 260 because the condition on line 258 was always true
259 left = left.modernize()
260 if isinstance(right, Expr): 260 ↛ 262line 260 didn't jump to line 262 because the condition on line 260 was always true
261 right = right.modernize()
262 return ExprBinOp(left=left, operator=op, right=right)
264 left = ExprSubscript._to_binop(elements[:-1], op=op)
265 right = elements[-1]
266 if isinstance(right, Expr): 266 ↛ 268line 266 didn't jump to line 268 because the condition on line 266 was always true
267 right = right.modernize()
268 return ExprBinOp(left=left, operator=op, right=right)
271@dataclass(eq=True, slots=True)
272class ExprAttribute(Expr):
273 """Attributes like `a.b`."""
275 values: list[str | Expr]
276 """The different parts of the dotted chain."""
278 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
279 precedence = _get_precedence(self)
280 yield from _yield(self.values[0], flat=flat, outer_precedence=precedence, is_left=True)
281 for value in self.values[1:]:
282 yield "."
283 yield from _yield(value, flat=flat, outer_precedence=precedence)
285 def modernize(self) -> ExprName | ExprAttribute:
286 if modern := _modern_types.get(self.canonical_path):
287 return ExprName(modern, parent=self.last.parent)
288 return self
290 def append(self, value: ExprName) -> None:
291 """Append a name to this attribute.
293 Parameters:
294 value: The expression name to append.
295 """
296 if value.parent is None: 296 ↛ 298line 296 didn't jump to line 298 because the condition on line 296 was always true
297 value.parent = self.last
298 self.values.append(value)
300 @property
301 def last(self) -> ExprName:
302 """The last part of this attribute (on the right)."""
303 # All values except the first one can *only* be names:
304 # we can't do `a.(b or c)` or `a."string"`.
305 return self.values[-1] # ty:ignore[invalid-return-type]
307 @property
308 def first(self) -> str | Expr:
309 """The first part of this attribute (on the left)."""
310 return self.values[0]
312 @property
313 def path(self) -> str:
314 """The path of this attribute."""
315 return self.last.path
317 @property
318 def canonical_path(self) -> str:
319 """The canonical path of this attribute."""
320 return self.last.canonical_path
323@dataclass(eq=True, slots=True)
324class ExprBinOp(Expr):
325 """Binary operations like `a + b`."""
327 left: str | Expr
328 """Left part."""
329 operator: str
330 """Binary operator."""
331 right: str | Expr
332 """Right part."""
334 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
335 precedence = _get_precedence(self)
336 right_precedence = precedence
337 if self.operator == "**" and isinstance(self.right, ExprUnaryOp):
338 # Unary operators on the right have higher precedence, e.g. `a ** -b`.
339 right_precedence = _OperatorPrecedence(precedence - 1)
340 yield from _yield(self.left, flat=flat, outer_precedence=precedence, is_left=True)
341 yield f" {self.operator} "
342 yield from _yield(self.right, flat=flat, outer_precedence=right_precedence, is_left=False)
345@dataclass(eq=True, slots=True)
346class ExprBoolOp(Expr):
347 """Boolean operations like `a or b`."""
349 operator: str
350 """Boolean operator."""
351 values: Sequence[str | Expr]
352 """Operands."""
354 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
355 precedence = _get_precedence(self)
356 it = iter(self.values)
357 yield from _yield(next(it), flat=flat, outer_precedence=precedence, is_left=True)
358 for value in it:
359 yield f" {self.operator} "
360 yield from _yield(value, flat=flat, outer_precedence=precedence, is_left=False)
363@dataclass(eq=True, slots=True)
364class ExprCall(Expr):
365 """Calls like `f()`."""
367 function: Expr
368 """Function called."""
369 arguments: Sequence[str | Expr]
370 """Passed arguments."""
372 @property
373 def canonical_path(self) -> str:
374 """The canonical path of this subscript's left part."""
375 return self.function.canonical_path
377 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
378 yield from _yield(self.function, flat=flat, outer_precedence=_get_precedence(self))
379 yield "("
380 yield from _join(self.arguments, ", ", flat=flat)
381 yield ")"
384@dataclass(eq=True, slots=True)
385class ExprCompare(Expr):
386 """Comparisons like `a > b`."""
388 left: str | Expr
389 """Left part."""
390 operators: Sequence[str]
391 """Comparison operators."""
392 comparators: Sequence[str | Expr]
393 """Things compared."""
395 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
396 precedence = _get_precedence(self)
397 yield from _yield(self.left, flat=flat, outer_precedence=precedence, is_left=True)
398 for op, comp in zip(self.operators, self.comparators, strict=False):
399 yield f" {op} "
400 yield from _yield(comp, flat=flat, outer_precedence=precedence)
403@dataclass(eq=True, slots=True)
404class ExprComprehension(Expr):
405 """Comprehensions like `a for b in c if d`."""
407 target: str | Expr
408 """Comprehension target (value added to the result)."""
409 iterable: str | Expr
410 """Value iterated on."""
411 conditions: Sequence[str | Expr]
412 """Conditions to include the target in the result."""
413 is_async: bool = False
414 """Async comprehension or not."""
416 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
417 if self.is_async: 417 ↛ 418line 417 didn't jump to line 418 because the condition on line 417 was never true
418 yield "async "
419 yield "for "
420 yield from _yield(self.target, flat=flat)
421 yield " in "
422 yield from _yield(self.iterable, flat=flat)
423 if self.conditions:
424 yield " if "
425 yield from _join(self.conditions, " if ", flat=flat)
428# TODO: `ExprConstant` is never instantiated,
429# see `_build_constant` below (it always returns the value directly).
430# Maybe we could simply get rid of it, as it wouldn't bring much value
431# if used anyway.
432@dataclass(eq=True, slots=True)
433class ExprConstant(Expr):
434 """Constants like `"a"` or `1`."""
436 value: str
437 """Constant value."""
439 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002
440 yield self.value
443@dataclass(eq=True, slots=True)
444class ExprDict(Expr):
445 """Dictionaries like `{"a": 0}`."""
447 keys: Sequence[str | Expr | None]
448 """Dict keys."""
449 values: Sequence[str | Expr]
450 """Dict values."""
452 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
453 yield "{"
454 yield from _join(
455 (("None" if key is None else key, ": ", value) for key, value in zip(self.keys, self.values, strict=False)),
456 ", ",
457 flat=flat,
458 )
459 yield "}"
462@dataclass(eq=True, slots=True)
463class ExprDictComp(Expr):
464 """Dict comprehensions like `{k: v for k, v in a}`."""
466 key: str | Expr
467 """Target key."""
468 value: str | Expr
469 """Target value."""
470 generators: Sequence[Expr]
471 """Generators iterated on."""
473 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
474 yield "{"
475 yield from _yield(self.key, flat=flat)
476 yield ": "
477 yield from _yield(self.value, flat=flat)
478 yield " "
479 yield from _join(self.generators, " ", flat=flat)
480 yield "}"
483@dataclass(eq=True, slots=True)
484class ExprExtSlice(Expr):
485 """Extended slice like `a[x:y, z]`."""
487 dims: Sequence[str | Expr]
488 """Dims."""
490 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
491 yield from _join(self.dims, ", ", flat=flat)
494@dataclass(eq=True, slots=True)
495class ExprFormatted(Expr):
496 """Formatted string like `{1 + 1}`."""
498 value: str | Expr
499 """Formatted value."""
501 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
502 yield "{"
503 # Prevent parentheses from being added, avoiding `{(1 + 1)}`
504 yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
505 yield "}"
508@dataclass(eq=True, slots=True)
509class ExprGeneratorExp(Expr):
510 """Generator expressions like `a for b in c for d in e`."""
512 element: str | Expr
513 """Yielded element."""
514 generators: Sequence[Expr]
515 """Generators iterated on."""
517 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
518 yield from _yield(self.element, flat=flat)
519 yield " "
520 yield from _join(self.generators, " ", flat=flat)
523@dataclass(eq=True, slots=True)
524class ExprIfExp(Expr):
525 """Conditions like `a if b else c`."""
527 body: str | Expr
528 """Value if test."""
529 test: str | Expr
530 """Condition."""
531 orelse: str | Expr
532 """Other expression."""
534 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
535 precedence = _get_precedence(self)
536 yield from _yield(self.body, flat=flat, outer_precedence=precedence, is_left=True)
537 yield " if "
538 # If the test itself is another if/else, its precedence is the same, which will not give
539 # a parenthesis: force it.
540 test_outer_precedence = _OperatorPrecedence(precedence + 1)
541 yield from _yield(self.test, flat=flat, outer_precedence=test_outer_precedence)
542 yield " else "
543 # If/else is right associative. For example, a nested if/else
544 # `a if b else c if d else e` is effectively `a if b else (c if d else e)`, so produce a
545 # flattened version without parentheses.
546 if isinstance(self.orelse, ExprIfExp): 546 ↛ 547line 546 didn't jump to line 547 because the condition on line 546 was never true
547 yield from self.orelse.iterate(flat=flat)
548 else:
549 yield from _yield(self.orelse, flat=flat, outer_precedence=precedence, is_left=False)
552@dataclass(eq=True, slots=True)
553class ExprInterpolation(Expr):
554 """Template string interpolation like `{name}`."""
556 value: str | Expr
557 """Interpolated value."""
559 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
560 yield "{"
561 # Prevent parentheses from being added, avoiding `{(1 + 1)}`
562 yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
563 yield "}"
566@dataclass(eq=True, slots=True)
567class ExprJoinedStr(Expr):
568 """Joined strings like `f"a {b} c"`."""
570 values: Sequence[str | Expr]
571 """Joined values."""
573 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
574 yield "f'"
575 yield from _join(self.values, "", flat=flat)
576 yield "'"
579@dataclass(eq=True, slots=True)
580class ExprKeyword(Expr):
581 """Keyword arguments like `a=b`."""
583 name: str
584 """Name."""
585 value: str | Expr
586 """Value."""
588 # Griffe is designed around accessing Python objects
589 # with the dot notation, for example `module.Class`.
590 # Function parameters were not taken into account
591 # because they are not accessible the same way.
592 # But we still want to be able to cross-reference
593 # documentation of function parameters in downstream
594 # tools like mkdocstrings. So we add a special case
595 # for keyword expressions, where they get a meaningful
596 # canonical path (contrary to most other expressions that
597 # aren't or do not begin with names or attributes)
598 # of the form `path.to.called_function(param_name)`.
599 # For this we need to store a reference to the `func` part
600 # of the call expression in the keyword one,
601 # hence the following field.
602 # We allow it to be None for backward compatibility.
603 function: Expr | None = None
604 """Expression referencing the function called with this parameter."""
606 @property
607 def canonical_path(self) -> str:
608 """Path of the expressed keyword."""
609 if self.function:
610 return f"{self.function.canonical_path}({self.name})"
611 return super(ExprKeyword, self).canonical_path
613 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
614 yield self.name
615 yield "="
616 yield from _yield(self.value, flat=flat)
619@dataclass(eq=True, slots=True)
620class ExprVarPositional(Expr):
621 """Variadic positional parameters like `*args`."""
623 value: Expr
624 """Starred value."""
626 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
627 yield "*"
628 yield from _yield(self.value, flat=flat)
631@dataclass(eq=True, slots=True)
632class ExprVarKeyword(Expr):
633 """Variadic keyword parameters like `**kwargs`."""
635 value: Expr
636 """Double-starred value."""
638 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
639 yield "**"
640 yield from _yield(self.value, flat=flat)
643@dataclass(eq=True, slots=True)
644class ExprLambda(Expr):
645 """Lambda expressions like `lambda a: a.b`."""
647 parameters: Sequence[ExprParameter]
648 """Lambda's parameters."""
649 body: str | Expr
650 """Lambda's body."""
652 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
653 pos_only = False
654 pos_or_kw = False
655 kw_only = False
656 length = len(self.parameters)
657 yield "lambda"
658 if length:
659 yield " "
660 for index, parameter in enumerate(self.parameters, 1):
661 if parameter.kind is ParameterKind.positional_only:
662 pos_only = True
663 elif parameter.kind is ParameterKind.var_positional:
664 yield "*"
665 elif parameter.kind is ParameterKind.var_keyword:
666 yield "**"
667 elif parameter.kind is ParameterKind.positional_or_keyword and not pos_or_kw:
668 pos_or_kw = True
669 elif parameter.kind is ParameterKind.keyword_only and not kw_only:
670 kw_only = True
671 yield "*, "
672 if parameter.kind is not ParameterKind.positional_only and pos_only:
673 pos_only = False
674 yield "/, "
675 yield parameter.name
676 if parameter.default and parameter.kind not in (ParameterKind.var_positional, ParameterKind.var_keyword):
677 yield "="
678 yield from _yield(parameter.default, flat=flat)
679 if index < length:
680 yield ", "
681 yield ": "
682 # Body of lambda should not have parentheses, avoiding `lambda: a.b`
683 yield from _yield(self.body, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
686@dataclass(eq=True, slots=True)
687class ExprList(Expr):
688 """Lists like `[0, 1, 2]`."""
690 elements: Sequence[Expr]
691 """List elements."""
693 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
694 yield "["
695 yield from _join(self.elements, ", ", flat=flat)
696 yield "]"
699@dataclass(eq=True, slots=True)
700class ExprListComp(Expr):
701 """List comprehensions like `[a for b in c]`."""
703 element: str | Expr
704 """Target value."""
705 generators: Sequence[Expr]
706 """Generators iterated on."""
708 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
709 yield "["
710 yield from _yield(self.element, flat=flat)
711 yield " "
712 yield from _join(self.generators, " ", flat=flat)
713 yield "]"
716@dataclass(eq=False, slots=True)
717class ExprName(Expr): # noqa: PLW1641
718 """This class represents a Python object identified by a name in a given scope."""
720 name: str
721 """Actual name."""
722 parent: str | ExprName | Module | Class | Function | None = None
723 """Parent (for resolution in its scope)."""
724 member: str | None = None
725 """Member name (for resolution in its scope)."""
727 def __eq__(self, other: object) -> bool:
728 """Two name expressions are equal if they have the same `name` value (`parent` is ignored)."""
729 if isinstance(other, ExprName):
730 return self.name == other.name
731 return NotImplemented
733 def iterate(self, *, flat: bool = True) -> Iterator[ExprName]: # noqa: ARG002
734 yield self
736 def modernize(self) -> ExprName:
737 if modern := _modern_types.get(self.canonical_path):
738 return ExprName(modern, parent=self.parent)
739 return self
741 @property
742 def path(self) -> str:
743 """The full, resolved name.
745 If it was given when creating the name, return that.
746 If a callable was given, call it and return its result.
747 It the name cannot be resolved, return the source.
748 """
749 if isinstance(self.parent, ExprName):
750 return f"{self.parent.path}.{self.name}"
751 return self.name
753 @property
754 def canonical_path(self) -> str:
755 """The canonical name (resolved one, not alias name)."""
756 if self.parent is None: 756 ↛ 757line 756 didn't jump to line 757 because the condition on line 756 was never true
757 return self.name
758 if isinstance(self.parent, ExprName):
759 return f"{self.parent.canonical_path}.{self.name}"
760 if isinstance(self.parent, str): 760 ↛ 761line 760 didn't jump to line 761 because the condition on line 760 was never true
761 return f"{self.parent}.{self.name}"
762 parent = self.parent.members.get(self.member, self.parent) # ty:ignore[no-matching-overload]
763 try:
764 return parent.resolve(self.name)
765 except NameResolutionError:
766 return self.name
768 @property
769 def resolved(self) -> Module | Class | None:
770 """The resolved object this name refers to."""
771 try:
772 return self.parent.modules_collection[self.parent.resolve(self.name)] # ty:ignore[possibly-missing-attribute]
773 except Exception: # noqa: BLE001
774 return self.parent.resolved[self.name] # ty:ignore[not-subscriptable, possibly-missing-attribute]
776 @property
777 def is_enum_class(self) -> bool:
778 """Whether this name resolves to an enumeration class."""
779 try:
780 bases = self.resolved.bases # ty:ignore[possibly-missing-attribute]
781 except Exception: # noqa: BLE001
782 return False
784 # TODO: Support inheritance?
785 # TODO: Support `StrEnum` and `IntEnum`.
786 return any(isinstance(base, Expr) and base.canonical_path == "enum.Enum" for base in bases)
788 @property
789 def is_enum_instance(self) -> bool:
790 """Whether this name resolves to an enumeration instance."""
791 try:
792 return self.parent.is_enum_class # ty:ignore[possibly-missing-attribute]
793 except Exception: # noqa: BLE001
794 return False
796 @property
797 def is_enum_value(self) -> bool:
798 """Whether this name resolves to an enumeration value."""
799 try:
800 return self.name == "value" and self.parent.is_enum_instance # ty:ignore[possibly-missing-attribute]
801 except Exception: # noqa: BLE001
802 return False
804 @property
805 def is_type_parameter(self) -> bool:
806 """Whether this name resolves to a type parameter."""
807 return "[" in self.canonical_path
810@dataclass(eq=True, slots=True)
811class ExprNamedExpr(Expr):
812 """Named/assignment expressions like `a := b`."""
814 target: Expr
815 """Target name."""
816 value: str | Expr
817 """Value."""
819 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
820 yield from _yield(self.target, flat=flat)
821 yield " := "
822 yield from _yield(self.value, flat=flat)
825@dataclass(eq=True, slots=True)
826class ExprParameter(Expr):
827 """Parameters in function signatures like `a: int = 0`."""
829 name: str
830 """Parameter name."""
831 kind: ParameterKind = ParameterKind.positional_or_keyword
832 """Parameter kind."""
833 annotation: Expr | None = None
834 """Parameter type."""
835 default: str | Expr | None = None
836 """Parameter default."""
839@dataclass(eq=True, slots=True)
840class ExprSet(Expr):
841 """Sets like `{0, 1, 2}`."""
843 elements: Sequence[str | Expr]
844 """Set elements."""
846 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
847 yield "{"
848 yield from _join(self.elements, ", ", flat=flat)
849 yield "}"
852@dataclass(eq=True, slots=True)
853class ExprSetComp(Expr):
854 """Set comprehensions like `{a for b in c}`."""
856 element: str | Expr
857 """Target value."""
858 generators: Sequence[Expr]
859 """Generators iterated on."""
861 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
862 yield "{"
863 yield from _yield(self.element, flat=flat)
864 yield " "
865 yield from _join(self.generators, " ", flat=flat)
866 yield "}"
869@dataclass(eq=True, slots=True)
870class ExprSlice(Expr):
871 """Slices like `[a:b:c]`."""
873 lower: str | Expr | None = None
874 """Lower bound."""
875 upper: str | Expr | None = None
876 """Upper bound."""
877 step: str | Expr | None = None
878 """Iteration step."""
880 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
881 if self.lower is not None:
882 yield from _yield(self.lower, flat=flat)
883 yield ":"
884 if self.upper is not None: 884 ↛ 886line 884 didn't jump to line 886 because the condition on line 884 was always true
885 yield from _yield(self.upper, flat=flat)
886 if self.step is not None: 886 ↛ 887line 886 didn't jump to line 887 because the condition on line 886 was never true
887 yield ":"
888 yield from _yield(self.step, flat=flat)
891@dataclass(eq=True, slots=True)
892class ExprSubscript(Expr):
893 """Subscripts like `a[b]`."""
895 left: str | Expr
896 """Left part."""
897 slice: str | Expr
898 """Slice part."""
900 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
901 yield from _yield(self.left, flat=flat, outer_precedence=_get_precedence(self))
902 yield "["
903 # Prevent parentheses from being added, avoiding `a[(b)]`
904 yield from _yield(self.slice, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
905 yield "]"
907 def modernize(self) -> ExprBinOp | ExprSubscript:
908 if self.canonical_path == "typing.Union":
909 return self._to_binop(self.slice.elements, op="|") # ty:ignore[unresolved-attribute]
910 if self.canonical_path == "typing.Optional":
911 left = self.slice if isinstance(self.slice, str) else self.slice.modernize()
912 return ExprBinOp(left=left, operator="|", right="None")
913 return ExprSubscript(
914 left=self.left if isinstance(self.left, str) else self.left.modernize(),
915 slice=self.slice if isinstance(self.slice, str) else self.slice.modernize(),
916 )
918 @property
919 def path(self) -> str:
920 """The path of this subscript's left part."""
921 if isinstance(self.left, str):
922 return self.left
923 return self.left.path
925 @property
926 def canonical_path(self) -> str:
927 """The canonical path of this subscript's left part."""
928 if isinstance(self.left, str): 928 ↛ 929line 928 didn't jump to line 929 because the condition on line 928 was never true
929 return self.left
930 return self.left.canonical_path
933@dataclass(eq=True, slots=True)
934class ExprTemplateStr(Expr):
935 """Template strings like `t"a {name}"`."""
937 values: Sequence[str | Expr]
938 """Joined values."""
940 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
941 yield "t'"
942 yield from _join(self.values, "", flat=flat)
943 yield "'"
946@dataclass(eq=True, slots=True)
947class ExprTuple(Expr):
948 """Tuples like `(0, 1, 2)`."""
950 elements: Sequence[str | Expr]
951 """Tuple elements."""
952 implicit: bool = False
953 """Whether the tuple is implicit (e.g. without parentheses in a subscript's slice)."""
955 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
956 if not self.implicit:
957 yield "("
958 yield from _join(self.elements, ", ", flat=flat)
959 if len(self.elements) == 1:
960 yield ","
961 if not self.implicit:
962 yield ")"
964 def modernize(self) -> ExprTuple:
965 return ExprTuple(
966 elements=[el if isinstance(el, str) else el.modernize() for el in self.elements],
967 implicit=self.implicit,
968 )
971@dataclass(eq=True, slots=True)
972class ExprUnaryOp(Expr):
973 """Unary operations like `-1`."""
975 operator: str
976 """Unary operator."""
977 value: str | Expr
978 """Value."""
980 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
981 yield self.operator
982 if self.operator == "not":
983 yield " "
984 yield from _yield(self.value, flat=flat, outer_precedence=_get_precedence(self))
987@dataclass(eq=True, slots=True)
988class ExprYield(Expr):
989 """Yield statements like `yield a`."""
991 value: str | Expr | None = None
992 """Yielded value."""
994 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
995 yield "yield"
996 if self.value is not None:
997 yield " "
998 yield from _yield(self.value, flat=flat)
1001@dataclass(eq=True, slots=True)
1002class ExprYieldFrom(Expr):
1003 """Yield statements like `yield from a`."""
1005 value: str | Expr
1006 """Yielded-from value."""
1008 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
1009 yield "yield from "
1010 yield from _yield(self.value, flat=flat)
1013_unary_op_map = {
1014 ast.Invert: "~",
1015 ast.Not: "not",
1016 ast.UAdd: "+",
1017 ast.USub: "-",
1018}
1020_binary_op_map = {
1021 ast.Add: "+",
1022 ast.BitAnd: "&",
1023 ast.BitOr: "|",
1024 ast.BitXor: "^",
1025 ast.Div: "/",
1026 ast.FloorDiv: "//",
1027 ast.LShift: "<<",
1028 ast.MatMult: "@",
1029 ast.Mod: "%",
1030 ast.Mult: "*",
1031 ast.Pow: "**",
1032 ast.RShift: ">>",
1033 ast.Sub: "-",
1034}
1036_bool_op_map = {
1037 ast.And: "and",
1038 ast.Or: "or",
1039}
1041_compare_op_map = {
1042 ast.Eq: "==",
1043 ast.NotEq: "!=",
1044 ast.Lt: "<",
1045 ast.LtE: "<=",
1046 ast.Gt: ">",
1047 ast.GtE: ">=",
1048 ast.Is: "is",
1049 ast.IsNot: "is not",
1050 ast.In: "in",
1051 ast.NotIn: "not in",
1052}
1054# TODO: Support `ast.Await`.
1055_precedence_map = {
1056 # Literals and names.
1057 ExprName: lambda _: _OperatorPrecedence.ATOMIC,
1058 ExprConstant: lambda _: _OperatorPrecedence.ATOMIC,
1059 ExprJoinedStr: lambda _: _OperatorPrecedence.ATOMIC,
1060 ExprFormatted: lambda _: _OperatorPrecedence.ATOMIC,
1061 # Container displays.
1062 ExprList: lambda _: _OperatorPrecedence.ATOMIC,
1063 ExprTuple: lambda _: _OperatorPrecedence.ATOMIC,
1064 ExprSet: lambda _: _OperatorPrecedence.ATOMIC,
1065 ExprDict: lambda _: _OperatorPrecedence.ATOMIC,
1066 # Comprehensions are self-contained units that produce a container.
1067 ExprListComp: lambda _: _OperatorPrecedence.ATOMIC,
1068 ExprSetComp: lambda _: _OperatorPrecedence.ATOMIC,
1069 ExprDictComp: lambda _: _OperatorPrecedence.ATOMIC,
1070 ExprAttribute: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE,
1071 ExprSubscript: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE,
1072 ExprCall: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE,
1073 ExprUnaryOp: lambda e: {"not": _OperatorPrecedence.NOT}.get(e.operator, _OperatorPrecedence.POS_NEG_BIT_NOT),
1074 ExprBinOp: lambda e: {
1075 "**": _OperatorPrecedence.EXPONENT,
1076 "*": _OperatorPrecedence.MUL_DIV_REMAIN,
1077 "@": _OperatorPrecedence.MUL_DIV_REMAIN,
1078 "/": _OperatorPrecedence.MUL_DIV_REMAIN,
1079 "//": _OperatorPrecedence.MUL_DIV_REMAIN,
1080 "%": _OperatorPrecedence.MUL_DIV_REMAIN,
1081 "+": _OperatorPrecedence.ADD_SUB,
1082 "-": _OperatorPrecedence.ADD_SUB,
1083 "<<": _OperatorPrecedence.LEFT_RIGHT_SHIFT,
1084 ">>": _OperatorPrecedence.LEFT_RIGHT_SHIFT,
1085 "&": _OperatorPrecedence.BIT_AND,
1086 "^": _OperatorPrecedence.BIT_XOR,
1087 "|": _OperatorPrecedence.BIT_OR,
1088 }[e.operator],
1089 ExprBoolOp: lambda e: {"and": _OperatorPrecedence.AND, "or": _OperatorPrecedence.OR}[e.operator],
1090 ExprCompare: lambda _: _OperatorPrecedence.COMP_MEMB_ID,
1091 ExprIfExp: lambda _: _OperatorPrecedence.IF_ELSE,
1092 ExprNamedExpr: lambda _: _OperatorPrecedence.ASSIGN,
1093 ExprLambda: lambda _: _OperatorPrecedence.LAMBDA,
1094 # NOTE: Ruff categorizes as atomic, but `(a for a in b).c` implies its less than `CALL_ATTRIBUTE`.
1095 ExprGeneratorExp: lambda _: _OperatorPrecedence.LAMBDA,
1096 ExprVarPositional: lambda _: _OperatorPrecedence.STARRED,
1097 ExprVarKeyword: lambda _: _OperatorPrecedence.STARRED,
1098 ExprYield: lambda _: _OperatorPrecedence.YIELD,
1099 ExprYieldFrom: lambda _: _OperatorPrecedence.YIELD,
1100 # These are not standalone, they appear in specific contexts where precendence is not a concern.
1101 # NOTE: `for ... in ... if` part, not the whole `[...]`.
1102 ExprComprehension: lambda _: _OperatorPrecedence.NONE,
1103 ExprExtSlice: lambda _: _OperatorPrecedence.NONE,
1104 ExprKeyword: lambda _: _OperatorPrecedence.NONE,
1105 ExprParameter: lambda _: _OperatorPrecedence.NONE,
1106 ExprSlice: lambda _: _OperatorPrecedence.NONE,
1107}
1110def _get_precedence(expr: Expr) -> _OperatorPrecedence:
1111 return _precedence_map.get(type(expr), lambda _: _OperatorPrecedence.NONE)(expr)
1114def _build_attribute(node: ast.Attribute, parent: Module | Class, **kwargs: Any) -> Expr:
1115 left = _build(node.value, parent, **kwargs)
1116 if isinstance(left, ExprAttribute):
1117 left.append(ExprName(node.attr))
1118 return left
1119 if isinstance(left, ExprName):
1120 return ExprAttribute([left, ExprName(node.attr, left)])
1121 if isinstance(left, str):
1122 return ExprAttribute([left, ExprName(node.attr, "str")])
1123 return ExprAttribute([left, ExprName(node.attr)])
1126def _build_binop(node: ast.BinOp, parent: Module | Class, **kwargs: Any) -> Expr:
1127 return ExprBinOp(
1128 _build(node.left, parent, **kwargs),
1129 _binary_op_map[type(node.op)],
1130 _build(node.right, parent, **kwargs),
1131 )
1134def _build_boolop(node: ast.BoolOp, parent: Module | Class, **kwargs: Any) -> Expr:
1135 return ExprBoolOp(
1136 _bool_op_map[type(node.op)],
1137 [_build(value, parent, **kwargs) for value in node.values],
1138 )
1141def _build_call(node: ast.Call, parent: Module | Class, **kwargs: Any) -> Expr:
1142 function = _build(node.func, parent, **kwargs)
1143 positional_args = [_build(arg, parent, **kwargs) for arg in node.args]
1144 keyword_args = [_build(kwarg, parent, function=function, **kwargs) for kwarg in node.keywords]
1145 return ExprCall(function, [*positional_args, *keyword_args])
1148def _build_compare(node: ast.Compare, parent: Module | Class, **kwargs: Any) -> Expr:
1149 return ExprCompare(
1150 _build(node.left, parent, **kwargs),
1151 [_compare_op_map[type(op)] for op in node.ops],
1152 [_build(comp, parent, **kwargs) for comp in node.comparators],
1153 )
1156def _build_comprehension(node: ast.comprehension, parent: Module | Class, **kwargs: Any) -> Expr:
1157 return ExprComprehension(
1158 _build(node.target, parent, compr_target=True, **kwargs),
1159 _build(node.iter, parent, **kwargs),
1160 [_build(condition, parent, **kwargs) for condition in node.ifs],
1161 is_async=bool(node.is_async),
1162 )
1165def _build_constant(
1166 node: ast.Constant,
1167 parent: Module | Class,
1168 *,
1169 in_formatted_str: bool = False,
1170 in_joined_str: bool = False,
1171 parse_strings: bool = False,
1172 literal_strings: bool = False,
1173 **kwargs: Any,
1174) -> str | Expr:
1175 if isinstance(node.value, str):
1176 if in_joined_str and not in_formatted_str:
1177 # We're in a f-string, not in a formatted value, don't keep quotes.
1178 return node.value
1179 if parse_strings and not literal_strings:
1180 # We're in a place where a string could be a type annotation
1181 # (and not in a Literal[...] type annotation).
1182 # We parse the string and build from the resulting nodes again.
1183 # If we fail to parse it (syntax errors), we consider it's a literal string and log a message.
1184 try:
1185 parsed = compile(
1186 node.value,
1187 mode="eval",
1188 filename="<string-annotation>",
1189 flags=ast.PyCF_ONLY_AST,
1190 optimize=1,
1191 )
1192 except SyntaxError:
1193 logger.debug(
1194 "Tried and failed to parse %r as Python code, "
1195 "falling back to using it as a string literal "
1196 "(postponed annotations might help: https://peps.python.org/pep-0563/)",
1197 node.value,
1198 )
1199 else:
1200 return _build(parsed.body, parent, **kwargs) # ty:ignore[unresolved-attribute]
1201 return {type(...): lambda _: "..."}.get(type(node.value), repr)(node.value)
1204def _build_dict(node: ast.Dict, parent: Module | Class, **kwargs: Any) -> Expr:
1205 return ExprDict(
1206 [None if key is None else _build(key, parent, **kwargs) for key in node.keys],
1207 [_build(value, parent, **kwargs) for value in node.values],
1208 )
1211def _build_dictcomp(node: ast.DictComp, parent: Module | Class, **kwargs: Any) -> Expr:
1212 return ExprDictComp(
1213 _build(node.key, parent, **kwargs),
1214 _build(node.value, parent, **kwargs),
1215 [_build(gen, parent, **kwargs) for gen in node.generators],
1216 )
1219def _build_formatted(
1220 node: ast.FormattedValue,
1221 parent: Module | Class,
1222 *,
1223 in_formatted_str: bool = False, # noqa: ARG001
1224 **kwargs: Any,
1225) -> Expr:
1226 return ExprFormatted(_build(node.value, parent, in_formatted_str=True, **kwargs))
1229def _build_generatorexp(node: ast.GeneratorExp, parent: Module | Class, **kwargs: Any) -> Expr:
1230 return ExprGeneratorExp(
1231 _build(node.elt, parent, **kwargs),
1232 [_build(gen, parent, **kwargs) for gen in node.generators],
1233 )
1236def _build_ifexp(node: ast.IfExp, parent: Module | Class, **kwargs: Any) -> Expr:
1237 return ExprIfExp(
1238 _build(node.body, parent, **kwargs),
1239 _build(node.test, parent, **kwargs),
1240 _build(node.orelse, parent, **kwargs),
1241 )
1244def _build_joinedstr(
1245 node: ast.JoinedStr,
1246 parent: Module | Class,
1247 *,
1248 in_joined_str: bool = False, # noqa: ARG001
1249 **kwargs: Any,
1250) -> Expr:
1251 return ExprJoinedStr([_build(value, parent, in_joined_str=True, **kwargs) for value in node.values])
1254def _build_keyword(node: ast.keyword, parent: Module | Class, function: Expr | None = None, **kwargs: Any) -> Expr:
1255 if node.arg is None:
1256 return ExprVarKeyword(_build(node.value, parent, **kwargs))
1257 return ExprKeyword(node.arg, _build(node.value, parent, **kwargs), function=function)
1260def _build_lambda(node: ast.Lambda, parent: Module | Class, **kwargs: Any) -> Expr:
1261 return ExprLambda(
1262 parameters=[
1263 ExprParameter(
1264 name=name,
1265 kind=kind,
1266 annotation=None,
1267 default=default
1268 if isinstance(default, str)
1269 else safe_get_expression(default, parent=parent, parse_strings=False),
1270 )
1271 for name, _, kind, default in get_parameters(node.args)
1272 ],
1273 body=_build(node.body, parent, **kwargs),
1274 )
1277def _build_list(node: ast.List, parent: Module | Class, **kwargs: Any) -> Expr:
1278 return ExprList([_build(el, parent, **kwargs) for el in node.elts])
1281def _build_listcomp(node: ast.ListComp, parent: Module | Class, **kwargs: Any) -> Expr:
1282 return ExprListComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators])
1285def _build_name(node: ast.Name, parent: Module | Class, member: str | None = None, **kwargs: Any) -> Expr: # noqa: ARG001
1286 return ExprName(node.id, parent, member)
1289def _build_named_expr(node: ast.NamedExpr, parent: Module | Class, **kwargs: Any) -> Expr:
1290 return ExprNamedExpr(_build(node.target, parent, **kwargs), _build(node.value, parent, **kwargs))
1293def _build_set(node: ast.Set, parent: Module | Class, **kwargs: Any) -> Expr:
1294 return ExprSet([_build(el, parent, **kwargs) for el in node.elts])
1297def _build_setcomp(node: ast.SetComp, parent: Module | Class, **kwargs: Any) -> Expr:
1298 return ExprSetComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators])
1301def _build_slice(node: ast.Slice, parent: Module | Class, **kwargs: Any) -> Expr:
1302 return ExprSlice(
1303 None if node.lower is None else _build(node.lower, parent, **kwargs),
1304 None if node.upper is None else _build(node.upper, parent, **kwargs),
1305 None if node.step is None else _build(node.step, parent, **kwargs),
1306 )
1309def _build_starred(node: ast.Starred, parent: Module | Class, **kwargs: Any) -> Expr:
1310 return ExprVarPositional(_build(node.value, parent, **kwargs))
1313def _build_subscript(
1314 node: ast.Subscript,
1315 parent: Module | Class,
1316 *,
1317 parse_strings: bool = False,
1318 literal_strings: bool = False,
1319 subscript_slice: bool = False, # noqa: ARG001
1320 **kwargs: Any,
1321) -> Expr:
1322 left = _build(node.value, parent, **kwargs)
1323 if parse_strings:
1324 if isinstance(left, (ExprAttribute, ExprName)) and left.canonical_path in {
1325 "typing.Literal",
1326 "typing_extensions.Literal",
1327 }:
1328 literal_strings = True
1329 slice_expr = _build(
1330 node.slice,
1331 parent,
1332 parse_strings=True,
1333 literal_strings=literal_strings,
1334 subscript_slice=True,
1335 **kwargs,
1336 )
1337 else:
1338 slice_expr = _build(node.slice, parent, subscript_slice=True, **kwargs)
1339 return ExprSubscript(left, slice_expr)
1342def _build_tuple(
1343 node: ast.Tuple,
1344 parent: Module | Class,
1345 *,
1346 subscript_slice: bool = False,
1347 compr_target: bool = False,
1348 **kwargs: Any,
1349) -> Expr:
1350 return ExprTuple([_build(el, parent, **kwargs) for el in node.elts], implicit=subscript_slice or compr_target)
1353def _build_unaryop(node: ast.UnaryOp, parent: Module | Class, **kwargs: Any) -> Expr:
1354 return ExprUnaryOp(_unary_op_map[type(node.op)], _build(node.operand, parent, **kwargs))
1357def _build_yield(node: ast.Yield, parent: Module | Class, **kwargs: Any) -> Expr:
1358 return ExprYield(None if node.value is None else _build(node.value, parent, **kwargs))
1361def _build_yield_from(node: ast.YieldFrom, parent: Module | Class, **kwargs: Any) -> Expr:
1362 return ExprYieldFrom(_build(node.value, parent, **kwargs))
1365class _BuildCallable(Protocol):
1366 def __call__(self, node: Any, parent: Module | Class, **kwargs: Any) -> Expr: ... 1366 ↛ exitline 1366 didn't return from function '__call__' because
1369_node_map: dict[type, _BuildCallable] = {
1370 ast.Attribute: _build_attribute,
1371 ast.BinOp: _build_binop,
1372 ast.BoolOp: _build_boolop,
1373 ast.Call: _build_call,
1374 ast.Compare: _build_compare,
1375 ast.comprehension: _build_comprehension,
1376 ast.Constant: _build_constant,
1377 ast.Dict: _build_dict,
1378 ast.DictComp: _build_dictcomp,
1379 ast.FormattedValue: _build_formatted,
1380 ast.GeneratorExp: _build_generatorexp,
1381 ast.IfExp: _build_ifexp,
1382 ast.JoinedStr: _build_joinedstr,
1383 ast.keyword: _build_keyword,
1384 ast.Lambda: _build_lambda,
1385 ast.List: _build_list,
1386 ast.ListComp: _build_listcomp,
1387 ast.Name: _build_name,
1388 ast.NamedExpr: _build_named_expr,
1389 ast.Set: _build_set,
1390 ast.SetComp: _build_setcomp,
1391 ast.Slice: _build_slice,
1392 ast.Starred: _build_starred,
1393 ast.Subscript: _build_subscript,
1394 ast.Tuple: _build_tuple,
1395 ast.UnaryOp: _build_unaryop,
1396 ast.Yield: _build_yield,
1397 ast.YieldFrom: _build_yield_from,
1398} # ty:ignore[invalid-assignment]
1400if sys.version_info >= (3, 14):
1402 def _build_interpolation(node: ast.Interpolation, parent: Module | Class, **kwargs: Any) -> Expr:
1403 return ExprInterpolation(_build(node.value, parent, **kwargs))
1405 def _build_templatestr(
1406 node: ast.TemplateStr,
1407 parent: Module | Class,
1408 **kwargs: Any,
1409 ) -> Expr:
1410 return ExprTemplateStr([_build(value, parent, in_joined_str=True, **kwargs) for value in node.values])
1412 _node_map.update(
1413 {
1414 ast.Interpolation: _build_interpolation,
1415 ast.TemplateStr: _build_templatestr,
1416 },
1417 )
1420def _build(node: ast.AST, parent: Module | Class, /, **kwargs: Any) -> Expr:
1421 return _node_map[type(node)](node, parent, **kwargs)
1424def get_expression(
1425 node: ast.AST | None,
1426 parent: Module | Class,
1427 *,
1428 member: str | None = None,
1429 parse_strings: bool | None = None,
1430) -> Expr | None:
1431 """Build an expression from an AST.
1433 Parameters:
1434 node: The annotation node.
1435 parent: The parent used to resolve the name.
1436 member: The member name (for resolution in its scope).
1437 parse_strings: Whether to try and parse strings as type annotations.
1439 Returns:
1440 A string or resovable name or expression.
1441 """
1442 if node is None:
1443 return None
1444 if parse_strings is None:
1445 try:
1446 module = parent.module
1447 except ValueError:
1448 parse_strings = False
1449 else:
1450 parse_strings = not module.imports_future_annotations
1451 return _build(node, parent, member=member, parse_strings=parse_strings)
1454def safe_get_expression(
1455 node: ast.AST | None,
1456 parent: Module | Class,
1457 *,
1458 member: str | None = None,
1459 parse_strings: bool | None = None,
1460 log_level: LogLevel | None = LogLevel.error,
1461 msg_format: str = "{path}:{lineno}: Failed to get expression from {node_class}: {error}",
1462) -> Expr | None:
1463 """Safely (no exception) build a resolvable annotation.
1465 Parameters:
1466 node: The annotation node.
1467 parent: The parent used to resolve the name.
1468 member: The member name (for resolution in its scope).
1469 parse_strings: Whether to try and parse strings as type annotations.
1470 log_level: Log level to use to log a message. None to disable logging.
1471 msg_format: A format string for the log message. Available placeholders:
1472 path, lineno, node, error.
1474 Returns:
1475 A string or resovable name or expression.
1476 """
1477 try:
1478 return get_expression(node, parent, member=member, parse_strings=parse_strings)
1479 except Exception as error: # noqa: BLE001
1480 if log_level is None: 1480 ↛ 1481line 1480 didn't jump to line 1481 because the condition on line 1480 was never true
1481 return None
1482 node_class = node.__class__.__name__
1483 try:
1484 path: Path | str = parent.relative_filepath
1485 except ValueError:
1486 path = "<in-memory>"
1487 lineno = node.lineno # ty:ignore[unresolved-attribute]
1488 error_str = f"{error.__class__.__name__}: {error}"
1489 message = msg_format.format(path=path, lineno=lineno, node_class=node_class, error=error_str)
1490 getattr(logger, log_level.value)(message)
1491 return None
1494_msg_format = "{path}:{lineno}: Failed to get %s expression from {node_class}: {error}"
1495get_annotation = partial(get_expression, parse_strings=None)
1496safe_get_annotation = partial(
1497 safe_get_expression,
1498 parse_strings=None,
1499 msg_format=_msg_format % "annotation",
1500)
1501get_base_class = partial(get_expression, parse_strings=False)
1502safe_get_base_class = partial(
1503 safe_get_expression,
1504 parse_strings=False,
1505 msg_format=_msg_format % "base class",
1506)
1507get_class_keyword = partial(get_expression, parse_strings=False)
1508safe_get_class_keyword = partial(
1509 safe_get_expression,
1510 parse_strings=False,
1511 msg_format=_msg_format % "class keyword",
1512)
1513get_condition = partial(get_expression, parse_strings=False)
1514safe_get_condition = partial(
1515 safe_get_expression,
1516 parse_strings=False,
1517 msg_format=_msg_format % "condition",
1518)