Coverage for src/_griffe/expressions.py: 89.23%
634 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:47 +0200
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-15 16:47 +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 helpersto 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, Iterable, Iterator, Sequence
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 pathlib import Path
25 from _griffe.models import Class, Module
28def _yield(element: str | Expr | tuple[str | Expr, ...], *, flat: bool = True) -> Iterator[str | Expr]:
29 if isinstance(element, str):
30 yield element
31 elif isinstance(element, tuple):
32 for elem in element:
33 yield from _yield(elem, flat=flat)
34 elif flat: 34 ↛ 37line 34 didn't jump to line 37 because the condition on line 34 was always true
35 yield from element.iterate(flat=True)
36 else:
37 yield element
40def _join(
41 elements: Iterable[str | Expr | tuple[str | Expr, ...]],
42 joint: str | Expr,
43 *,
44 flat: bool = True,
45) -> Iterator[str | Expr]:
46 it = iter(elements)
47 try:
48 yield from _yield(next(it), flat=flat)
49 except StopIteration:
50 return
51 for element in it:
52 yield from _yield(joint, flat=flat)
53 yield from _yield(element, flat=flat)
56def _field_as_dict(
57 element: str | bool | Expr | list[str | Expr] | None,
58 **kwargs: Any,
59) -> str | bool | None | list | dict:
60 if isinstance(element, Expr): 60 ↛ 61line 60 didn't jump to line 61 because the condition on line 60 was never true
61 return _expr_as_dict(element, **kwargs)
62 if isinstance(element, list):
63 return [_field_as_dict(elem, **kwargs) for elem in element]
64 return element
67def _expr_as_dict(expression: Expr, **kwargs: Any) -> dict[str, Any]:
68 fields = {
69 field.name: _field_as_dict(getattr(expression, field.name), **kwargs)
70 for field in sorted(getfields(expression), key=lambda f: f.name)
71 if field.name != "parent"
72 }
73 fields["cls"] = expression.classname
74 return fields
77# YORE: EOL 3.9: Remove block.
78_dataclass_opts: dict[str, bool] = {}
79if sys.version_info >= (3, 10):
80 _dataclass_opts["slots"] = True
83@dataclass
84class Expr:
85 """Base class for expressions."""
87 def __str__(self) -> str:
88 return "".join(elem if isinstance(elem, str) else elem.name for elem in self.iterate(flat=True)) # type: ignore[attr-defined]
90 def __iter__(self) -> Iterator[str | Expr]:
91 """Iterate on the expression syntax and elements."""
92 yield from self.iterate(flat=False)
94 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002
95 """Iterate on the expression elements.
97 Parameters:
98 flat: Expressions are trees.
100 When flat is false, this method iterates only on the first layer of the tree.
101 To iterate on all the subparts of the expression, you have to do so recursively.
102 It allows to handle each subpart specifically (for example subscripts, attribute, etc.),
103 without them getting rendered as strings.
105 On the contrary, when flat is true, the whole tree is flattened as a sequence
106 of strings and instances of [Names][griffe.ExprName].
108 Yields:
109 Strings and names when flat, strings and expressions otherwise.
110 """
111 yield from ()
113 def modernize(self) -> Expr:
114 """Modernize the expression.
116 For example, use PEP 604 type unions `|` instead of `typing.Union`.
118 Returns:
119 A modernized expression.
120 """
121 return self
123 def as_dict(self, **kwargs: Any) -> dict[str, Any]:
124 """Return the expression as a dictionary.
126 Parameters:
127 **kwargs: Configuration options (none available yet).
130 Returns:
131 A dictionary.
132 """
133 return _expr_as_dict(self, **kwargs)
135 @property
136 def classname(self) -> str:
137 """The expression class name."""
138 return self.__class__.__name__
140 @property
141 def path(self) -> str:
142 """Path of the expressed name/attribute."""
143 return str(self)
145 @property
146 def canonical_path(self) -> str:
147 """Path of the expressed name/attribute."""
148 return str(self)
150 @property
151 def canonical_name(self) -> str:
152 """Name of the expressed name/attribute."""
153 return self.canonical_path.rsplit(".", 1)[-1]
155 @property
156 def is_classvar(self) -> bool:
157 """Whether this attribute is annotated with `ClassVar`."""
158 return isinstance(self, ExprSubscript) and self.canonical_name == "ClassVar"
160 @property
161 def is_tuple(self) -> bool:
162 """Whether this expression is a tuple."""
163 return isinstance(self, ExprSubscript) and self.canonical_name.lower() == "tuple"
165 @property
166 def is_iterator(self) -> bool:
167 """Whether this expression is an iterator."""
168 return isinstance(self, ExprSubscript) and self.canonical_name == "Iterator"
170 @property
171 def is_generator(self) -> bool:
172 """Whether this expression is a generator."""
173 return isinstance(self, ExprSubscript) and self.canonical_name == "Generator"
176# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
177@dataclass(eq=True, **_dataclass_opts)
178class ExprAttribute(Expr):
179 """Attributes like `a.b`."""
181 values: list[str | Expr]
182 """The different parts of the dotted chain."""
184 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
185 yield from _join(self.values, ".", flat=flat)
187 def append(self, value: ExprName) -> None:
188 """Append a name to this attribute.
190 Parameters:
191 value: The expression name to append.
192 """
193 if value.parent is None: 193 ↛ 195line 193 didn't jump to line 195 because the condition on line 193 was always true
194 value.parent = self.last
195 self.values.append(value)
197 @property
198 def last(self) -> ExprName:
199 """The last part of this attribute (on the right)."""
200 # All values except the first one can *only* be names:
201 # we can't do `a.(b or c)` or `a."string"`.
202 return self.values[-1] # type: ignore[return-value]
204 @property
205 def first(self) -> str | Expr:
206 """The first part of this attribute (on the left)."""
207 return self.values[0]
209 @property
210 def path(self) -> str:
211 """The path of this attribute."""
212 return self.last.path
214 @property
215 def canonical_path(self) -> str:
216 """The canonical path of this attribute."""
217 return self.last.canonical_path
220# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
221@dataclass(eq=True, **_dataclass_opts)
222class ExprBinOp(Expr):
223 """Binary operations like `a + b`."""
225 left: str | Expr
226 """Left part."""
227 operator: str
228 """Binary operator."""
229 right: str | Expr
230 """Right part."""
232 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
233 yield from _yield(self.left, flat=flat)
234 yield f" {self.operator} "
235 yield from _yield(self.right, flat=flat)
238# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
239@dataclass(eq=True, **_dataclass_opts)
240class ExprBoolOp(Expr):
241 """Boolean operations like `a or b`."""
243 operator: str
244 """Boolean operator."""
245 values: Sequence[str | Expr]
246 """Operands."""
248 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
249 yield from _join(self.values, f" {self.operator} ", flat=flat)
252# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
253@dataclass(eq=True, **_dataclass_opts)
254class ExprCall(Expr):
255 """Calls like `f()`."""
257 function: Expr
258 """Function called."""
259 arguments: Sequence[str | Expr]
260 """Passed arguments."""
262 @property
263 def canonical_path(self) -> str:
264 """The canonical path of this subscript's left part."""
265 return self.function.canonical_path
267 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
268 yield from _yield(self.function, flat=flat)
269 yield "("
270 yield from _join(self.arguments, ", ", flat=flat)
271 yield ")"
274# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
275@dataclass(eq=True, **_dataclass_opts)
276class ExprCompare(Expr):
277 """Comparisons like `a > b`."""
279 left: str | Expr
280 """Left part."""
281 operators: Sequence[str]
282 """Comparison operators."""
283 comparators: Sequence[str | Expr]
284 """Things compared."""
286 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
287 yield from _yield(self.left, flat=flat)
288 yield " "
289 yield from _join(zip_longest(self.operators, [], self.comparators, fillvalue=" "), " ", flat=flat)
292# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
293@dataclass(eq=True, **_dataclass_opts)
294class ExprComprehension(Expr):
295 """Comprehensions like `a for b in c if d`."""
297 target: str | Expr
298 """Comprehension target (value added to the result)."""
299 iterable: str | Expr
300 """Value iterated on."""
301 conditions: Sequence[str | Expr]
302 """Conditions to include the target in the result."""
303 is_async: bool = False
304 """Async comprehension or not."""
306 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
307 if self.is_async:
308 yield "async "
309 yield "for "
310 yield from _yield(self.target, flat=flat)
311 yield " in "
312 yield from _yield(self.iterable, flat=flat)
313 if self.conditions:
314 yield " if "
315 yield from _join(self.conditions, " if ", flat=flat)
318# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
319@dataclass(eq=True, **_dataclass_opts)
320class ExprConstant(Expr):
321 """Constants like `"a"` or `1`."""
323 value: str
324 """Constant value."""
326 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002
327 yield self.value
330# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
331@dataclass(eq=True, **_dataclass_opts)
332class ExprDict(Expr):
333 """Dictionaries like `{"a": 0}`."""
335 keys: Sequence[str | Expr | None]
336 """Dict keys."""
337 values: Sequence[str | Expr]
338 """Dict values."""
340 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
341 yield "{"
342 yield from _join(
343 (("None" if key is None else key, ": ", value) for key, value in zip(self.keys, self.values)),
344 ", ",
345 flat=flat,
346 )
347 yield "}"
350# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
351@dataclass(eq=True, **_dataclass_opts)
352class ExprDictComp(Expr):
353 """Dict comprehensions like `{k: v for k, v in a}`."""
355 key: str | Expr
356 """Target key."""
357 value: str | Expr
358 """Target value."""
359 generators: Sequence[Expr]
360 """Generators iterated on."""
362 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
363 yield "{"
364 yield from _yield(self.key, flat=flat)
365 yield ": "
366 yield from _yield(self.value, flat=flat)
367 yield from _join(self.generators, " ", flat=flat)
368 yield "}"
371# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
372@dataclass(eq=True, **_dataclass_opts)
373class ExprExtSlice(Expr):
374 """Extended slice like `a[x:y, z]`."""
376 dims: Sequence[str | Expr]
377 """Dims."""
379 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
380 yield from _join(self.dims, ", ", flat=flat)
383# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
384@dataclass(eq=True, **_dataclass_opts)
385class ExprFormatted(Expr):
386 """Formatted string like `{1 + 1}`."""
388 value: str | Expr
389 """Formatted value."""
391 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
392 yield "{"
393 yield from _yield(self.value, flat=flat)
394 yield "}"
397# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
398@dataclass(eq=True, **_dataclass_opts)
399class ExprGeneratorExp(Expr):
400 """Generator expressions like `a for b in c for d in e`."""
402 element: str | Expr
403 """Yielded element."""
404 generators: Sequence[Expr]
405 """Generators iterated on."""
407 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
408 yield from _yield(self.element, flat=flat)
409 yield " "
410 yield from _join(self.generators, " ", flat=flat)
413# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
414@dataclass(eq=True, **_dataclass_opts)
415class ExprIfExp(Expr):
416 """Conditions like `a if b else c`."""
418 body: str | Expr
419 """Value if test."""
420 test: str | Expr
421 """Condition."""
422 orelse: str | Expr
423 """Other expression."""
425 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
426 yield from _yield(self.body, flat=flat)
427 yield " if "
428 yield from _yield(self.test, flat=flat)
429 yield " else "
430 yield from _yield(self.orelse, flat=flat)
433# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
434@dataclass(eq=True, **_dataclass_opts)
435class ExprJoinedStr(Expr):
436 """Joined strings like `f"a {b} c"`."""
438 values: Sequence[str | Expr]
439 """Joined values."""
441 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
442 yield "f'"
443 yield from _join(self.values, "", flat=flat)
444 yield "'"
447# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
448@dataclass(eq=True, **_dataclass_opts)
449class ExprKeyword(Expr):
450 """Keyword arguments like `a=b`."""
452 name: str
453 """Name."""
454 value: str | Expr
455 """Value."""
457 # Griffe is designed around accessing Python objects
458 # with the dot notation, for example `module.Class`.
459 # Function parameters were not taken into account
460 # because they are not accessible the same way.
461 # But we still want to be able to cross-reference
462 # documentation of function parameters in downstream
463 # tools like mkdocstrings. So we add a special case
464 # for keyword expressions, where they get a meaningful
465 # canonical path (contrary to most other expressions that
466 # aren't or do not begin with names or attributes)
467 # of the form `path.to.called_function(param_name)`.
468 # For this we need to store a reference to the `func` part
469 # of the call expression in the keyword one,
470 # hence the following field.
471 # We allow it to be None for backward compatibility.
472 function: Expr | None = None
473 """Expression referencing the function called with this parameter."""
475 @property
476 def canonical_path(self) -> str:
477 """Path of the expressed keyword."""
478 if self.function:
479 return f"{self.function.canonical_path}({self.name})"
480 return super(ExprKeyword, self).canonical_path # noqa: UP008
482 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
483 yield self.name
484 yield "="
485 yield from _yield(self.value, flat=flat)
488# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
489@dataclass(eq=True, **_dataclass_opts)
490class ExprVarPositional(Expr):
491 """Variadic positional parameters like `*args`."""
493 value: Expr
494 """Starred value."""
496 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
497 yield "*"
498 yield from _yield(self.value, flat=flat)
501# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
502@dataclass(eq=True, **_dataclass_opts)
503class ExprVarKeyword(Expr):
504 """Variadic keyword parameters like `**kwargs`."""
506 value: Expr
507 """Double-starred value."""
509 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
510 yield "**"
511 yield from _yield(self.value, flat=flat)
514# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
515@dataclass(eq=True, **_dataclass_opts)
516class ExprLambda(Expr):
517 """Lambda expressions like `lambda a: a.b`."""
519 parameters: Sequence[ExprParameter]
520 """Lambda's parameters."""
521 body: str | Expr
522 """Lambda's body."""
524 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
525 pos_only = False
526 pos_or_kw = False
527 kw_only = False
528 length = len(self.parameters)
529 yield "lambda"
530 if length: 530 ↛ 532line 530 didn't jump to line 532 because the condition on line 530 was always true
531 yield " "
532 for index, parameter in enumerate(self.parameters, 1):
533 if parameter.kind is ParameterKind.positional_only:
534 pos_only = True
535 elif parameter.kind is ParameterKind.var_positional:
536 yield "*"
537 elif parameter.kind is ParameterKind.var_keyword:
538 yield "**"
539 elif parameter.kind is ParameterKind.positional_or_keyword and not pos_or_kw:
540 pos_or_kw = True
541 elif parameter.kind is ParameterKind.keyword_only and not kw_only:
542 kw_only = True
543 yield "*, "
544 if parameter.kind is not ParameterKind.positional_only and pos_only:
545 pos_only = False
546 yield "/, "
547 yield parameter.name
548 if parameter.default and parameter.kind not in (ParameterKind.var_positional, ParameterKind.var_keyword):
549 yield "="
550 yield from _yield(parameter.default, flat=flat)
551 if index < length:
552 yield ", "
553 yield ": "
554 yield from _yield(self.body, flat=flat)
557# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
558@dataclass(eq=True, **_dataclass_opts)
559class ExprList(Expr):
560 """Lists like `[0, 1, 2]`."""
562 elements: Sequence[Expr]
563 """List elements."""
565 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
566 yield "["
567 yield from _join(self.elements, ", ", flat=flat)
568 yield "]"
571# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
572@dataclass(eq=True, **_dataclass_opts)
573class ExprListComp(Expr):
574 """List comprehensions like `[a for b in c]`."""
576 element: str | Expr
577 """Target value."""
578 generators: Sequence[Expr]
579 """Generators iterated on."""
581 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
582 yield "["
583 yield from _yield(self.element, flat=flat)
584 yield " "
585 yield from _join(self.generators, " ", flat=flat)
586 yield "]"
589# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
590@dataclass(eq=False, **_dataclass_opts)
591class ExprName(Expr):
592 """This class represents a Python object identified by a name in a given scope."""
594 name: str
595 """Actual name."""
596 parent: str | ExprName | Module | Class | None = None
597 """Parent (for resolution in its scope)."""
599 def __eq__(self, other: object) -> bool:
600 """Two name expressions are equal if they have the same `name` value (`parent` is ignored)."""
601 if isinstance(other, ExprName):
602 return self.name == other.name
603 return NotImplemented
605 def iterate(self, *, flat: bool = True) -> Iterator[ExprName]: # noqa: ARG002
606 yield self
608 @property
609 def path(self) -> str:
610 """The full, resolved name.
612 If it was given when creating the name, return that.
613 If a callable was given, call it and return its result.
614 It the name cannot be resolved, return the source.
615 """
616 if isinstance(self.parent, ExprName):
617 return f"{self.parent.path}.{self.name}"
618 return self.name
620 @property
621 def canonical_path(self) -> str:
622 """The canonical name (resolved one, not alias name)."""
623 if self.parent is None: 623 ↛ 624line 623 didn't jump to line 624 because the condition on line 623 was never true
624 return self.name
625 if isinstance(self.parent, ExprName):
626 return f"{self.parent.canonical_path}.{self.name}"
627 if isinstance(self.parent, str): 627 ↛ 628line 627 didn't jump to line 628 because the condition on line 627 was never true
628 return f"{self.parent}.{self.name}"
629 try:
630 return self.parent.resolve(self.name)
631 except NameResolutionError:
632 return self.name
634 @property
635 def resolved(self) -> Module | Class | None:
636 """The resolved object this name refers to."""
637 try:
638 return self.parent.modules_collection[self.parent.resolve(self.name)] # type: ignore[union-attr]
639 except Exception: # noqa: BLE001
640 return self.parent.resolved[self.name] # type: ignore[union-attr,index]
642 @property
643 def is_enum_class(self) -> bool:
644 """Whether this name resolves to an enumeration class."""
645 try:
646 bases = self.resolved.bases # type: ignore[union-attr]
647 except Exception: # noqa: BLE001
648 return False
650 # TODO: Support inheritance?
651 # TODO: Support `StrEnum` and `IntEnum`.
652 return any(isinstance(base, Expr) and base.canonical_path == "enum.Enum" for base in bases)
654 @property
655 def is_enum_instance(self) -> bool:
656 """Whether this name resolves to an enumeration instance."""
657 try:
658 return self.parent.is_enum_class # type: ignore[union-attr]
659 except Exception: # noqa: BLE001
660 return False
662 @property
663 def is_enum_value(self) -> bool:
664 """Whether this name resolves to an enumeration value."""
665 try:
666 return self.name == "value" and self.parent.is_enum_instance # type: ignore[union-attr]
667 except Exception: # noqa: BLE001
668 return False
671# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
672@dataclass(eq=True, **_dataclass_opts)
673class ExprNamedExpr(Expr):
674 """Named/assignment expressions like `a := b`."""
676 target: Expr
677 """Target name."""
678 value: str | Expr
679 """Value."""
681 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
682 yield "("
683 yield from _yield(self.target, flat=flat)
684 yield " := "
685 yield from _yield(self.value, flat=flat)
686 yield ")"
689# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
690@dataclass(eq=True, **_dataclass_opts)
691class ExprParameter(Expr):
692 """Parameters in function signatures like `a: int = 0`."""
694 name: str
695 """Parameter name."""
696 kind: ParameterKind = ParameterKind.positional_or_keyword
697 """Parameter kind."""
698 annotation: Expr | None = None
699 """Parameter type."""
700 default: str | Expr | None = None
701 """Parameter default."""
704# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
705@dataclass(eq=True, **_dataclass_opts)
706class ExprSet(Expr):
707 """Sets like `{0, 1, 2}`."""
709 elements: Sequence[str | Expr]
710 """Set elements."""
712 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
713 yield "{"
714 yield from _join(self.elements, ", ", flat=flat)
715 yield "}"
718# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
719@dataclass(eq=True, **_dataclass_opts)
720class ExprSetComp(Expr):
721 """Set comprehensions like `{a for b in c}`."""
723 element: str | Expr
724 """Target value."""
725 generators: Sequence[Expr]
726 """Generators iterated on."""
728 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
729 yield "{"
730 yield from _yield(self.element, flat=flat)
731 yield " "
732 yield from _join(self.generators, " ", flat=flat)
733 yield "}"
736# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
737@dataclass(eq=True, **_dataclass_opts)
738class ExprSlice(Expr):
739 """Slices like `[a:b:c]`."""
741 lower: str | Expr | None = None
742 """Lower bound."""
743 upper: str | Expr | None = None
744 """Upper bound."""
745 step: str | Expr | None = None
746 """Iteration step."""
748 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
749 if self.lower is not None:
750 yield from _yield(self.lower, flat=flat)
751 yield ":"
752 if self.upper is not None: 752 ↛ 754line 752 didn't jump to line 754 because the condition on line 752 was always true
753 yield from _yield(self.upper, flat=flat)
754 if self.step is not None: 754 ↛ 755line 754 didn't jump to line 755 because the condition on line 754 was never true
755 yield ":"
756 yield from _yield(self.step, flat=flat)
759# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
760@dataclass(eq=True, **_dataclass_opts)
761class ExprSubscript(Expr):
762 """Subscripts like `a[b]`."""
764 left: str | Expr
765 """Left part."""
766 slice: Expr
767 """Slice part."""
769 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
770 yield from _yield(self.left, flat=flat)
771 yield "["
772 yield from _yield(self.slice, flat=flat)
773 yield "]"
775 @property
776 def path(self) -> str:
777 """The path of this subscript's left part."""
778 if isinstance(self.left, str):
779 return self.left
780 return self.left.path
782 @property
783 def canonical_path(self) -> str:
784 """The canonical path of this subscript's left part."""
785 if isinstance(self.left, str): 785 ↛ 786line 785 didn't jump to line 786 because the condition on line 785 was never true
786 return self.left
787 return self.left.canonical_path
790# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
791@dataclass(eq=True, **_dataclass_opts)
792class ExprTuple(Expr):
793 """Tuples like `(0, 1, 2)`."""
795 elements: Sequence[str | Expr]
796 """Tuple elements."""
797 implicit: bool = False
798 """Whether the tuple is implicit (e.g. without parentheses in a subscript's slice)."""
800 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
801 if not self.implicit:
802 yield "("
803 yield from _join(self.elements, ", ", flat=flat)
804 if not self.implicit:
805 yield ")"
808# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
809@dataclass(eq=True, **_dataclass_opts)
810class ExprUnaryOp(Expr):
811 """Unary operations like `-1`."""
813 operator: str
814 """Unary operator."""
815 value: str | Expr
816 """Value."""
818 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
819 yield self.operator
820 yield from _yield(self.value, flat=flat)
823# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
824@dataclass(eq=True, **_dataclass_opts)
825class ExprYield(Expr):
826 """Yield statements like `yield a`."""
828 value: str | Expr | None = None
829 """Yielded value."""
831 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
832 yield "yield"
833 if self.value is not None:
834 yield " "
835 yield from _yield(self.value, flat=flat)
838# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
839@dataclass(eq=True, **_dataclass_opts)
840class ExprYieldFrom(Expr):
841 """Yield statements like `yield from a`."""
843 value: str | Expr
844 """Yielded-from value."""
846 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
847 yield "yield from "
848 yield from _yield(self.value, flat=flat)
851_unary_op_map = {
852 ast.Invert: "~",
853 ast.Not: "not ",
854 ast.UAdd: "+",
855 ast.USub: "-",
856}
858_binary_op_map = {
859 ast.Add: "+",
860 ast.BitAnd: "&",
861 ast.BitOr: "|",
862 ast.BitXor: "^",
863 ast.Div: "/",
864 ast.FloorDiv: "//",
865 ast.LShift: "<<",
866 ast.MatMult: "@",
867 ast.Mod: "%",
868 ast.Mult: "*",
869 ast.Pow: "**",
870 ast.RShift: ">>",
871 ast.Sub: "-",
872}
874_bool_op_map = {
875 ast.And: "and",
876 ast.Or: "or",
877}
879_compare_op_map = {
880 ast.Eq: "==",
881 ast.NotEq: "!=",
882 ast.Lt: "<",
883 ast.LtE: "<=",
884 ast.Gt: ">",
885 ast.GtE: ">=",
886 ast.Is: "is",
887 ast.IsNot: "is not",
888 ast.In: "in",
889 ast.NotIn: "not in",
890}
893def _build_attribute(node: ast.Attribute, parent: Module | Class, **kwargs: Any) -> Expr:
894 left = _build(node.value, parent, **kwargs)
895 if isinstance(left, ExprAttribute):
896 left.append(ExprName(node.attr))
897 return left
898 if isinstance(left, ExprName):
899 return ExprAttribute([left, ExprName(node.attr, left)])
900 if isinstance(left, str):
901 return ExprAttribute([left, ExprName(node.attr, "str")])
902 return ExprAttribute([left, ExprName(node.attr)])
905def _build_binop(node: ast.BinOp, parent: Module | Class, **kwargs: Any) -> Expr:
906 return ExprBinOp(
907 _build(node.left, parent, **kwargs),
908 _binary_op_map[type(node.op)],
909 _build(node.right, parent, **kwargs),
910 )
913def _build_boolop(node: ast.BoolOp, parent: Module | Class, **kwargs: Any) -> Expr:
914 return ExprBoolOp(
915 _bool_op_map[type(node.op)],
916 [_build(value, parent, **kwargs) for value in node.values],
917 )
920def _build_call(node: ast.Call, parent: Module | Class, **kwargs: Any) -> Expr:
921 function = _build(node.func, parent, **kwargs)
922 positional_args = [_build(arg, parent, **kwargs) for arg in node.args]
923 keyword_args = [_build(kwarg, parent, function=function, **kwargs) for kwarg in node.keywords]
924 return ExprCall(function, [*positional_args, *keyword_args])
927def _build_compare(node: ast.Compare, parent: Module | Class, **kwargs: Any) -> Expr:
928 return ExprCompare(
929 _build(node.left, parent, **kwargs),
930 [_compare_op_map[type(op)] for op in node.ops],
931 [_build(comp, parent, **kwargs) for comp in node.comparators],
932 )
935def _build_comprehension(node: ast.comprehension, parent: Module | Class, **kwargs: Any) -> Expr:
936 return ExprComprehension(
937 _build(node.target, parent, **kwargs),
938 _build(node.iter, parent, **kwargs),
939 [_build(condition, parent, **kwargs) for condition in node.ifs],
940 is_async=bool(node.is_async),
941 )
944def _build_constant(
945 node: ast.Constant,
946 parent: Module | Class,
947 *,
948 in_formatted_str: bool = False,
949 in_joined_str: bool = False,
950 parse_strings: bool = False,
951 literal_strings: bool = False,
952 **kwargs: Any,
953) -> str | Expr:
954 if isinstance(node.value, str):
955 if in_joined_str and not in_formatted_str:
956 # We're in a f-string, not in a formatted value, don't keep quotes.
957 return node.value
958 if parse_strings and not literal_strings:
959 # We're in a place where a string could be a type annotation
960 # (and not in a Literal[...] type annotation).
961 # We parse the string and build from the resulting nodes again.
962 # If we fail to parse it (syntax errors), we consider it's a literal string and log a message.
963 try:
964 parsed = compile(
965 node.value,
966 mode="eval",
967 filename="<string-annotation>",
968 flags=ast.PyCF_ONLY_AST,
969 optimize=1,
970 )
971 except SyntaxError:
972 logger.debug(
973 f"Tried and failed to parse {node.value!r} as Python code, "
974 "falling back to using it as a string literal "
975 "(postponed annotations might help: https://peps.python.org/pep-0563/)",
976 )
977 else:
978 return _build(parsed.body, parent, **kwargs) # type: ignore[attr-defined]
979 return {type(...): lambda _: "..."}.get(type(node.value), repr)(node.value)
982def _build_dict(node: ast.Dict, parent: Module | Class, **kwargs: Any) -> Expr:
983 return ExprDict(
984 [None if key is None else _build(key, parent, **kwargs) for key in node.keys],
985 [_build(value, parent, **kwargs) for value in node.values],
986 )
989def _build_dictcomp(node: ast.DictComp, parent: Module | Class, **kwargs: Any) -> Expr:
990 return ExprDictComp(
991 _build(node.key, parent, **kwargs),
992 _build(node.value, parent, **kwargs),
993 [_build(gen, parent, **kwargs) for gen in node.generators],
994 )
997def _build_formatted(
998 node: ast.FormattedValue,
999 parent: Module | Class,
1000 *,
1001 in_formatted_str: bool = False, # noqa: ARG001
1002 **kwargs: Any,
1003) -> Expr:
1004 return ExprFormatted(_build(node.value, parent, in_formatted_str=True, **kwargs))
1007def _build_generatorexp(node: ast.GeneratorExp, parent: Module | Class, **kwargs: Any) -> Expr:
1008 return ExprGeneratorExp(
1009 _build(node.elt, parent, **kwargs),
1010 [_build(gen, parent, **kwargs) for gen in node.generators],
1011 )
1014def _build_ifexp(node: ast.IfExp, parent: Module | Class, **kwargs: Any) -> Expr:
1015 return ExprIfExp(
1016 _build(node.body, parent, **kwargs),
1017 _build(node.test, parent, **kwargs),
1018 _build(node.orelse, parent, **kwargs),
1019 )
1022def _build_joinedstr(
1023 node: ast.JoinedStr,
1024 parent: Module | Class,
1025 *,
1026 in_joined_str: bool = False, # noqa: ARG001
1027 **kwargs: Any,
1028) -> Expr:
1029 return ExprJoinedStr([_build(value, parent, in_joined_str=True, **kwargs) for value in node.values])
1032def _build_keyword(node: ast.keyword, parent: Module | Class, function: Expr | None = None, **kwargs: Any) -> Expr:
1033 if node.arg is None:
1034 return ExprVarKeyword(_build(node.value, parent, **kwargs))
1035 return ExprKeyword(node.arg, _build(node.value, parent, **kwargs), function=function)
1038def _build_lambda(node: ast.Lambda, parent: Module | Class, **kwargs: Any) -> Expr:
1039 return ExprLambda(
1040 parameters=[
1041 ExprParameter(
1042 name=name,
1043 kind=kind,
1044 annotation=None,
1045 default=default
1046 if isinstance(default, str)
1047 else safe_get_expression(default, parent=parent, parse_strings=False),
1048 )
1049 for name, _, kind, default in get_parameters(node.args)
1050 ],
1051 body=_build(node.body, parent, **kwargs),
1052 )
1055def _build_list(node: ast.List, parent: Module | Class, **kwargs: Any) -> Expr:
1056 return ExprList([_build(el, parent, **kwargs) for el in node.elts])
1059def _build_listcomp(node: ast.ListComp, parent: Module | Class, **kwargs: Any) -> Expr:
1060 return ExprListComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators])
1063def _build_name(node: ast.Name, parent: Module | Class, **kwargs: Any) -> Expr: # noqa: ARG001
1064 return ExprName(node.id, parent)
1067def _build_named_expr(node: ast.NamedExpr, parent: Module | Class, **kwargs: Any) -> Expr:
1068 return ExprNamedExpr(_build(node.target, parent, **kwargs), _build(node.value, parent, **kwargs))
1071def _build_set(node: ast.Set, parent: Module | Class, **kwargs: Any) -> Expr:
1072 return ExprSet([_build(el, parent, **kwargs) for el in node.elts])
1075def _build_setcomp(node: ast.SetComp, parent: Module | Class, **kwargs: Any) -> Expr:
1076 return ExprSetComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators])
1079def _build_slice(node: ast.Slice, parent: Module | Class, **kwargs: Any) -> Expr:
1080 return ExprSlice(
1081 None if node.lower is None else _build(node.lower, parent, **kwargs),
1082 None if node.upper is None else _build(node.upper, parent, **kwargs),
1083 None if node.step is None else _build(node.step, parent, **kwargs),
1084 )
1087def _build_starred(node: ast.Starred, parent: Module | Class, **kwargs: Any) -> Expr:
1088 return ExprVarPositional(_build(node.value, parent, **kwargs))
1091def _build_subscript(
1092 node: ast.Subscript,
1093 parent: Module | Class,
1094 *,
1095 parse_strings: bool = False,
1096 literal_strings: bool = False,
1097 in_subscript: bool = False, # noqa: ARG001
1098 **kwargs: Any,
1099) -> Expr:
1100 left = _build(node.value, parent, **kwargs)
1101 if parse_strings:
1102 if isinstance(left, (ExprAttribute, ExprName)) and left.canonical_path in {
1103 "typing.Literal",
1104 "typing_extensions.Literal",
1105 }:
1106 literal_strings = True
1107 slice = _build(
1108 node.slice,
1109 parent,
1110 parse_strings=True,
1111 literal_strings=literal_strings,
1112 in_subscript=True,
1113 **kwargs,
1114 )
1115 else:
1116 slice = _build(node.slice, parent, in_subscript=True, **kwargs)
1117 return ExprSubscript(left, slice)
1120def _build_tuple(
1121 node: ast.Tuple,
1122 parent: Module | Class,
1123 *,
1124 in_subscript: bool = False,
1125 **kwargs: Any,
1126) -> Expr:
1127 return ExprTuple([_build(el, parent, **kwargs) for el in node.elts], implicit=in_subscript)
1130def _build_unaryop(node: ast.UnaryOp, parent: Module | Class, **kwargs: Any) -> Expr:
1131 return ExprUnaryOp(_unary_op_map[type(node.op)], _build(node.operand, parent, **kwargs))
1134def _build_yield(node: ast.Yield, parent: Module | Class, **kwargs: Any) -> Expr:
1135 return ExprYield(None if node.value is None else _build(node.value, parent, **kwargs))
1138def _build_yield_from(node: ast.YieldFrom, parent: Module | Class, **kwargs: Any) -> Expr:
1139 return ExprYieldFrom(_build(node.value, parent, **kwargs))
1142_node_map: dict[type, Callable[[Any, Module | Class], Expr]] = {
1143 ast.Attribute: _build_attribute,
1144 ast.BinOp: _build_binop,
1145 ast.BoolOp: _build_boolop,
1146 ast.Call: _build_call,
1147 ast.Compare: _build_compare,
1148 ast.comprehension: _build_comprehension,
1149 ast.Constant: _build_constant, # type: ignore[dict-item]
1150 ast.Dict: _build_dict,
1151 ast.DictComp: _build_dictcomp,
1152 ast.FormattedValue: _build_formatted,
1153 ast.GeneratorExp: _build_generatorexp,
1154 ast.IfExp: _build_ifexp,
1155 ast.JoinedStr: _build_joinedstr,
1156 ast.keyword: _build_keyword,
1157 ast.Lambda: _build_lambda,
1158 ast.List: _build_list,
1159 ast.ListComp: _build_listcomp,
1160 ast.Name: _build_name,
1161 ast.NamedExpr: _build_named_expr,
1162 ast.Set: _build_set,
1163 ast.SetComp: _build_setcomp,
1164 ast.Slice: _build_slice,
1165 ast.Starred: _build_starred,
1166 ast.Subscript: _build_subscript,
1167 ast.Tuple: _build_tuple,
1168 ast.UnaryOp: _build_unaryop,
1169 ast.Yield: _build_yield,
1170 ast.YieldFrom: _build_yield_from,
1171}
1173# YORE: EOL 3.8: Remove block.
1174if sys.version_info < (3, 9):
1176 def _build_extslice(node: ast.ExtSlice, parent: Module | Class, **kwargs: Any) -> Expr:
1177 return ExprExtSlice([_build(dim, parent, **kwargs) for dim in node.dims])
1179 def _build_index(node: ast.Index, parent: Module | Class, **kwargs: Any) -> Expr:
1180 return _build(node.value, parent, **kwargs)
1182 _node_map[ast.ExtSlice] = _build_extslice
1183 _node_map[ast.Index] = _build_index
1186def _build(node: ast.AST, parent: Module | Class, **kwargs: Any) -> Expr:
1187 return _node_map[type(node)](node, parent, **kwargs)
1190def get_expression(
1191 node: ast.AST | None,
1192 parent: Module | Class,
1193 *,
1194 parse_strings: bool | None = None,
1195) -> Expr | None:
1196 """Build an expression from an AST.
1198 Parameters:
1199 node: The annotation node.
1200 parent: The parent used to resolve the name.
1201 parse_strings: Whether to try and parse strings as type annotations.
1203 Returns:
1204 A string or resovable name or expression.
1205 """
1206 if node is None:
1207 return None
1208 if parse_strings is None:
1209 try:
1210 module = parent.module
1211 except ValueError:
1212 parse_strings = False
1213 else:
1214 parse_strings = not module.imports_future_annotations
1215 return _build(node, parent, parse_strings=parse_strings)
1218def safe_get_expression(
1219 node: ast.AST | None,
1220 parent: Module | Class,
1221 *,
1222 parse_strings: bool | None = None,
1223 log_level: LogLevel | None = LogLevel.error,
1224 msg_format: str = "{path}:{lineno}: Failed to get expression from {node_class}: {error}",
1225) -> Expr | None:
1226 """Safely (no exception) build a resolvable annotation.
1228 Parameters:
1229 node: The annotation node.
1230 parent: The parent used to resolve the name.
1231 parse_strings: Whether to try and parse strings as type annotations.
1232 log_level: Log level to use to log a message. None to disable logging.
1233 msg_format: A format string for the log message. Available placeholders:
1234 path, lineno, node, error.
1236 Returns:
1237 A string or resovable name or expression.
1238 """
1239 try:
1240 return get_expression(node, parent, parse_strings=parse_strings)
1241 except Exception as error: # noqa: BLE001
1242 if log_level is None: 1242 ↛ 1243line 1242 didn't jump to line 1243 because the condition on line 1242 was never true
1243 return None
1244 node_class = node.__class__.__name__
1245 try:
1246 path: Path | str = parent.relative_filepath
1247 except ValueError:
1248 path = "<in-memory>"
1249 lineno = node.lineno # type: ignore[union-attr]
1250 error_str = f"{error.__class__.__name__}: {error}"
1251 message = msg_format.format(path=path, lineno=lineno, node_class=node_class, error=error_str)
1252 getattr(logger, log_level.value)(message)
1253 return None
1256_msg_format = "{path}:{lineno}: Failed to get %s expression from {node_class}: {error}"
1257get_annotation = partial(get_expression, parse_strings=None)
1258safe_get_annotation = partial(
1259 safe_get_expression,
1260 parse_strings=None,
1261 msg_format=_msg_format % "annotation",
1262)
1263get_base_class = partial(get_expression, parse_strings=False)
1264safe_get_base_class = partial(
1265 safe_get_expression,
1266 parse_strings=False,
1267 msg_format=_msg_format % "base class",
1268)
1269get_condition = partial(get_expression, parse_strings=False)
1270safe_get_condition = partial(
1271 safe_get_expression,
1272 parse_strings=False,
1273 msg_format=_msg_format % "condition",
1274)