Coverage for src/_griffe/expressions.py: 86.96%
627 statements
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
1# 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 functools import partial
14from itertools import zip_longest
15from typing import TYPE_CHECKING, Any, Callable
17from _griffe.agents.nodes.parameters import get_parameters
18from _griffe.enumerations import LogLevel, ParameterKind
19from _griffe.exceptions import NameResolutionError
20from _griffe.logger import logger
22if TYPE_CHECKING:
23 from collections.abc import Iterable, Iterator, Sequence
24 from pathlib import Path
26 from _griffe.models import Class, Module
29def _yield(element: str | Expr | tuple[str | Expr, ...], *, flat: bool = True) -> Iterator[str | Expr]:
30 if isinstance(element, str):
31 yield element
32 elif isinstance(element, tuple):
33 for elem in element:
34 yield from _yield(elem, flat=flat)
35 elif flat: 35 ↛ 38line 35 didn't jump to line 38 because the condition on line 35 was always true
36 yield from element.iterate(flat=True)
37 else:
38 yield element
41def _join(
42 elements: Iterable[str | Expr | tuple[str | Expr, ...]],
43 joint: str | Expr,
44 *,
45 flat: bool = True,
46) -> Iterator[str | Expr]:
47 it = iter(elements)
48 try:
49 yield from _yield(next(it), flat=flat)
50 except StopIteration:
51 return
52 for element in it:
53 yield from _yield(joint, flat=flat)
54 yield from _yield(element, flat=flat)
57def _field_as_dict(
58 element: str | bool | Expr | list[str | Expr] | None,
59 **kwargs: Any,
60) -> str | bool | None | list | dict:
61 if isinstance(element, Expr): 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true
62 return _expr_as_dict(element, **kwargs)
63 if isinstance(element, list):
64 return [_field_as_dict(elem, **kwargs) for elem in element]
65 return element
68def _expr_as_dict(expression: Expr, **kwargs: Any) -> dict[str, Any]:
69 fields = {
70 field.name: _field_as_dict(getattr(expression, field.name), **kwargs)
71 for field in sorted(getfields(expression), key=lambda f: f.name)
72 if field.name != "parent"
73 }
74 fields["cls"] = expression.classname
75 return fields
78# YORE: EOL 3.9: Remove block.
79_dataclass_opts: dict[str, bool] = {}
80if sys.version_info >= (3, 10):
81 _dataclass_opts["slots"] = True
84@dataclass
85class Expr:
86 """Base class for expressions."""
88 def __str__(self) -> str:
89 return "".join(elem if isinstance(elem, str) else elem.name for elem in self.iterate(flat=True)) # type: ignore[attr-defined]
91 def __iter__(self) -> Iterator[str | Expr]:
92 """Iterate on the expression syntax and elements."""
93 yield from self.iterate(flat=False)
95 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002
96 """Iterate on the expression elements.
98 Parameters:
99 flat: Expressions are trees.
101 When flat is false, this method iterates only on the first layer of the tree.
102 To iterate on all the subparts of the expression, you have to do so recursively.
103 It allows to handle each subpart specifically (for example subscripts, attribute, etc.),
104 without them getting rendered as strings.
106 On the contrary, when flat is true, the whole tree is flattened as a sequence
107 of strings and instances of [Names][griffe.ExprName].
109 Yields:
110 Strings and names when flat, strings and expressions otherwise.
111 """
112 yield from ()
114 def modernize(self) -> Expr:
115 """Modernize the expression.
117 For example, use PEP 604 type unions `|` instead of `typing.Union`.
119 Returns:
120 A modernized expression.
121 """
122 return self
124 def as_dict(self, **kwargs: Any) -> dict[str, Any]:
125 """Return the expression as a dictionary.
127 Parameters:
128 **kwargs: Configuration options (none available yet).
131 Returns:
132 A dictionary.
133 """
134 return _expr_as_dict(self, **kwargs)
136 @property
137 def classname(self) -> str:
138 """The expression class name."""
139 return self.__class__.__name__
141 @property
142 def path(self) -> str:
143 """Path of the expressed name/attribute."""
144 return str(self)
146 @property
147 def canonical_path(self) -> str:
148 """Path of the expressed name/attribute."""
149 return str(self)
151 @property
152 def canonical_name(self) -> str:
153 """Name of the expressed name/attribute."""
154 return self.canonical_path.rsplit(".", 1)[-1]
156 @property
157 def is_classvar(self) -> bool:
158 """Whether this attribute is annotated with `ClassVar`."""
159 return isinstance(self, ExprSubscript) and self.canonical_name == "ClassVar"
161 @property
162 def is_tuple(self) -> bool:
163 """Whether this expression is a tuple."""
164 return isinstance(self, ExprSubscript) and self.canonical_name.lower() == "tuple"
166 @property
167 def is_iterator(self) -> bool:
168 """Whether this expression is an iterator."""
169 return isinstance(self, ExprSubscript) and self.canonical_name == "Iterator"
171 @property
172 def is_generator(self) -> bool:
173 """Whether this expression is a generator."""
174 return isinstance(self, ExprSubscript) and self.canonical_name == "Generator"
177# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
178@dataclass(eq=True, **_dataclass_opts)
179class ExprAttribute(Expr):
180 """Attributes like `a.b`."""
182 values: list[str | Expr]
183 """The different parts of the dotted chain."""
185 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
186 yield from _join(self.values, ".", flat=flat)
188 def append(self, value: ExprName) -> None:
189 """Append a name to this attribute.
191 Parameters:
192 value: The expression name to append.
193 """
194 if value.parent is None: 194 ↛ 196line 194 didn't jump to line 196 because the condition on line 194 was always true
195 value.parent = self.last
196 self.values.append(value)
198 @property
199 def last(self) -> ExprName:
200 """The last part of this attribute (on the right)."""
201 # All values except the first one can *only* be names:
202 # we can't do `a.(b or c)` or `a."string"`.
203 return self.values[-1] # type: ignore[return-value]
205 @property
206 def first(self) -> str | Expr:
207 """The first part of this attribute (on the left)."""
208 return self.values[0]
210 @property
211 def path(self) -> str:
212 """The path of this attribute."""
213 return self.last.path
215 @property
216 def canonical_path(self) -> str:
217 """The canonical path of this attribute."""
218 return self.last.canonical_path
221# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
222@dataclass(eq=True, **_dataclass_opts)
223class ExprBinOp(Expr):
224 """Binary operations like `a + b`."""
226 left: str | Expr
227 """Left part."""
228 operator: str
229 """Binary operator."""
230 right: str | Expr
231 """Right part."""
233 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
234 yield from _yield(self.left, flat=flat)
235 yield f" {self.operator} "
236 yield from _yield(self.right, flat=flat)
239# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
240@dataclass(eq=True, **_dataclass_opts)
241class ExprBoolOp(Expr):
242 """Boolean operations like `a or b`."""
244 operator: str
245 """Boolean operator."""
246 values: Sequence[str | Expr]
247 """Operands."""
249 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
250 yield from _join(self.values, f" {self.operator} ", flat=flat)
253# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
254@dataclass(eq=True, **_dataclass_opts)
255class ExprCall(Expr):
256 """Calls like `f()`."""
258 function: Expr
259 """Function called."""
260 arguments: Sequence[str | Expr]
261 """Passed arguments."""
263 @property
264 def canonical_path(self) -> str:
265 """The canonical path of this subscript's left part."""
266 return self.function.canonical_path
268 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
269 yield from _yield(self.function, flat=flat)
270 yield "("
271 yield from _join(self.arguments, ", ", flat=flat)
272 yield ")"
275# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
276@dataclass(eq=True, **_dataclass_opts)
277class ExprCompare(Expr):
278 """Comparisons like `a > b`."""
280 left: str | Expr
281 """Left part."""
282 operators: Sequence[str]
283 """Comparison operators."""
284 comparators: Sequence[str | Expr]
285 """Things compared."""
287 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
288 yield from _yield(self.left, flat=flat)
289 yield " "
290 yield from _join(zip_longest(self.operators, [], self.comparators, fillvalue=" "), " ", flat=flat)
293# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
294@dataclass(eq=True, **_dataclass_opts)
295class ExprComprehension(Expr):
296 """Comprehensions like `a for b in c if d`."""
298 target: str | Expr
299 """Comprehension target (value added to the result)."""
300 iterable: str | Expr
301 """Value iterated on."""
302 conditions: Sequence[str | Expr]
303 """Conditions to include the target in the result."""
304 is_async: bool = False
305 """Async comprehension or not."""
307 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
308 if self.is_async:
309 yield "async "
310 yield "for "
311 yield from _yield(self.target, flat=flat)
312 yield " in "
313 yield from _yield(self.iterable, flat=flat)
314 if self.conditions:
315 yield " if "
316 yield from _join(self.conditions, " if ", flat=flat)
319# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
320@dataclass(eq=True, **_dataclass_opts)
321class ExprConstant(Expr):
322 """Constants like `"a"` or `1`."""
324 value: str
325 """Constant value."""
327 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002
328 yield self.value
331# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
332@dataclass(eq=True, **_dataclass_opts)
333class ExprDict(Expr):
334 """Dictionaries like `{"a": 0}`."""
336 keys: Sequence[str | Expr | None]
337 """Dict keys."""
338 values: Sequence[str | Expr]
339 """Dict values."""
341 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
342 yield "{"
343 yield from _join(
344 (("None" if key is None else key, ": ", value) for key, value in zip(self.keys, self.values)),
345 ", ",
346 flat=flat,
347 )
348 yield "}"
351# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
352@dataclass(eq=True, **_dataclass_opts)
353class ExprDictComp(Expr):
354 """Dict comprehensions like `{k: v for k, v in a}`."""
356 key: str | Expr
357 """Target key."""
358 value: str | Expr
359 """Target value."""
360 generators: Sequence[Expr]
361 """Generators iterated on."""
363 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
364 yield "{"
365 yield from _yield(self.key, flat=flat)
366 yield ": "
367 yield from _yield(self.value, flat=flat)
368 yield from _join(self.generators, " ", flat=flat)
369 yield "}"
372# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
373@dataclass(eq=True, **_dataclass_opts)
374class ExprExtSlice(Expr):
375 """Extended slice like `a[x:y, z]`."""
377 dims: Sequence[str | Expr]
378 """Dims."""
380 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
381 yield from _join(self.dims, ", ", flat=flat)
384# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
385@dataclass(eq=True, **_dataclass_opts)
386class ExprFormatted(Expr):
387 """Formatted string like `{1 + 1}`."""
389 value: str | Expr
390 """Formatted value."""
392 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
393 yield "{"
394 yield from _yield(self.value, flat=flat)
395 yield "}"
398# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
399@dataclass(eq=True, **_dataclass_opts)
400class ExprGeneratorExp(Expr):
401 """Generator expressions like `a for b in c for d in e`."""
403 element: str | Expr
404 """Yielded element."""
405 generators: Sequence[Expr]
406 """Generators iterated on."""
408 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
409 yield from _yield(self.element, flat=flat)
410 yield " "
411 yield from _join(self.generators, " ", flat=flat)
414# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
415@dataclass(eq=True, **_dataclass_opts)
416class ExprIfExp(Expr):
417 """Conditions like `a if b else c`."""
419 body: str | Expr
420 """Value if test."""
421 test: str | Expr
422 """Condition."""
423 orelse: str | Expr
424 """Other expression."""
426 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
427 yield from _yield(self.body, flat=flat)
428 yield " if "
429 yield from _yield(self.test, flat=flat)
430 yield " else "
431 yield from _yield(self.orelse, flat=flat)
434# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
435@dataclass(eq=True, **_dataclass_opts)
436class ExprJoinedStr(Expr):
437 """Joined strings like `f"a {b} c"`."""
439 values: Sequence[str | Expr]
440 """Joined values."""
442 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
443 yield "f'"
444 yield from _join(self.values, "", flat=flat)
445 yield "'"
448# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
449@dataclass(eq=True, **_dataclass_opts)
450class ExprKeyword(Expr):
451 """Keyword arguments like `a=b`."""
453 name: str
454 """Name."""
455 value: str | Expr
456 """Value."""
458 # Griffe is designed around accessing Python objects
459 # with the dot notation, for example `module.Class`.
460 # Function parameters were not taken into account
461 # because they are not accessible the same way.
462 # But we still want to be able to cross-reference
463 # documentation of function parameters in downstream
464 # tools like mkdocstrings. So we add a special case
465 # for keyword expressions, where they get a meaningful
466 # canonical path (contrary to most other expressions that
467 # aren't or do not begin with names or attributes)
468 # of the form `path.to.called_function(param_name)`.
469 # For this we need to store a reference to the `func` part
470 # of the call expression in the keyword one,
471 # hence the following field.
472 # We allow it to be None for backward compatibility.
473 function: Expr | None = None
474 """Expression referencing the function called with this parameter."""
476 @property
477 def canonical_path(self) -> str:
478 """Path of the expressed keyword."""
479 if self.function:
480 return f"{self.function.canonical_path}({self.name})"
481 return super(ExprKeyword, self).canonical_path # noqa: UP008
483 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
484 yield self.name
485 yield "="
486 yield from _yield(self.value, flat=flat)
489# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
490@dataclass(eq=True, **_dataclass_opts)
491class ExprVarPositional(Expr):
492 """Variadic positional parameters like `*args`."""
494 value: Expr
495 """Starred value."""
497 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
498 yield "*"
499 yield from _yield(self.value, flat=flat)
502# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
503@dataclass(eq=True, **_dataclass_opts)
504class ExprVarKeyword(Expr):
505 """Variadic keyword parameters like `**kwargs`."""
507 value: Expr
508 """Double-starred value."""
510 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
511 yield "**"
512 yield from _yield(self.value, flat=flat)
515# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
516@dataclass(eq=True, **_dataclass_opts)
517class ExprLambda(Expr):
518 """Lambda expressions like `lambda a: a.b`."""
520 parameters: Sequence[ExprParameter]
521 """Lambda's parameters."""
522 body: str | Expr
523 """Lambda's body."""
525 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
526 pos_only = False
527 pos_or_kw = False
528 kw_only = False
529 length = len(self.parameters)
530 yield "lambda"
531 if length: 531 ↛ 533line 531 didn't jump to line 533 because the condition on line 531 was always true
532 yield " "
533 for index, parameter in enumerate(self.parameters, 1):
534 if parameter.kind is ParameterKind.positional_only:
535 pos_only = True
536 elif parameter.kind is ParameterKind.var_positional:
537 yield "*"
538 elif parameter.kind is ParameterKind.var_keyword:
539 yield "**"
540 elif parameter.kind is ParameterKind.positional_or_keyword and not pos_or_kw:
541 pos_or_kw = True
542 elif parameter.kind is ParameterKind.keyword_only and not kw_only:
543 kw_only = True
544 yield "*, "
545 if parameter.kind is not ParameterKind.positional_only and pos_only:
546 pos_only = False
547 yield "/, "
548 yield parameter.name
549 if parameter.default and parameter.kind not in (ParameterKind.var_positional, ParameterKind.var_keyword):
550 yield "="
551 yield from _yield(parameter.default, flat=flat)
552 if index < length:
553 yield ", "
554 yield ": "
555 yield from _yield(self.body, flat=flat)
558# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
559@dataclass(eq=True, **_dataclass_opts)
560class ExprList(Expr):
561 """Lists like `[0, 1, 2]`."""
563 elements: Sequence[Expr]
564 """List elements."""
566 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
567 yield "["
568 yield from _join(self.elements, ", ", flat=flat)
569 yield "]"
572# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
573@dataclass(eq=True, **_dataclass_opts)
574class ExprListComp(Expr):
575 """List comprehensions like `[a for b in c]`."""
577 element: str | Expr
578 """Target value."""
579 generators: Sequence[Expr]
580 """Generators iterated on."""
582 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
583 yield "["
584 yield from _yield(self.element, flat=flat)
585 yield " "
586 yield from _join(self.generators, " ", flat=flat)
587 yield "]"
590# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
591@dataclass(eq=False, **_dataclass_opts)
592class ExprName(Expr):
593 """This class represents a Python object identified by a name in a given scope."""
595 name: str
596 """Actual name."""
597 parent: str | ExprName | Module | Class | None = None
598 """Parent (for resolution in its scope)."""
600 def __eq__(self, other: object) -> bool:
601 """Two name expressions are equal if they have the same `name` value (`parent` is ignored)."""
602 if isinstance(other, ExprName):
603 return self.name == other.name
604 return NotImplemented
606 def iterate(self, *, flat: bool = True) -> Iterator[ExprName]: # noqa: ARG002
607 yield self
609 @property
610 def path(self) -> str:
611 """The full, resolved name.
613 If it was given when creating the name, return that.
614 If a callable was given, call it and return its result.
615 It the name cannot be resolved, return the source.
616 """
617 if isinstance(self.parent, ExprName):
618 return f"{self.parent.path}.{self.name}"
619 return self.name
621 @property
622 def canonical_path(self) -> str:
623 """The canonical name (resolved one, not alias name)."""
624 if self.parent is None: 624 ↛ 625line 624 didn't jump to line 625 because the condition on line 624 was never true
625 return self.name
626 if isinstance(self.parent, ExprName):
627 return f"{self.parent.canonical_path}.{self.name}"
628 if isinstance(self.parent, str): 628 ↛ 629line 628 didn't jump to line 629 because the condition on line 628 was never true
629 return f"{self.parent}.{self.name}"
630 try:
631 return self.parent.resolve(self.name)
632 except NameResolutionError:
633 return self.name
635 @property
636 def resolved(self) -> Module | Class | None:
637 """The resolved object this name refers to."""
638 try:
639 return self.parent.modules_collection[self.parent.resolve(self.name)] # type: ignore[union-attr]
640 except Exception: # noqa: BLE001
641 return self.parent.resolved[self.name] # type: ignore[union-attr,index]
643 @property
644 def is_enum_class(self) -> bool:
645 """Whether this name resolves to an enumeration class."""
646 try:
647 bases = self.resolved.bases # type: ignore[union-attr]
648 except Exception: # noqa: BLE001
649 return False
651 # TODO: Support inheritance?
652 # TODO: Support `StrEnum` and `IntEnum`.
653 return any(isinstance(base, Expr) and base.canonical_path == "enum.Enum" for base in bases)
655 @property
656 def is_enum_instance(self) -> bool:
657 """Whether this name resolves to an enumeration instance."""
658 try:
659 return self.parent.is_enum_class # type: ignore[union-attr]
660 except Exception: # noqa: BLE001
661 return False
663 @property
664 def is_enum_value(self) -> bool:
665 """Whether this name resolves to an enumeration value."""
666 try:
667 return self.name == "value" and self.parent.is_enum_instance # type: ignore[union-attr]
668 except Exception: # noqa: BLE001
669 return False
672# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
673@dataclass(eq=True, **_dataclass_opts)
674class ExprNamedExpr(Expr):
675 """Named/assignment expressions like `a := b`."""
677 target: Expr
678 """Target name."""
679 value: str | Expr
680 """Value."""
682 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
683 yield "("
684 yield from _yield(self.target, flat=flat)
685 yield " := "
686 yield from _yield(self.value, flat=flat)
687 yield ")"
690# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
691@dataclass(eq=True, **_dataclass_opts)
692class ExprParameter(Expr):
693 """Parameters in function signatures like `a: int = 0`."""
695 name: str
696 """Parameter name."""
697 kind: ParameterKind = ParameterKind.positional_or_keyword
698 """Parameter kind."""
699 annotation: Expr | None = None
700 """Parameter type."""
701 default: str | Expr | None = None
702 """Parameter default."""
705# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
706@dataclass(eq=True, **_dataclass_opts)
707class ExprSet(Expr):
708 """Sets like `{0, 1, 2}`."""
710 elements: Sequence[str | Expr]
711 """Set elements."""
713 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
714 yield "{"
715 yield from _join(self.elements, ", ", flat=flat)
716 yield "}"
719# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
720@dataclass(eq=True, **_dataclass_opts)
721class ExprSetComp(Expr):
722 """Set comprehensions like `{a for b in c}`."""
724 element: str | Expr
725 """Target value."""
726 generators: Sequence[Expr]
727 """Generators iterated on."""
729 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
730 yield "{"
731 yield from _yield(self.element, flat=flat)
732 yield " "
733 yield from _join(self.generators, " ", flat=flat)
734 yield "}"
737# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
738@dataclass(eq=True, **_dataclass_opts)
739class ExprSlice(Expr):
740 """Slices like `[a:b:c]`."""
742 lower: str | Expr | None = None
743 """Lower bound."""
744 upper: str | Expr | None = None
745 """Upper bound."""
746 step: str | Expr | None = None
747 """Iteration step."""
749 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
750 if self.lower is not None:
751 yield from _yield(self.lower, flat=flat)
752 yield ":"
753 if self.upper is not None: 753 ↛ 755line 753 didn't jump to line 755 because the condition on line 753 was always true
754 yield from _yield(self.upper, flat=flat)
755 if self.step is not None: 755 ↛ 756line 755 didn't jump to line 756 because the condition on line 755 was never true
756 yield ":"
757 yield from _yield(self.step, flat=flat)
760# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
761@dataclass(eq=True, **_dataclass_opts)
762class ExprSubscript(Expr):
763 """Subscripts like `a[b]`."""
765 left: str | Expr
766 """Left part."""
767 slice: Expr
768 """Slice part."""
770 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
771 yield from _yield(self.left, flat=flat)
772 yield "["
773 yield from _yield(self.slice, flat=flat)
774 yield "]"
776 @property
777 def path(self) -> str:
778 """The path of this subscript's left part."""
779 if isinstance(self.left, str):
780 return self.left
781 return self.left.path
783 @property
784 def canonical_path(self) -> str:
785 """The canonical path of this subscript's left part."""
786 if isinstance(self.left, str): 786 ↛ 787line 786 didn't jump to line 787 because the condition on line 786 was never true
787 return self.left
788 return self.left.canonical_path
791# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
792@dataclass(eq=True, **_dataclass_opts)
793class ExprTuple(Expr):
794 """Tuples like `(0, 1, 2)`."""
796 elements: Sequence[str | Expr]
797 """Tuple elements."""
798 implicit: bool = False
799 """Whether the tuple is implicit (e.g. without parentheses in a subscript's slice)."""
801 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
802 if not self.implicit:
803 yield "("
804 yield from _join(self.elements, ", ", flat=flat)
805 if not self.implicit:
806 yield ")"
809# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
810@dataclass(eq=True, **_dataclass_opts)
811class ExprUnaryOp(Expr):
812 """Unary operations like `-1`."""
814 operator: str
815 """Unary operator."""
816 value: str | Expr
817 """Value."""
819 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
820 yield self.operator
821 yield from _yield(self.value, flat=flat)
824# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
825@dataclass(eq=True, **_dataclass_opts)
826class ExprYield(Expr):
827 """Yield statements like `yield a`."""
829 value: str | Expr | None = None
830 """Yielded value."""
832 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
833 yield "yield"
834 if self.value is not None:
835 yield " "
836 yield from _yield(self.value, flat=flat)
839# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
840@dataclass(eq=True, **_dataclass_opts)
841class ExprYieldFrom(Expr):
842 """Yield statements like `yield from a`."""
844 value: str | Expr
845 """Yielded-from value."""
847 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
848 yield "yield from "
849 yield from _yield(self.value, flat=flat)
852_unary_op_map = {
853 ast.Invert: "~",
854 ast.Not: "not ",
855 ast.UAdd: "+",
856 ast.USub: "-",
857}
859_binary_op_map = {
860 ast.Add: "+",
861 ast.BitAnd: "&",
862 ast.BitOr: "|",
863 ast.BitXor: "^",
864 ast.Div: "/",
865 ast.FloorDiv: "//",
866 ast.LShift: "<<",
867 ast.MatMult: "@",
868 ast.Mod: "%",
869 ast.Mult: "*",
870 ast.Pow: "**",
871 ast.RShift: ">>",
872 ast.Sub: "-",
873}
875_bool_op_map = {
876 ast.And: "and",
877 ast.Or: "or",
878}
880_compare_op_map = {
881 ast.Eq: "==",
882 ast.NotEq: "!=",
883 ast.Lt: "<",
884 ast.LtE: "<=",
885 ast.Gt: ">",
886 ast.GtE: ">=",
887 ast.Is: "is",
888 ast.IsNot: "is not",
889 ast.In: "in",
890 ast.NotIn: "not in",
891}
894def _build_attribute(node: ast.Attribute, parent: Module | Class, **kwargs: Any) -> Expr:
895 left = _build(node.value, parent, **kwargs)
896 if isinstance(left, ExprAttribute):
897 left.append(ExprName(node.attr))
898 return left
899 if isinstance(left, ExprName):
900 return ExprAttribute([left, ExprName(node.attr, left)])
901 if isinstance(left, str):
902 return ExprAttribute([left, ExprName(node.attr, "str")])
903 return ExprAttribute([left, ExprName(node.attr)])
906def _build_binop(node: ast.BinOp, parent: Module | Class, **kwargs: Any) -> Expr:
907 return ExprBinOp(
908 _build(node.left, parent, **kwargs),
909 _binary_op_map[type(node.op)],
910 _build(node.right, parent, **kwargs),
911 )
914def _build_boolop(node: ast.BoolOp, parent: Module | Class, **kwargs: Any) -> Expr:
915 return ExprBoolOp(
916 _bool_op_map[type(node.op)],
917 [_build(value, parent, **kwargs) for value in node.values],
918 )
921def _build_call(node: ast.Call, parent: Module | Class, **kwargs: Any) -> Expr:
922 function = _build(node.func, parent, **kwargs)
923 positional_args = [_build(arg, parent, **kwargs) for arg in node.args]
924 keyword_args = [_build(kwarg, parent, function=function, **kwargs) for kwarg in node.keywords]
925 return ExprCall(function, [*positional_args, *keyword_args])
928def _build_compare(node: ast.Compare, parent: Module | Class, **kwargs: Any) -> Expr:
929 return ExprCompare(
930 _build(node.left, parent, **kwargs),
931 [_compare_op_map[type(op)] for op in node.ops],
932 [_build(comp, parent, **kwargs) for comp in node.comparators],
933 )
936def _build_comprehension(node: ast.comprehension, parent: Module | Class, **kwargs: Any) -> Expr:
937 return ExprComprehension(
938 _build(node.target, parent, **kwargs),
939 _build(node.iter, parent, **kwargs),
940 [_build(condition, parent, **kwargs) for condition in node.ifs],
941 is_async=bool(node.is_async),
942 )
945def _build_constant(
946 node: ast.Constant,
947 parent: Module | Class,
948 *,
949 in_formatted_str: bool = False,
950 in_joined_str: bool = False,
951 parse_strings: bool = False,
952 literal_strings: bool = False,
953 **kwargs: Any,
954) -> str | Expr:
955 if isinstance(node.value, str):
956 if in_joined_str and not in_formatted_str:
957 # We're in a f-string, not in a formatted value, don't keep quotes.
958 return node.value
959 if parse_strings and not literal_strings:
960 # We're in a place where a string could be a type annotation
961 # (and not in a Literal[...] type annotation).
962 # We parse the string and build from the resulting nodes again.
963 # If we fail to parse it (syntax errors), we consider it's a literal string and log a message.
964 try:
965 parsed = compile(
966 node.value,
967 mode="eval",
968 filename="<string-annotation>",
969 flags=ast.PyCF_ONLY_AST,
970 optimize=1,
971 )
972 except SyntaxError:
973 logger.debug(
974 "Tried and failed to parse %r as Python code, "
975 "falling back to using it as a string literal "
976 "(postponed annotations might help: https://peps.python.org/pep-0563/)",
977 node.value,
978 )
979 else:
980 return _build(parsed.body, parent, **kwargs) # type: ignore[attr-defined]
981 return {type(...): lambda _: "..."}.get(type(node.value), repr)(node.value)
984def _build_dict(node: ast.Dict, parent: Module | Class, **kwargs: Any) -> Expr:
985 return ExprDict(
986 [None if key is None else _build(key, parent, **kwargs) for key in node.keys],
987 [_build(value, parent, **kwargs) for value in node.values],
988 )
991def _build_dictcomp(node: ast.DictComp, parent: Module | Class, **kwargs: Any) -> Expr:
992 return ExprDictComp(
993 _build(node.key, parent, **kwargs),
994 _build(node.value, parent, **kwargs),
995 [_build(gen, parent, **kwargs) for gen in node.generators],
996 )
999def _build_formatted(
1000 node: ast.FormattedValue,
1001 parent: Module | Class,
1002 *,
1003 in_formatted_str: bool = False, # noqa: ARG001
1004 **kwargs: Any,
1005) -> Expr:
1006 return ExprFormatted(_build(node.value, parent, in_formatted_str=True, **kwargs))
1009def _build_generatorexp(node: ast.GeneratorExp, parent: Module | Class, **kwargs: Any) -> Expr:
1010 return ExprGeneratorExp(
1011 _build(node.elt, parent, **kwargs),
1012 [_build(gen, parent, **kwargs) for gen in node.generators],
1013 )
1016def _build_ifexp(node: ast.IfExp, parent: Module | Class, **kwargs: Any) -> Expr:
1017 return ExprIfExp(
1018 _build(node.body, parent, **kwargs),
1019 _build(node.test, parent, **kwargs),
1020 _build(node.orelse, parent, **kwargs),
1021 )
1024def _build_joinedstr(
1025 node: ast.JoinedStr,
1026 parent: Module | Class,
1027 *,
1028 in_joined_str: bool = False, # noqa: ARG001
1029 **kwargs: Any,
1030) -> Expr:
1031 return ExprJoinedStr([_build(value, parent, in_joined_str=True, **kwargs) for value in node.values])
1034def _build_keyword(node: ast.keyword, parent: Module | Class, function: Expr | None = None, **kwargs: Any) -> Expr:
1035 if node.arg is None:
1036 return ExprVarKeyword(_build(node.value, parent, **kwargs))
1037 return ExprKeyword(node.arg, _build(node.value, parent, **kwargs), function=function)
1040def _build_lambda(node: ast.Lambda, parent: Module | Class, **kwargs: Any) -> Expr:
1041 return ExprLambda(
1042 parameters=[
1043 ExprParameter(
1044 name=name,
1045 kind=kind,
1046 annotation=None,
1047 default=default
1048 if isinstance(default, str)
1049 else safe_get_expression(default, parent=parent, parse_strings=False),
1050 )
1051 for name, _, kind, default in get_parameters(node.args)
1052 ],
1053 body=_build(node.body, parent, **kwargs),
1054 )
1057def _build_list(node: ast.List, parent: Module | Class, **kwargs: Any) -> Expr:
1058 return ExprList([_build(el, parent, **kwargs) for el in node.elts])
1061def _build_listcomp(node: ast.ListComp, parent: Module | Class, **kwargs: Any) -> Expr:
1062 return ExprListComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators])
1065def _build_name(node: ast.Name, parent: Module | Class, **kwargs: Any) -> Expr: # noqa: ARG001
1066 return ExprName(node.id, parent)
1069def _build_named_expr(node: ast.NamedExpr, parent: Module | Class, **kwargs: Any) -> Expr:
1070 return ExprNamedExpr(_build(node.target, parent, **kwargs), _build(node.value, parent, **kwargs))
1073def _build_set(node: ast.Set, parent: Module | Class, **kwargs: Any) -> Expr:
1074 return ExprSet([_build(el, parent, **kwargs) for el in node.elts])
1077def _build_setcomp(node: ast.SetComp, parent: Module | Class, **kwargs: Any) -> Expr:
1078 return ExprSetComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators])
1081def _build_slice(node: ast.Slice, parent: Module | Class, **kwargs: Any) -> Expr:
1082 return ExprSlice(
1083 None if node.lower is None else _build(node.lower, parent, **kwargs),
1084 None if node.upper is None else _build(node.upper, parent, **kwargs),
1085 None if node.step is None else _build(node.step, parent, **kwargs),
1086 )
1089def _build_starred(node: ast.Starred, parent: Module | Class, **kwargs: Any) -> Expr:
1090 return ExprVarPositional(_build(node.value, parent, **kwargs))
1093def _build_subscript(
1094 node: ast.Subscript,
1095 parent: Module | Class,
1096 *,
1097 parse_strings: bool = False,
1098 literal_strings: bool = False,
1099 in_subscript: bool = False, # noqa: ARG001
1100 **kwargs: Any,
1101) -> Expr:
1102 left = _build(node.value, parent, **kwargs)
1103 if parse_strings:
1104 if isinstance(left, (ExprAttribute, ExprName)) and left.canonical_path in {
1105 "typing.Literal",
1106 "typing_extensions.Literal",
1107 }:
1108 literal_strings = True
1109 slice = _build(
1110 node.slice,
1111 parent,
1112 parse_strings=True,
1113 literal_strings=literal_strings,
1114 in_subscript=True,
1115 **kwargs,
1116 )
1117 else:
1118 slice = _build(node.slice, parent, in_subscript=True, **kwargs)
1119 return ExprSubscript(left, slice)
1122def _build_tuple(
1123 node: ast.Tuple,
1124 parent: Module | Class,
1125 *,
1126 in_subscript: bool = False,
1127 **kwargs: Any,
1128) -> Expr:
1129 return ExprTuple([_build(el, parent, **kwargs) for el in node.elts], implicit=in_subscript)
1132def _build_unaryop(node: ast.UnaryOp, parent: Module | Class, **kwargs: Any) -> Expr:
1133 return ExprUnaryOp(_unary_op_map[type(node.op)], _build(node.operand, parent, **kwargs))
1136def _build_yield(node: ast.Yield, parent: Module | Class, **kwargs: Any) -> Expr:
1137 return ExprYield(None if node.value is None else _build(node.value, parent, **kwargs))
1140def _build_yield_from(node: ast.YieldFrom, parent: Module | Class, **kwargs: Any) -> Expr:
1141 return ExprYieldFrom(_build(node.value, parent, **kwargs))
1144_node_map: dict[type, Callable[[Any, Module | Class], Expr]] = {
1145 ast.Attribute: _build_attribute,
1146 ast.BinOp: _build_binop,
1147 ast.BoolOp: _build_boolop,
1148 ast.Call: _build_call,
1149 ast.Compare: _build_compare,
1150 ast.comprehension: _build_comprehension,
1151 ast.Constant: _build_constant, # type: ignore[dict-item]
1152 ast.Dict: _build_dict,
1153 ast.DictComp: _build_dictcomp,
1154 ast.FormattedValue: _build_formatted,
1155 ast.GeneratorExp: _build_generatorexp,
1156 ast.IfExp: _build_ifexp,
1157 ast.JoinedStr: _build_joinedstr,
1158 ast.keyword: _build_keyword,
1159 ast.Lambda: _build_lambda,
1160 ast.List: _build_list,
1161 ast.ListComp: _build_listcomp,
1162 ast.Name: _build_name,
1163 ast.NamedExpr: _build_named_expr,
1164 ast.Set: _build_set,
1165 ast.SetComp: _build_setcomp,
1166 ast.Slice: _build_slice,
1167 ast.Starred: _build_starred,
1168 ast.Subscript: _build_subscript,
1169 ast.Tuple: _build_tuple,
1170 ast.UnaryOp: _build_unaryop,
1171 ast.Yield: _build_yield,
1172 ast.YieldFrom: _build_yield_from,
1173}
1176def _build(node: ast.AST, parent: Module | Class, **kwargs: Any) -> Expr:
1177 return _node_map[type(node)](node, parent, **kwargs)
1180def get_expression(
1181 node: ast.AST | None,
1182 parent: Module | Class,
1183 *,
1184 parse_strings: bool | None = None,
1185) -> Expr | None:
1186 """Build an expression from an AST.
1188 Parameters:
1189 node: The annotation node.
1190 parent: The parent used to resolve the name.
1191 parse_strings: Whether to try and parse strings as type annotations.
1193 Returns:
1194 A string or resovable name or expression.
1195 """
1196 if node is None:
1197 return None
1198 if parse_strings is None:
1199 try:
1200 module = parent.module
1201 except ValueError:
1202 parse_strings = False
1203 else:
1204 parse_strings = not module.imports_future_annotations
1205 return _build(node, parent, parse_strings=parse_strings)
1208def safe_get_expression(
1209 node: ast.AST | None,
1210 parent: Module | Class,
1211 *,
1212 parse_strings: bool | None = None,
1213 log_level: LogLevel | None = LogLevel.error,
1214 msg_format: str = "{path}:{lineno}: Failed to get expression from {node_class}: {error}",
1215) -> Expr | None:
1216 """Safely (no exception) build a resolvable annotation.
1218 Parameters:
1219 node: The annotation node.
1220 parent: The parent used to resolve the name.
1221 parse_strings: Whether to try and parse strings as type annotations.
1222 log_level: Log level to use to log a message. None to disable logging.
1223 msg_format: A format string for the log message. Available placeholders:
1224 path, lineno, node, error.
1226 Returns:
1227 A string or resovable name or expression.
1228 """
1229 try:
1230 return get_expression(node, parent, parse_strings=parse_strings)
1231 except Exception as error: # noqa: BLE001
1232 if log_level is None: 1232 ↛ 1233line 1232 didn't jump to line 1233 because the condition on line 1232 was never true
1233 return None
1234 node_class = node.__class__.__name__
1235 try:
1236 path: Path | str = parent.relative_filepath
1237 except ValueError:
1238 path = "<in-memory>"
1239 lineno = node.lineno # type: ignore[union-attr]
1240 error_str = f"{error.__class__.__name__}: {error}"
1241 message = msg_format.format(path=path, lineno=lineno, node_class=node_class, error=error_str)
1242 getattr(logger, log_level.value)(message)
1243 return None
1246_msg_format = "{path}:{lineno}: Failed to get %s expression from {node_class}: {error}"
1247get_annotation = partial(get_expression, parse_strings=None)
1248safe_get_annotation = partial(
1249 safe_get_expression,
1250 parse_strings=None,
1251 msg_format=_msg_format % "annotation",
1252)
1253get_base_class = partial(get_expression, parse_strings=False)
1254safe_get_base_class = partial(
1255 safe_get_expression,
1256 parse_strings=False,
1257 msg_format=_msg_format % "base class",
1258)
1259get_condition = partial(get_expression, parse_strings=False)
1260safe_get_condition = partial(
1261 safe_get_expression,
1262 parse_strings=False,
1263 msg_format=_msg_format % "condition",
1264)