Coverage for src/griffe/_internal/expressions.py: 92.26%
728 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# 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, Callable
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}
160# YORE: EOL 3.9: Remove block.
161_dataclass_opts: dict[str, bool] = {}
162if sys.version_info >= (3, 10):
163 _dataclass_opts["slots"] = True
166@dataclass
167class Expr:
168 """Base class for expressions."""
170 def __str__(self) -> str:
171 return "".join(elem if isinstance(elem, str) else elem.name for elem in self.iterate(flat=True)) # type: ignore[attr-defined]
173 def __iter__(self) -> Iterator[str | Expr]:
174 """Iterate on the expression syntax and elements."""
175 yield from self.iterate(flat=False)
177 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002
178 """Iterate on the expression elements.
180 Parameters:
181 flat: Expressions are trees.
183 When flat is false, this method iterates only on the first layer of the tree.
184 To iterate on all the subparts of the expression, you have to do so recursively.
185 It allows to handle each subpart specifically (for example subscripts, attribute, etc.),
186 without them getting rendered as strings.
188 On the contrary, when flat is true, the whole tree is flattened as a sequence
189 of strings and instances of [Names][griffe.ExprName].
191 Yields:
192 Strings and names when flat, strings and expressions otherwise.
193 """
194 yield from ()
196 def modernize(self) -> Expr:
197 """Modernize the expression.
199 For example, use PEP 604 type unions `|` instead of `typing.Union`.
201 Returns:
202 A modernized expression.
203 """
204 return self
206 def as_dict(self, **kwargs: Any) -> dict[str, Any]:
207 """Return the expression as a dictionary.
209 Parameters:
210 **kwargs: Configuration options (none available yet).
213 Returns:
214 A dictionary.
215 """
216 return _expr_as_dict(self, **kwargs)
218 @property
219 def classname(self) -> str:
220 """The expression class name."""
221 return self.__class__.__name__
223 @property
224 def path(self) -> str:
225 """Path of the expressed name/attribute."""
226 return str(self)
228 @property
229 def canonical_path(self) -> str:
230 """Path of the expressed name/attribute."""
231 return str(self)
233 @property
234 def canonical_name(self) -> str:
235 """Name of the expressed name/attribute/parameter."""
236 # We must handle things like `griffe.Visitor` and `griffe.Visitor(code)`.
237 return self.canonical_path.rsplit(".", 1)[-1].split("(", 1)[-1].removesuffix(")")
239 @property
240 def is_classvar(self) -> bool:
241 """Whether this attribute is annotated with `ClassVar`."""
242 return isinstance(self, ExprSubscript) and self.canonical_name == "ClassVar"
244 @property
245 def is_tuple(self) -> bool:
246 """Whether this expression is a tuple."""
247 return isinstance(self, ExprSubscript) and self.canonical_name.lower() == "tuple"
249 @property
250 def is_iterator(self) -> bool:
251 """Whether this expression is an iterator."""
252 return isinstance(self, ExprSubscript) and self.canonical_name == "Iterator"
254 @property
255 def is_generator(self) -> bool:
256 """Whether this expression is a generator."""
257 return isinstance(self, ExprSubscript) and self.canonical_name == "Generator"
260# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
261@dataclass(eq=True, **_dataclass_opts)
262class ExprAttribute(Expr):
263 """Attributes like `a.b`."""
265 values: list[str | Expr]
266 """The different parts of the dotted chain."""
268 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
269 precedence = _get_precedence(self)
270 yield from _yield(self.values[0], flat=flat, outer_precedence=precedence, is_left=True)
271 for value in self.values[1:]:
272 yield "."
273 yield from _yield(value, flat=flat, outer_precedence=precedence)
275 def modernize(self) -> ExprName | ExprAttribute:
276 if modern := _modern_types.get(self.canonical_path):
277 return ExprName(modern, parent=self.last.parent)
278 return self
280 def append(self, value: ExprName) -> None:
281 """Append a name to this attribute.
283 Parameters:
284 value: The expression name to append.
285 """
286 if value.parent is None: 286 ↛ 288line 286 didn't jump to line 288 because the condition on line 286 was always true
287 value.parent = self.last
288 self.values.append(value)
290 @property
291 def last(self) -> ExprName:
292 """The last part of this attribute (on the right)."""
293 # All values except the first one can *only* be names:
294 # we can't do `a.(b or c)` or `a."string"`.
295 return self.values[-1] # type: ignore[return-value]
297 @property
298 def first(self) -> str | Expr:
299 """The first part of this attribute (on the left)."""
300 return self.values[0]
302 @property
303 def path(self) -> str:
304 """The path of this attribute."""
305 return self.last.path
307 @property
308 def canonical_path(self) -> str:
309 """The canonical path of this attribute."""
310 return self.last.canonical_path
313# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
314@dataclass(eq=True, **_dataclass_opts)
315class ExprBinOp(Expr):
316 """Binary operations like `a + b`."""
318 left: str | Expr
319 """Left part."""
320 operator: str
321 """Binary operator."""
322 right: str | Expr
323 """Right part."""
325 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
326 precedence = _get_precedence(self)
327 right_precedence = precedence
328 if self.operator == "**" and isinstance(self.right, ExprUnaryOp):
329 # Unary operators on the right have higher precedence, e.g. `a ** -b`.
330 right_precedence = _OperatorPrecedence(precedence - 1)
331 yield from _yield(self.left, flat=flat, outer_precedence=precedence, is_left=True)
332 yield f" {self.operator} "
333 yield from _yield(self.right, flat=flat, outer_precedence=right_precedence, is_left=False)
336# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
337@dataclass(eq=True, **_dataclass_opts)
338class ExprBoolOp(Expr):
339 """Boolean operations like `a or b`."""
341 operator: str
342 """Boolean operator."""
343 values: Sequence[str | Expr]
344 """Operands."""
346 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
347 precedence = _get_precedence(self)
348 it = iter(self.values)
349 yield from _yield(next(it), flat=flat, outer_precedence=precedence, is_left=True)
350 for value in it:
351 yield f" {self.operator} "
352 yield from _yield(value, flat=flat, outer_precedence=precedence, is_left=False)
355# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
356@dataclass(eq=True, **_dataclass_opts)
357class ExprCall(Expr):
358 """Calls like `f()`."""
360 function: Expr
361 """Function called."""
362 arguments: Sequence[str | Expr]
363 """Passed arguments."""
365 @property
366 def canonical_path(self) -> str:
367 """The canonical path of this subscript's left part."""
368 return self.function.canonical_path
370 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
371 yield from _yield(self.function, flat=flat, outer_precedence=_get_precedence(self))
372 yield "("
373 yield from _join(self.arguments, ", ", flat=flat)
374 yield ")"
377# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
378@dataclass(eq=True, **_dataclass_opts)
379class ExprCompare(Expr):
380 """Comparisons like `a > b`."""
382 left: str | Expr
383 """Left part."""
384 operators: Sequence[str]
385 """Comparison operators."""
386 comparators: Sequence[str | Expr]
387 """Things compared."""
389 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
390 precedence = _get_precedence(self)
391 yield from _yield(self.left, flat=flat, outer_precedence=precedence, is_left=True)
392 for op, comp in zip(self.operators, self.comparators):
393 yield f" {op} "
394 yield from _yield(comp, flat=flat, outer_precedence=precedence)
397# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
398@dataclass(eq=True, **_dataclass_opts)
399class ExprComprehension(Expr):
400 """Comprehensions like `a for b in c if d`."""
402 target: str | Expr
403 """Comprehension target (value added to the result)."""
404 iterable: str | Expr
405 """Value iterated on."""
406 conditions: Sequence[str | Expr]
407 """Conditions to include the target in the result."""
408 is_async: bool = False
409 """Async comprehension or not."""
411 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
412 if self.is_async: 412 ↛ 413line 412 didn't jump to line 413 because the condition on line 412 was never true
413 yield "async "
414 yield "for "
415 yield from _yield(self.target, flat=flat)
416 yield " in "
417 yield from _yield(self.iterable, flat=flat)
418 if self.conditions: 418 ↛ 419line 418 didn't jump to line 419 because the condition on line 418 was never true
419 yield " if "
420 yield from _join(self.conditions, " if ", flat=flat)
423# TODO: `ExprConstant` is never instantiated,
424# see `_build_constant` below (it always returns the value directly).
425# Maybe we could simply get rid of it, as it wouldn't bring much value
426# if used anyway.
427# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
428@dataclass(eq=True, **_dataclass_opts)
429class ExprConstant(Expr):
430 """Constants like `"a"` or `1`."""
432 value: str
433 """Constant value."""
435 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002
436 yield self.value
439# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
440@dataclass(eq=True, **_dataclass_opts)
441class ExprDict(Expr):
442 """Dictionaries like `{"a": 0}`."""
444 keys: Sequence[str | Expr | None]
445 """Dict keys."""
446 values: Sequence[str | Expr]
447 """Dict values."""
449 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
450 yield "{"
451 yield from _join(
452 (("None" if key is None else key, ": ", value) for key, value in zip(self.keys, self.values)),
453 ", ",
454 flat=flat,
455 )
456 yield "}"
459# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
460@dataclass(eq=True, **_dataclass_opts)
461class ExprDictComp(Expr):
462 """Dict comprehensions like `{k: v for k, v in a}`."""
464 key: str | Expr
465 """Target key."""
466 value: str | Expr
467 """Target value."""
468 generators: Sequence[Expr]
469 """Generators iterated on."""
471 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
472 yield "{"
473 yield from _yield(self.key, flat=flat)
474 yield ": "
475 yield from _yield(self.value, flat=flat)
476 yield from _join(self.generators, " ", flat=flat)
477 yield "}"
480# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
481@dataclass(eq=True, **_dataclass_opts)
482class ExprExtSlice(Expr):
483 """Extended slice like `a[x:y, z]`."""
485 dims: Sequence[str | Expr]
486 """Dims."""
488 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
489 yield from _join(self.dims, ", ", flat=flat)
492# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
493@dataclass(eq=True, **_dataclass_opts)
494class ExprFormatted(Expr):
495 """Formatted string like `{1 + 1}`."""
497 value: str | Expr
498 """Formatted value."""
500 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
501 yield "{"
502 # Prevent parentheses from being added, avoiding `{(1 + 1)}`
503 yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
504 yield "}"
507# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
508@dataclass(eq=True, **_dataclass_opts)
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# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
524@dataclass(eq=True, **_dataclass_opts)
525class ExprIfExp(Expr):
526 """Conditions like `a if b else c`."""
528 body: str | Expr
529 """Value if test."""
530 test: str | Expr
531 """Condition."""
532 orelse: str | Expr
533 """Other expression."""
535 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
536 precedence = _get_precedence(self)
537 yield from _yield(self.body, flat=flat, outer_precedence=precedence, is_left=True)
538 yield " if "
539 # If the test itself is another if/else, its precedence is the same, which will not give
540 # a parenthesis: force it.
541 test_outer_precedence = _OperatorPrecedence(precedence + 1)
542 yield from _yield(self.test, flat=flat, outer_precedence=test_outer_precedence)
543 yield " else "
544 # If/else is right associative. For example, a nested if/else
545 # `a if b else c if d else e` is effectively `a if b else (c if d else e)`, so produce a
546 # flattened version without parentheses.
547 if isinstance(self.orelse, ExprIfExp): 547 ↛ 548line 547 didn't jump to line 548 because the condition on line 547 was never true
548 yield from self.orelse.iterate(flat=flat)
549 else:
550 yield from _yield(self.orelse, flat=flat, outer_precedence=precedence, is_left=False)
553# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
554@dataclass(eq=True, **_dataclass_opts)
555class ExprJoinedStr(Expr):
556 """Joined strings like `f"a {b} c"`."""
558 values: Sequence[str | Expr]
559 """Joined values."""
561 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
562 yield "f'"
563 yield from _join(self.values, "", flat=flat)
564 yield "'"
567# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
568@dataclass(eq=True, **_dataclass_opts)
569class ExprKeyword(Expr):
570 """Keyword arguments like `a=b`."""
572 name: str
573 """Name."""
574 value: str | Expr
575 """Value."""
577 # Griffe is designed around accessing Python objects
578 # with the dot notation, for example `module.Class`.
579 # Function parameters were not taken into account
580 # because they are not accessible the same way.
581 # But we still want to be able to cross-reference
582 # documentation of function parameters in downstream
583 # tools like mkdocstrings. So we add a special case
584 # for keyword expressions, where they get a meaningful
585 # canonical path (contrary to most other expressions that
586 # aren't or do not begin with names or attributes)
587 # of the form `path.to.called_function(param_name)`.
588 # For this we need to store a reference to the `func` part
589 # of the call expression in the keyword one,
590 # hence the following field.
591 # We allow it to be None for backward compatibility.
592 function: Expr | None = None
593 """Expression referencing the function called with this parameter."""
595 @property
596 def canonical_path(self) -> str:
597 """Path of the expressed keyword."""
598 if self.function:
599 return f"{self.function.canonical_path}({self.name})"
600 return super(ExprKeyword, self).canonical_path # noqa: UP008
602 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
603 yield self.name
604 yield "="
605 yield from _yield(self.value, flat=flat)
608# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
609@dataclass(eq=True, **_dataclass_opts)
610class ExprVarPositional(Expr):
611 """Variadic positional parameters like `*args`."""
613 value: Expr
614 """Starred value."""
616 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
617 yield "*"
618 yield from _yield(self.value, flat=flat)
621# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
622@dataclass(eq=True, **_dataclass_opts)
623class ExprVarKeyword(Expr):
624 """Variadic keyword parameters like `**kwargs`."""
626 value: Expr
627 """Double-starred value."""
629 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
630 yield "**"
631 yield from _yield(self.value, flat=flat)
634# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
635@dataclass(eq=True, **_dataclass_opts)
636class ExprLambda(Expr):
637 """Lambda expressions like `lambda a: a.b`."""
639 parameters: Sequence[ExprParameter]
640 """Lambda's parameters."""
641 body: str | Expr
642 """Lambda's body."""
644 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
645 pos_only = False
646 pos_or_kw = False
647 kw_only = False
648 length = len(self.parameters)
649 yield "lambda"
650 if length:
651 yield " "
652 for index, parameter in enumerate(self.parameters, 1):
653 if parameter.kind is ParameterKind.positional_only:
654 pos_only = True
655 elif parameter.kind is ParameterKind.var_positional:
656 yield "*"
657 elif parameter.kind is ParameterKind.var_keyword:
658 yield "**"
659 elif parameter.kind is ParameterKind.positional_or_keyword and not pos_or_kw:
660 pos_or_kw = True
661 elif parameter.kind is ParameterKind.keyword_only and not kw_only:
662 kw_only = True
663 yield "*, "
664 if parameter.kind is not ParameterKind.positional_only and pos_only:
665 pos_only = False
666 yield "/, "
667 yield parameter.name
668 if parameter.default and parameter.kind not in (ParameterKind.var_positional, ParameterKind.var_keyword):
669 yield "="
670 yield from _yield(parameter.default, flat=flat)
671 if index < length:
672 yield ", "
673 yield ": "
674 # Body of lambda should not have parentheses, avoiding `lambda: a.b`
675 yield from _yield(self.body, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
678# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
679@dataclass(eq=True, **_dataclass_opts)
680class ExprList(Expr):
681 """Lists like `[0, 1, 2]`."""
683 elements: Sequence[Expr]
684 """List elements."""
686 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
687 yield "["
688 yield from _join(self.elements, ", ", flat=flat)
689 yield "]"
692# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
693@dataclass(eq=True, **_dataclass_opts)
694class ExprListComp(Expr):
695 """List comprehensions like `[a for b in c]`."""
697 element: str | Expr
698 """Target value."""
699 generators: Sequence[Expr]
700 """Generators iterated on."""
702 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
703 yield "["
704 yield from _yield(self.element, flat=flat)
705 yield " "
706 yield from _join(self.generators, " ", flat=flat)
707 yield "]"
710# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
711@dataclass(eq=False, **_dataclass_opts)
712class ExprName(Expr): # noqa: PLW1641
713 """This class represents a Python object identified by a name in a given scope."""
715 name: str
716 """Actual name."""
717 parent: str | ExprName | Module | Class | Function | None = None
718 """Parent (for resolution in its scope)."""
719 member: str | None = None
720 """Member name (for resolution in its scope)."""
722 def __eq__(self, other: object) -> bool:
723 """Two name expressions are equal if they have the same `name` value (`parent` is ignored)."""
724 if isinstance(other, ExprName):
725 return self.name == other.name
726 return NotImplemented
728 def iterate(self, *, flat: bool = True) -> Iterator[ExprName]: # noqa: ARG002
729 yield self
731 def modernize(self) -> ExprName:
732 if modern := _modern_types.get(self.canonical_path):
733 return ExprName(modern, parent=self.parent)
734 return self
736 @property
737 def path(self) -> str:
738 """The full, resolved name.
740 If it was given when creating the name, return that.
741 If a callable was given, call it and return its result.
742 It the name cannot be resolved, return the source.
743 """
744 if isinstance(self.parent, ExprName):
745 return f"{self.parent.path}.{self.name}"
746 return self.name
748 @property
749 def canonical_path(self) -> str:
750 """The canonical name (resolved one, not alias name)."""
751 if self.parent is None: 751 ↛ 752line 751 didn't jump to line 752 because the condition on line 751 was never true
752 return self.name
753 if isinstance(self.parent, ExprName):
754 return f"{self.parent.canonical_path}.{self.name}"
755 if isinstance(self.parent, str): 755 ↛ 756line 755 didn't jump to line 756 because the condition on line 755 was never true
756 return f"{self.parent}.{self.name}"
757 parent = self.parent.members.get(self.member, self.parent) # type: ignore[arg-type]
758 try:
759 return parent.resolve(self.name)
760 except NameResolutionError:
761 return self.name
763 @property
764 def resolved(self) -> Module | Class | None:
765 """The resolved object this name refers to."""
766 try:
767 return self.parent.modules_collection[self.parent.resolve(self.name)] # type: ignore[union-attr]
768 except Exception: # noqa: BLE001
769 return self.parent.resolved[self.name] # type: ignore[union-attr,index]
771 @property
772 def is_enum_class(self) -> bool:
773 """Whether this name resolves to an enumeration class."""
774 try:
775 bases = self.resolved.bases # type: ignore[union-attr]
776 except Exception: # noqa: BLE001
777 return False
779 # TODO: Support inheritance?
780 # TODO: Support `StrEnum` and `IntEnum`.
781 return any(isinstance(base, Expr) and base.canonical_path == "enum.Enum" for base in bases)
783 @property
784 def is_enum_instance(self) -> bool:
785 """Whether this name resolves to an enumeration instance."""
786 try:
787 return self.parent.is_enum_class # type: ignore[union-attr]
788 except Exception: # noqa: BLE001
789 return False
791 @property
792 def is_enum_value(self) -> bool:
793 """Whether this name resolves to an enumeration value."""
794 try:
795 return self.name == "value" and self.parent.is_enum_instance # type: ignore[union-attr]
796 except Exception: # noqa: BLE001
797 return False
799 @property
800 def is_type_parameter(self) -> bool:
801 """Whether this name resolves to a type parameter."""
802 return "[" in self.canonical_path
805# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
806@dataclass(eq=True, **_dataclass_opts)
807class ExprNamedExpr(Expr):
808 """Named/assignment expressions like `a := b`."""
810 target: Expr
811 """Target name."""
812 value: str | Expr
813 """Value."""
815 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
816 yield from _yield(self.target, flat=flat)
817 yield " := "
818 yield from _yield(self.value, flat=flat)
821# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
822@dataclass(eq=True, **_dataclass_opts)
823class ExprParameter(Expr):
824 """Parameters in function signatures like `a: int = 0`."""
826 name: str
827 """Parameter name."""
828 kind: ParameterKind = ParameterKind.positional_or_keyword
829 """Parameter kind."""
830 annotation: Expr | None = None
831 """Parameter type."""
832 default: str | Expr | None = None
833 """Parameter default."""
836# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
837@dataclass(eq=True, **_dataclass_opts)
838class ExprSet(Expr):
839 """Sets like `{0, 1, 2}`."""
841 elements: Sequence[str | Expr]
842 """Set elements."""
844 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
845 yield "{"
846 yield from _join(self.elements, ", ", flat=flat)
847 yield "}"
850# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
851@dataclass(eq=True, **_dataclass_opts)
852class ExprSetComp(Expr):
853 """Set comprehensions like `{a for b in c}`."""
855 element: str | Expr
856 """Target value."""
857 generators: Sequence[Expr]
858 """Generators iterated on."""
860 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
861 yield "{"
862 yield from _yield(self.element, flat=flat)
863 yield " "
864 yield from _join(self.generators, " ", flat=flat)
865 yield "}"
868# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
869@dataclass(eq=True, **_dataclass_opts)
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# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
892@dataclass(eq=True, **_dataclass_opts)
893class ExprSubscript(Expr):
894 """Subscripts like `a[b]`."""
896 left: str | Expr
897 """Left part."""
898 slice: str | Expr
899 """Slice part."""
901 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
902 yield from _yield(self.left, flat=flat, outer_precedence=_get_precedence(self))
903 yield "["
904 # Prevent parentheses from being added, avoiding `a[(b)]`
905 yield from _yield(self.slice, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
906 yield "]"
908 @staticmethod
909 def _to_binop(elements: Sequence[Expr], op: str) -> ExprBinOp:
910 if len(elements) == 2: # noqa: PLR2004
911 left, right = elements
912 if isinstance(left, Expr): 912 ↛ 914line 912 didn't jump to line 914 because the condition on line 912 was always true
913 left = left.modernize()
914 if isinstance(right, Expr): 914 ↛ 916line 914 didn't jump to line 916 because the condition on line 914 was always true
915 right = right.modernize()
916 return ExprBinOp(left=left, operator=op, right=right)
918 left = ExprSubscript._to_binop(elements[:-1], op=op)
919 right = elements[-1]
920 if isinstance(right, Expr): 920 ↛ 922line 920 didn't jump to line 922 because the condition on line 920 was always true
921 right = right.modernize()
922 return ExprBinOp(left=left, operator=op, right=right)
924 def modernize(self) -> ExprBinOp | ExprSubscript:
925 if self.canonical_path == "typing.Union":
926 return self._to_binop(self.slice.elements, op="|") # type: ignore[union-attr]
927 if self.canonical_path == "typing.Optional":
928 left = self.slice if isinstance(self.slice, str) else self.slice.modernize()
929 return ExprBinOp(left=left, operator="|", right="None")
930 return ExprSubscript(
931 left=self.left if isinstance(self.left, str) else self.left.modernize(),
932 slice=self.slice if isinstance(self.slice, str) else self.slice.modernize(),
933 )
935 @property
936 def path(self) -> str:
937 """The path of this subscript's left part."""
938 if isinstance(self.left, str):
939 return self.left
940 return self.left.path
942 @property
943 def canonical_path(self) -> str:
944 """The canonical path of this subscript's left part."""
945 if isinstance(self.left, str): 945 ↛ 946line 945 didn't jump to line 946 because the condition on line 945 was never true
946 return self.left
947 return self.left.canonical_path
950# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
951@dataclass(eq=True, **_dataclass_opts)
952class ExprTuple(Expr):
953 """Tuples like `(0, 1, 2)`."""
955 elements: Sequence[str | Expr]
956 """Tuple elements."""
957 implicit: bool = False
958 """Whether the tuple is implicit (e.g. without parentheses in a subscript's slice)."""
960 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
961 if not self.implicit:
962 yield "("
963 yield from _join(self.elements, ", ", flat=flat)
964 if len(self.elements) == 1:
965 yield ","
966 if not self.implicit:
967 yield ")"
969 def modernize(self) -> ExprTuple:
970 return ExprTuple(
971 elements=[el if isinstance(el, str) else el.modernize() for el in self.elements],
972 implicit=self.implicit,
973 )
976# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
977@dataclass(eq=True, **_dataclass_opts)
978class ExprUnaryOp(Expr):
979 """Unary operations like `-1`."""
981 operator: str
982 """Unary operator."""
983 value: str | Expr
984 """Value."""
986 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
987 yield self.operator
988 if self.operator == "not":
989 yield " "
990 yield from _yield(self.value, flat=flat, outer_precedence=_get_precedence(self))
993# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
994@dataclass(eq=True, **_dataclass_opts)
995class ExprYield(Expr):
996 """Yield statements like `yield a`."""
998 value: str | Expr | None = None
999 """Yielded value."""
1001 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
1002 yield "yield"
1003 if self.value is not None:
1004 yield " "
1005 yield from _yield(self.value, flat=flat)
1008# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
1009@dataclass(eq=True, **_dataclass_opts)
1010class ExprYieldFrom(Expr):
1011 """Yield statements like `yield from a`."""
1013 value: str | Expr
1014 """Yielded-from value."""
1016 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
1017 yield "yield from "
1018 yield from _yield(self.value, flat=flat)
1021_unary_op_map = {
1022 ast.Invert: "~",
1023 ast.Not: "not",
1024 ast.UAdd: "+",
1025 ast.USub: "-",
1026}
1028_binary_op_map = {
1029 ast.Add: "+",
1030 ast.BitAnd: "&",
1031 ast.BitOr: "|",
1032 ast.BitXor: "^",
1033 ast.Div: "/",
1034 ast.FloorDiv: "//",
1035 ast.LShift: "<<",
1036 ast.MatMult: "@",
1037 ast.Mod: "%",
1038 ast.Mult: "*",
1039 ast.Pow: "**",
1040 ast.RShift: ">>",
1041 ast.Sub: "-",
1042}
1044_bool_op_map = {
1045 ast.And: "and",
1046 ast.Or: "or",
1047}
1049_compare_op_map = {
1050 ast.Eq: "==",
1051 ast.NotEq: "!=",
1052 ast.Lt: "<",
1053 ast.LtE: "<=",
1054 ast.Gt: ">",
1055 ast.GtE: ">=",
1056 ast.Is: "is",
1057 ast.IsNot: "is not",
1058 ast.In: "in",
1059 ast.NotIn: "not in",
1060}
1062# TODO: Support `ast.Await`.
1063_precedence_map = {
1064 # Literals and names.
1065 ExprName: lambda _: _OperatorPrecedence.ATOMIC,
1066 ExprConstant: lambda _: _OperatorPrecedence.ATOMIC,
1067 ExprJoinedStr: lambda _: _OperatorPrecedence.ATOMIC,
1068 ExprFormatted: lambda _: _OperatorPrecedence.ATOMIC,
1069 # Container displays.
1070 ExprList: lambda _: _OperatorPrecedence.ATOMIC,
1071 ExprTuple: lambda _: _OperatorPrecedence.ATOMIC,
1072 ExprSet: lambda _: _OperatorPrecedence.ATOMIC,
1073 ExprDict: lambda _: _OperatorPrecedence.ATOMIC,
1074 # Comprehensions are self-contained units that produce a container.
1075 ExprListComp: lambda _: _OperatorPrecedence.ATOMIC,
1076 ExprSetComp: lambda _: _OperatorPrecedence.ATOMIC,
1077 ExprDictComp: lambda _: _OperatorPrecedence.ATOMIC,
1078 ExprAttribute: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE,
1079 ExprSubscript: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE,
1080 ExprCall: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE,
1081 ExprUnaryOp: lambda e: {"not": _OperatorPrecedence.NOT}.get(e.operator, _OperatorPrecedence.POS_NEG_BIT_NOT),
1082 ExprBinOp: lambda e: {
1083 "**": _OperatorPrecedence.EXPONENT,
1084 "*": _OperatorPrecedence.MUL_DIV_REMAIN,
1085 "@": _OperatorPrecedence.MUL_DIV_REMAIN,
1086 "/": _OperatorPrecedence.MUL_DIV_REMAIN,
1087 "//": _OperatorPrecedence.MUL_DIV_REMAIN,
1088 "%": _OperatorPrecedence.MUL_DIV_REMAIN,
1089 "+": _OperatorPrecedence.ADD_SUB,
1090 "-": _OperatorPrecedence.ADD_SUB,
1091 "<<": _OperatorPrecedence.LEFT_RIGHT_SHIFT,
1092 ">>": _OperatorPrecedence.LEFT_RIGHT_SHIFT,
1093 "&": _OperatorPrecedence.BIT_AND,
1094 "^": _OperatorPrecedence.BIT_XOR,
1095 "|": _OperatorPrecedence.BIT_OR,
1096 }[e.operator],
1097 ExprBoolOp: lambda e: {"and": _OperatorPrecedence.AND, "or": _OperatorPrecedence.OR}[e.operator],
1098 ExprCompare: lambda _: _OperatorPrecedence.COMP_MEMB_ID,
1099 ExprIfExp: lambda _: _OperatorPrecedence.IF_ELSE,
1100 ExprNamedExpr: lambda _: _OperatorPrecedence.ASSIGN,
1101 ExprLambda: lambda _: _OperatorPrecedence.LAMBDA,
1102 # NOTE: Ruff categorizes as atomic, but `(a for a in b).c` implies its less than `CALL_ATTRIBUTE`.
1103 ExprGeneratorExp: lambda _: _OperatorPrecedence.LAMBDA,
1104 ExprVarPositional: lambda _: _OperatorPrecedence.STARRED,
1105 ExprVarKeyword: lambda _: _OperatorPrecedence.STARRED,
1106 ExprYield: lambda _: _OperatorPrecedence.YIELD,
1107 ExprYieldFrom: lambda _: _OperatorPrecedence.YIELD,
1108 # These are not standalone, they appear in specific contexts where precendence is not a concern.
1109 # NOTE: `for ... in ... if` part, not the whole `[...]`.
1110 ExprComprehension: lambda _: _OperatorPrecedence.NONE,
1111 ExprExtSlice: lambda _: _OperatorPrecedence.NONE,
1112 ExprKeyword: lambda _: _OperatorPrecedence.NONE,
1113 ExprParameter: lambda _: _OperatorPrecedence.NONE,
1114 ExprSlice: lambda _: _OperatorPrecedence.NONE,
1115}
1118def _get_precedence(expr: Expr) -> _OperatorPrecedence:
1119 return _precedence_map.get(type(expr), lambda _: _OperatorPrecedence.NONE)(expr)
1122def _build_attribute(node: ast.Attribute, parent: Module | Class, **kwargs: Any) -> Expr:
1123 left = _build(node.value, parent, **kwargs)
1124 if isinstance(left, ExprAttribute):
1125 left.append(ExprName(node.attr))
1126 return left
1127 if isinstance(left, ExprName):
1128 return ExprAttribute([left, ExprName(node.attr, left)])
1129 if isinstance(left, str):
1130 return ExprAttribute([left, ExprName(node.attr, "str")])
1131 return ExprAttribute([left, ExprName(node.attr)])
1134def _build_binop(node: ast.BinOp, parent: Module | Class, **kwargs: Any) -> Expr:
1135 return ExprBinOp(
1136 _build(node.left, parent, **kwargs),
1137 _binary_op_map[type(node.op)],
1138 _build(node.right, parent, **kwargs),
1139 )
1142def _build_boolop(node: ast.BoolOp, parent: Module | Class, **kwargs: Any) -> Expr:
1143 return ExprBoolOp(
1144 _bool_op_map[type(node.op)],
1145 [_build(value, parent, **kwargs) for value in node.values],
1146 )
1149def _build_call(node: ast.Call, parent: Module | Class, **kwargs: Any) -> Expr:
1150 function = _build(node.func, parent, **kwargs)
1151 positional_args = [_build(arg, parent, **kwargs) for arg in node.args]
1152 keyword_args = [_build(kwarg, parent, function=function, **kwargs) for kwarg in node.keywords]
1153 return ExprCall(function, [*positional_args, *keyword_args])
1156def _build_compare(node: ast.Compare, parent: Module | Class, **kwargs: Any) -> Expr:
1157 return ExprCompare(
1158 _build(node.left, parent, **kwargs),
1159 [_compare_op_map[type(op)] for op in node.ops],
1160 [_build(comp, parent, **kwargs) for comp in node.comparators],
1161 )
1164def _build_comprehension(node: ast.comprehension, parent: Module | Class, **kwargs: Any) -> Expr:
1165 return ExprComprehension(
1166 _build(node.target, parent, **kwargs),
1167 _build(node.iter, parent, **kwargs),
1168 [_build(condition, parent, **kwargs) for condition in node.ifs],
1169 is_async=bool(node.is_async),
1170 )
1173def _build_constant(
1174 node: ast.Constant,
1175 parent: Module | Class,
1176 *,
1177 in_formatted_str: bool = False,
1178 in_joined_str: bool = False,
1179 parse_strings: bool = False,
1180 literal_strings: bool = False,
1181 **kwargs: Any,
1182) -> str | Expr:
1183 if isinstance(node.value, str):
1184 if in_joined_str and not in_formatted_str:
1185 # We're in a f-string, not in a formatted value, don't keep quotes.
1186 return node.value
1187 if parse_strings and not literal_strings:
1188 # We're in a place where a string could be a type annotation
1189 # (and not in a Literal[...] type annotation).
1190 # We parse the string and build from the resulting nodes again.
1191 # If we fail to parse it (syntax errors), we consider it's a literal string and log a message.
1192 try:
1193 parsed = compile(
1194 node.value,
1195 mode="eval",
1196 filename="<string-annotation>",
1197 flags=ast.PyCF_ONLY_AST,
1198 optimize=1,
1199 )
1200 except SyntaxError:
1201 logger.debug(
1202 "Tried and failed to parse %r as Python code, "
1203 "falling back to using it as a string literal "
1204 "(postponed annotations might help: https://peps.python.org/pep-0563/)",
1205 node.value,
1206 )
1207 else:
1208 return _build(parsed.body, parent, **kwargs) # type: ignore[attr-defined]
1209 return {type(...): lambda _: "..."}.get(type(node.value), repr)(node.value) # type: ignore[arg-type]
1212def _build_dict(node: ast.Dict, parent: Module | Class, **kwargs: Any) -> Expr:
1213 return ExprDict(
1214 [None if key is None else _build(key, parent, **kwargs) for key in node.keys],
1215 [_build(value, parent, **kwargs) for value in node.values],
1216 )
1219def _build_dictcomp(node: ast.DictComp, parent: Module | Class, **kwargs: Any) -> Expr:
1220 return ExprDictComp(
1221 _build(node.key, parent, **kwargs),
1222 _build(node.value, parent, **kwargs),
1223 [_build(gen, parent, **kwargs) for gen in node.generators],
1224 )
1227def _build_formatted(
1228 node: ast.FormattedValue,
1229 parent: Module | Class,
1230 *,
1231 in_formatted_str: bool = False, # noqa: ARG001
1232 **kwargs: Any,
1233) -> Expr:
1234 return ExprFormatted(_build(node.value, parent, in_formatted_str=True, **kwargs))
1237def _build_generatorexp(node: ast.GeneratorExp, parent: Module | Class, **kwargs: Any) -> Expr:
1238 return ExprGeneratorExp(
1239 _build(node.elt, parent, **kwargs),
1240 [_build(gen, parent, **kwargs) for gen in node.generators],
1241 )
1244def _build_ifexp(node: ast.IfExp, parent: Module | Class, **kwargs: Any) -> Expr:
1245 return ExprIfExp(
1246 _build(node.body, parent, **kwargs),
1247 _build(node.test, parent, **kwargs),
1248 _build(node.orelse, parent, **kwargs),
1249 )
1252def _build_joinedstr(
1253 node: ast.JoinedStr,
1254 parent: Module | Class,
1255 *,
1256 in_joined_str: bool = False, # noqa: ARG001
1257 **kwargs: Any,
1258) -> Expr:
1259 return ExprJoinedStr([_build(value, parent, in_joined_str=True, **kwargs) for value in node.values])
1262def _build_keyword(node: ast.keyword, parent: Module | Class, function: Expr | None = None, **kwargs: Any) -> Expr:
1263 if node.arg is None:
1264 return ExprVarKeyword(_build(node.value, parent, **kwargs))
1265 return ExprKeyword(node.arg, _build(node.value, parent, **kwargs), function=function)
1268def _build_lambda(node: ast.Lambda, parent: Module | Class, **kwargs: Any) -> Expr:
1269 return ExprLambda(
1270 parameters=[
1271 ExprParameter(
1272 name=name,
1273 kind=kind,
1274 annotation=None,
1275 default=default
1276 if isinstance(default, str)
1277 else safe_get_expression(default, parent=parent, parse_strings=False),
1278 )
1279 for name, _, kind, default in get_parameters(node.args)
1280 ],
1281 body=_build(node.body, parent, **kwargs),
1282 )
1285def _build_list(node: ast.List, parent: Module | Class, **kwargs: Any) -> Expr:
1286 return ExprList([_build(el, parent, **kwargs) for el in node.elts])
1289def _build_listcomp(node: ast.ListComp, parent: Module | Class, **kwargs: Any) -> Expr:
1290 return ExprListComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators])
1293def _build_name(node: ast.Name, parent: Module | Class, member: str | None = None, **kwargs: Any) -> Expr: # noqa: ARG001
1294 return ExprName(node.id, parent, member)
1297def _build_named_expr(node: ast.NamedExpr, parent: Module | Class, **kwargs: Any) -> Expr:
1298 return ExprNamedExpr(_build(node.target, parent, **kwargs), _build(node.value, parent, **kwargs))
1301def _build_set(node: ast.Set, parent: Module | Class, **kwargs: Any) -> Expr:
1302 return ExprSet([_build(el, parent, **kwargs) for el in node.elts])
1305def _build_setcomp(node: ast.SetComp, parent: Module | Class, **kwargs: Any) -> Expr:
1306 return ExprSetComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators])
1309def _build_slice(node: ast.Slice, parent: Module | Class, **kwargs: Any) -> Expr:
1310 return ExprSlice(
1311 None if node.lower is None else _build(node.lower, parent, **kwargs),
1312 None if node.upper is None else _build(node.upper, parent, **kwargs),
1313 None if node.step is None else _build(node.step, parent, **kwargs),
1314 )
1317def _build_starred(node: ast.Starred, parent: Module | Class, **kwargs: Any) -> Expr:
1318 return ExprVarPositional(_build(node.value, parent, **kwargs))
1321def _build_subscript(
1322 node: ast.Subscript,
1323 parent: Module | Class,
1324 *,
1325 parse_strings: bool = False,
1326 literal_strings: bool = False,
1327 in_subscript: bool = False, # noqa: ARG001
1328 **kwargs: Any,
1329) -> Expr:
1330 left = _build(node.value, parent, **kwargs)
1331 if parse_strings:
1332 if isinstance(left, (ExprAttribute, ExprName)) and left.canonical_path in {
1333 "typing.Literal",
1334 "typing_extensions.Literal",
1335 }:
1336 literal_strings = True
1337 slice_expr = _build(
1338 node.slice,
1339 parent,
1340 parse_strings=True,
1341 literal_strings=literal_strings,
1342 in_subscript=True,
1343 **kwargs,
1344 )
1345 else:
1346 slice_expr = _build(node.slice, parent, in_subscript=True, **kwargs)
1347 return ExprSubscript(left, slice_expr)
1350def _build_tuple(
1351 node: ast.Tuple,
1352 parent: Module | Class,
1353 *,
1354 in_subscript: bool = False,
1355 **kwargs: Any,
1356) -> Expr:
1357 return ExprTuple([_build(el, parent, **kwargs) for el in node.elts], implicit=in_subscript)
1360def _build_unaryop(node: ast.UnaryOp, parent: Module | Class, **kwargs: Any) -> Expr:
1361 return ExprUnaryOp(_unary_op_map[type(node.op)], _build(node.operand, parent, **kwargs))
1364def _build_yield(node: ast.Yield, parent: Module | Class, **kwargs: Any) -> Expr:
1365 return ExprYield(None if node.value is None else _build(node.value, parent, **kwargs))
1368def _build_yield_from(node: ast.YieldFrom, parent: Module | Class, **kwargs: Any) -> Expr:
1369 return ExprYieldFrom(_build(node.value, parent, **kwargs))
1372_node_map: dict[type, Callable[[Any, Module | Class], Expr]] = {
1373 ast.Attribute: _build_attribute,
1374 ast.BinOp: _build_binop,
1375 ast.BoolOp: _build_boolop,
1376 ast.Call: _build_call,
1377 ast.Compare: _build_compare,
1378 ast.comprehension: _build_comprehension,
1379 ast.Constant: _build_constant, # type: ignore[dict-item]
1380 ast.Dict: _build_dict,
1381 ast.DictComp: _build_dictcomp,
1382 ast.FormattedValue: _build_formatted,
1383 ast.GeneratorExp: _build_generatorexp,
1384 ast.IfExp: _build_ifexp,
1385 ast.JoinedStr: _build_joinedstr,
1386 ast.keyword: _build_keyword,
1387 ast.Lambda: _build_lambda,
1388 ast.List: _build_list,
1389 ast.ListComp: _build_listcomp,
1390 ast.Name: _build_name,
1391 ast.NamedExpr: _build_named_expr,
1392 ast.Set: _build_set,
1393 ast.SetComp: _build_setcomp,
1394 ast.Slice: _build_slice,
1395 ast.Starred: _build_starred,
1396 ast.Subscript: _build_subscript,
1397 ast.Tuple: _build_tuple,
1398 ast.UnaryOp: _build_unaryop,
1399 ast.Yield: _build_yield,
1400 ast.YieldFrom: _build_yield_from,
1401}
1404def _build(node: ast.AST, parent: Module | Class, /, **kwargs: Any) -> Expr:
1405 return _node_map[type(node)](node, parent, **kwargs)
1408def get_expression(
1409 node: ast.AST | None,
1410 parent: Module | Class,
1411 *,
1412 member: str | None = None,
1413 parse_strings: bool | None = None,
1414) -> Expr | None:
1415 """Build an expression from an AST.
1417 Parameters:
1418 node: The annotation node.
1419 parent: The parent used to resolve the name.
1420 member: The member name (for resolution in its scope).
1421 parse_strings: Whether to try and parse strings as type annotations.
1423 Returns:
1424 A string or resovable name or expression.
1425 """
1426 if node is None:
1427 return None
1428 if parse_strings is None:
1429 try:
1430 module = parent.module
1431 except ValueError:
1432 parse_strings = False
1433 else:
1434 parse_strings = not module.imports_future_annotations
1435 return _build(node, parent, member=member, parse_strings=parse_strings)
1438def safe_get_expression(
1439 node: ast.AST | None,
1440 parent: Module | Class,
1441 *,
1442 member: str | None = None,
1443 parse_strings: bool | None = None,
1444 log_level: LogLevel | None = LogLevel.error,
1445 msg_format: str = "{path}:{lineno}: Failed to get expression from {node_class}: {error}",
1446) -> Expr | None:
1447 """Safely (no exception) build a resolvable annotation.
1449 Parameters:
1450 node: The annotation node.
1451 parent: The parent used to resolve the name.
1452 member: The member name (for resolution in its scope).
1453 parse_strings: Whether to try and parse strings as type annotations.
1454 log_level: Log level to use to log a message. None to disable logging.
1455 msg_format: A format string for the log message. Available placeholders:
1456 path, lineno, node, error.
1458 Returns:
1459 A string or resovable name or expression.
1460 """
1461 try:
1462 return get_expression(node, parent, member=member, parse_strings=parse_strings)
1463 except Exception as error: # noqa: BLE001
1464 if log_level is None: 1464 ↛ 1465line 1464 didn't jump to line 1465 because the condition on line 1464 was never true
1465 return None
1466 node_class = node.__class__.__name__
1467 try:
1468 path: Path | str = parent.relative_filepath
1469 except ValueError:
1470 path = "<in-memory>"
1471 lineno = node.lineno # type: ignore[union-attr]
1472 error_str = f"{error.__class__.__name__}: {error}"
1473 message = msg_format.format(path=path, lineno=lineno, node_class=node_class, error=error_str)
1474 getattr(logger, log_level.value)(message)
1475 return None
1478_msg_format = "{path}:{lineno}: Failed to get %s expression from {node_class}: {error}"
1479get_annotation = partial(get_expression, parse_strings=None)
1480safe_get_annotation = partial(
1481 safe_get_expression,
1482 parse_strings=None,
1483 msg_format=_msg_format % "annotation",
1484)
1485get_base_class = partial(get_expression, parse_strings=False)
1486safe_get_base_class = partial(
1487 safe_get_expression,
1488 parse_strings=False,
1489 msg_format=_msg_format % "base class",
1490)
1491get_condition = partial(get_expression, parse_strings=False)
1492safe_get_condition = partial(
1493 safe_get_expression,
1494 parse_strings=False,
1495 msg_format=_msg_format % "condition",
1496)