Coverage for packages / griffelib / src / griffe / _internal / expressions.py: 92.71%

752 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-11 11:48 +0100

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. 

6 

7from __future__ import annotations 

8 

9import ast 

10import sys 

11from dataclasses import dataclass 

12from dataclasses import fields as getfields 

13from enum import IntEnum, auto 

14from functools import partial 

15from typing import TYPE_CHECKING, Any, Protocol 

16 

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 

21 

22if TYPE_CHECKING: 

23 from collections.abc import Iterable, Iterator, Sequence 

24 from pathlib import Path 

25 

26 from griffe._internal.models import Class, Function, Module 

27 

28 

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 

35 

36 # The enum members are declared in ascending order of precedence. 

37 

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() 

43 

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...}` 

64 

65 

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 

90 

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 

107 

108 

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. 

116 

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) 

130 

131 

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 

141 

142 

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 

151 

152 

153_modern_types = { 

154 "typing.Tuple": "tuple", 

155 "typing.Dict": "dict", 

156 "typing.List": "list", 

157 "typing.Set": "set", 

158} 

159 

160 

161@dataclass 

162class Expr: 

163 """Base class for expressions.""" 

164 

165 def __str__(self) -> str: 

166 return "".join(elem if isinstance(elem, str) else elem.name for elem in self.iterate(flat=True)) # ty:ignore[unresolved-attribute] 

167 

168 def __iter__(self) -> Iterator[str | Expr]: 

169 """Iterate on the expression syntax and elements.""" 

170 yield from self.iterate(flat=False) 

171 

172 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002 

173 """Iterate on the expression elements. 

174 

175 Parameters: 

176 flat: Expressions are trees. 

177 

178 When flat is false, this method iterates only on the first layer of the tree. 

179 To iterate on all the subparts of the expression, you have to do so recursively. 

180 It allows to handle each subpart specifically (for example subscripts, attribute, etc.), 

181 without them getting rendered as strings. 

182 

183 On the contrary, when flat is true, the whole tree is flattened as a sequence 

184 of strings and instances of [Names][griffe.ExprName]. 

185 

186 Yields: 

187 Strings and names when flat, strings and expressions otherwise. 

188 """ 

189 yield from () 

190 

191 def modernize(self) -> Expr: 

192 """Modernize the expression. 

193 

194 For example, use PEP 604 type unions `|` instead of `typing.Union`. 

195 

196 Returns: 

197 A modernized expression. 

198 """ 

199 return self 

200 

201 def as_dict(self, **kwargs: Any) -> dict[str, Any]: 

202 """Return the expression as a dictionary. 

203 

204 Parameters: 

205 **kwargs: Configuration options (none available yet). 

206 

207 

208 Returns: 

209 A dictionary. 

210 """ 

211 return _expr_as_dict(self, **kwargs) 

212 

213 @property 

214 def classname(self) -> str: 

215 """The expression class name.""" 

216 return self.__class__.__name__ 

217 

218 @property 

219 def path(self) -> str: 

220 """Path of the expressed name/attribute.""" 

221 return str(self) 

222 

223 @property 

224 def canonical_path(self) -> str: 

225 """Path of the expressed name/attribute.""" 

226 return str(self) 

227 

228 @property 

229 def canonical_name(self) -> str: 

230 """Name of the expressed name/attribute/parameter.""" 

231 # We must handle things like `griffe.Visitor` and `griffe.Visitor(code)`. 

232 return self.canonical_path.rsplit(".", 1)[-1].split("(", 1)[-1].removesuffix(")") 

233 

234 @property 

235 def is_classvar(self) -> bool: 

236 """Whether this attribute is annotated with `ClassVar`.""" 

237 return isinstance(self, ExprSubscript) and self.canonical_name == "ClassVar" 

238 

239 @property 

240 def is_tuple(self) -> bool: 

241 """Whether this expression is a tuple.""" 

242 return isinstance(self, ExprSubscript) and self.canonical_name.lower() == "tuple" 

243 

244 @property 

245 def is_iterator(self) -> bool: 

246 """Whether this expression is an iterator.""" 

247 return isinstance(self, ExprSubscript) and self.canonical_name == "Iterator" 

248 

249 @property 

250 def is_generator(self) -> bool: 

251 """Whether this expression is a generator.""" 

252 return isinstance(self, ExprSubscript) and self.canonical_name == "Generator" 

253 

254 @staticmethod 

255 def _to_binop(elements: Sequence[Expr], op: str) -> ExprBinOp: 

256 if len(elements) == 2: # noqa: PLR2004 

257 left, right = elements 

258 if isinstance(left, Expr): 258 ↛ 260line 258 didn't jump to line 260 because the condition on line 258 was always true

259 left = left.modernize() 

260 if isinstance(right, Expr): 260 ↛ 262line 260 didn't jump to line 262 because the condition on line 260 was always true

261 right = right.modernize() 

262 return ExprBinOp(left=left, operator=op, right=right) 

263 

264 left = ExprSubscript._to_binop(elements[:-1], op=op) 

265 right = elements[-1] 

266 if isinstance(right, Expr): 266 ↛ 268line 266 didn't jump to line 268 because the condition on line 266 was always true

267 right = right.modernize() 

268 return ExprBinOp(left=left, operator=op, right=right) 

269 

270 

271@dataclass(eq=True, slots=True) 

272class ExprAttribute(Expr): 

273 """Attributes like `a.b`.""" 

274 

275 values: list[str | Expr] 

276 """The different parts of the dotted chain.""" 

277 

278 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

279 precedence = _get_precedence(self) 

280 yield from _yield(self.values[0], flat=flat, outer_precedence=precedence, is_left=True) 

281 for value in self.values[1:]: 

282 yield "." 

283 yield from _yield(value, flat=flat, outer_precedence=precedence) 

284 

285 def modernize(self) -> ExprName | ExprAttribute: 

286 if modern := _modern_types.get(self.canonical_path): 

287 return ExprName(modern, parent=self.last.parent) 

288 return self 

289 

290 def append(self, value: ExprName) -> None: 

291 """Append a name to this attribute. 

292 

293 Parameters: 

294 value: The expression name to append. 

295 """ 

296 if value.parent is None: 296 ↛ 298line 296 didn't jump to line 298 because the condition on line 296 was always true

297 value.parent = self.last 

298 self.values.append(value) 

299 

300 @property 

301 def last(self) -> ExprName: 

302 """The last part of this attribute (on the right).""" 

303 # All values except the first one can *only* be names: 

304 # we can't do `a.(b or c)` or `a."string"`. 

305 return self.values[-1] # ty:ignore[invalid-return-type] 

306 

307 @property 

308 def first(self) -> str | Expr: 

309 """The first part of this attribute (on the left).""" 

310 return self.values[0] 

311 

312 @property 

313 def path(self) -> str: 

314 """The path of this attribute.""" 

315 return self.last.path 

316 

317 @property 

318 def canonical_path(self) -> str: 

319 """The canonical path of this attribute.""" 

320 return self.last.canonical_path 

321 

322 

323@dataclass(eq=True, slots=True) 

324class ExprBinOp(Expr): 

325 """Binary operations like `a + b`.""" 

326 

327 left: str | Expr 

328 """Left part.""" 

329 operator: str 

330 """Binary operator.""" 

331 right: str | Expr 

332 """Right part.""" 

333 

334 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

335 precedence = _get_precedence(self) 

336 right_precedence = precedence 

337 if self.operator == "**" and isinstance(self.right, ExprUnaryOp): 

338 # Unary operators on the right have higher precedence, e.g. `a ** -b`. 

339 right_precedence = _OperatorPrecedence(precedence - 1) 

340 yield from _yield(self.left, flat=flat, outer_precedence=precedence, is_left=True) 

341 yield f" {self.operator} " 

342 yield from _yield(self.right, flat=flat, outer_precedence=right_precedence, is_left=False) 

343 

344 

345@dataclass(eq=True, slots=True) 

346class ExprBoolOp(Expr): 

347 """Boolean operations like `a or b`.""" 

348 

349 operator: str 

350 """Boolean operator.""" 

351 values: Sequence[str | Expr] 

352 """Operands.""" 

353 

354 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

355 precedence = _get_precedence(self) 

356 it = iter(self.values) 

357 yield from _yield(next(it), flat=flat, outer_precedence=precedence, is_left=True) 

358 for value in it: 

359 yield f" {self.operator} " 

360 yield from _yield(value, flat=flat, outer_precedence=precedence, is_left=False) 

361 

362 

363@dataclass(eq=True, slots=True) 

364class ExprCall(Expr): 

365 """Calls like `f()`.""" 

366 

367 function: Expr 

368 """Function called.""" 

369 arguments: Sequence[str | Expr] 

370 """Passed arguments.""" 

371 

372 @property 

373 def canonical_path(self) -> str: 

374 """The canonical path of this subscript's left part.""" 

375 return self.function.canonical_path 

376 

377 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

378 yield from _yield(self.function, flat=flat, outer_precedence=_get_precedence(self)) 

379 yield "(" 

380 yield from _join(self.arguments, ", ", flat=flat) 

381 yield ")" 

382 

383 

384@dataclass(eq=True, slots=True) 

385class ExprCompare(Expr): 

386 """Comparisons like `a > b`.""" 

387 

388 left: str | Expr 

389 """Left part.""" 

390 operators: Sequence[str] 

391 """Comparison operators.""" 

392 comparators: Sequence[str | Expr] 

393 """Things compared.""" 

394 

395 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

396 precedence = _get_precedence(self) 

397 yield from _yield(self.left, flat=flat, outer_precedence=precedence, is_left=True) 

398 for op, comp in zip(self.operators, self.comparators, strict=False): 

399 yield f" {op} " 

400 yield from _yield(comp, flat=flat, outer_precedence=precedence) 

401 

402 

403@dataclass(eq=True, slots=True) 

404class ExprComprehension(Expr): 

405 """Comprehensions like `a for b in c if d`.""" 

406 

407 target: str | Expr 

408 """Comprehension target (value added to the result).""" 

409 iterable: str | Expr 

410 """Value iterated on.""" 

411 conditions: Sequence[str | Expr] 

412 """Conditions to include the target in the result.""" 

413 is_async: bool = False 

414 """Async comprehension or not.""" 

415 

416 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

417 if self.is_async: 417 ↛ 418line 417 didn't jump to line 418 because the condition on line 417 was never true

418 yield "async " 

419 yield "for " 

420 yield from _yield(self.target, flat=flat) 

421 yield " in " 

422 yield from _yield(self.iterable, flat=flat) 

423 if self.conditions: 

424 yield " if " 

425 yield from _join(self.conditions, " if ", flat=flat) 

426 

427 

428# TODO: `ExprConstant` is never instantiated, 

429# see `_build_constant` below (it always returns the value directly). 

430# Maybe we could simply get rid of it, as it wouldn't bring much value 

431# if used anyway. 

432@dataclass(eq=True, slots=True) 

433class ExprConstant(Expr): 

434 """Constants like `"a"` or `1`.""" 

435 

436 value: str 

437 """Constant value.""" 

438 

439 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002 

440 yield self.value 

441 

442 

443@dataclass(eq=True, slots=True) 

444class ExprDict(Expr): 

445 """Dictionaries like `{"a": 0}`.""" 

446 

447 keys: Sequence[str | Expr | None] 

448 """Dict keys.""" 

449 values: Sequence[str | Expr] 

450 """Dict values.""" 

451 

452 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

453 yield "{" 

454 yield from _join( 

455 (("None" if key is None else key, ": ", value) for key, value in zip(self.keys, self.values, strict=False)), 

456 ", ", 

457 flat=flat, 

458 ) 

459 yield "}" 

460 

461 

462@dataclass(eq=True, slots=True) 

463class ExprDictComp(Expr): 

464 """Dict comprehensions like `{k: v for k, v in a}`.""" 

465 

466 key: str | Expr 

467 """Target key.""" 

468 value: str | Expr 

469 """Target value.""" 

470 generators: Sequence[Expr] 

471 """Generators iterated on.""" 

472 

473 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

474 yield "{" 

475 yield from _yield(self.key, flat=flat) 

476 yield ": " 

477 yield from _yield(self.value, flat=flat) 

478 yield " " 

479 yield from _join(self.generators, " ", flat=flat) 

480 yield "}" 

481 

482 

483@dataclass(eq=True, slots=True) 

484class ExprExtSlice(Expr): 

485 """Extended slice like `a[x:y, z]`.""" 

486 

487 dims: Sequence[str | Expr] 

488 """Dims.""" 

489 

490 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

491 yield from _join(self.dims, ", ", flat=flat) 

492 

493 

494@dataclass(eq=True, slots=True) 

495class ExprFormatted(Expr): 

496 """Formatted string like `{1 + 1}`.""" 

497 

498 value: str | Expr 

499 """Formatted value.""" 

500 

501 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

502 yield "{" 

503 # Prevent parentheses from being added, avoiding `{(1 + 1)}` 

504 yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.NONE) 

505 yield "}" 

506 

507 

508@dataclass(eq=True, slots=True) 

509class ExprGeneratorExp(Expr): 

510 """Generator expressions like `a for b in c for d in e`.""" 

511 

512 element: str | Expr 

513 """Yielded element.""" 

514 generators: Sequence[Expr] 

515 """Generators iterated on.""" 

516 

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) 

521 

522 

523@dataclass(eq=True, slots=True) 

524class ExprIfExp(Expr): 

525 """Conditions like `a if b else c`.""" 

526 

527 body: str | Expr 

528 """Value if test.""" 

529 test: str | Expr 

530 """Condition.""" 

531 orelse: str | Expr 

532 """Other expression.""" 

533 

534 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

535 precedence = _get_precedence(self) 

536 yield from _yield(self.body, flat=flat, outer_precedence=precedence, is_left=True) 

537 yield " if " 

538 # If the test itself is another if/else, its precedence is the same, which will not give 

539 # a parenthesis: force it. 

540 test_outer_precedence = _OperatorPrecedence(precedence + 1) 

541 yield from _yield(self.test, flat=flat, outer_precedence=test_outer_precedence) 

542 yield " else " 

543 # If/else is right associative. For example, a nested if/else 

544 # `a if b else c if d else e` is effectively `a if b else (c if d else e)`, so produce a 

545 # flattened version without parentheses. 

546 if isinstance(self.orelse, ExprIfExp): 546 ↛ 547line 546 didn't jump to line 547 because the condition on line 546 was never true

547 yield from self.orelse.iterate(flat=flat) 

548 else: 

549 yield from _yield(self.orelse, flat=flat, outer_precedence=precedence, is_left=False) 

550 

551 

552@dataclass(eq=True, slots=True) 

553class ExprInterpolation(Expr): 

554 """Template string interpolation like `{name}`.""" 

555 

556 value: str | Expr 

557 """Interpolated value.""" 

558 

559 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

560 yield "{" 

561 # Prevent parentheses from being added, avoiding `{(1 + 1)}` 

562 yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.NONE) 

563 yield "}" 

564 

565 

566@dataclass(eq=True, slots=True) 

567class ExprJoinedStr(Expr): 

568 """Joined strings like `f"a {b} c"`.""" 

569 

570 values: Sequence[str | Expr] 

571 """Joined values.""" 

572 

573 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

574 yield "f'" 

575 yield from _join(self.values, "", flat=flat) 

576 yield "'" 

577 

578 

579@dataclass(eq=True, slots=True) 

580class ExprKeyword(Expr): 

581 """Keyword arguments like `a=b`.""" 

582 

583 name: str 

584 """Name.""" 

585 value: str | Expr 

586 """Value.""" 

587 

588 # Griffe is designed around accessing Python objects 

589 # with the dot notation, for example `module.Class`. 

590 # Function parameters were not taken into account 

591 # because they are not accessible the same way. 

592 # But we still want to be able to cross-reference 

593 # documentation of function parameters in downstream 

594 # tools like mkdocstrings. So we add a special case 

595 # for keyword expressions, where they get a meaningful 

596 # canonical path (contrary to most other expressions that 

597 # aren't or do not begin with names or attributes) 

598 # of the form `path.to.called_function(param_name)`. 

599 # For this we need to store a reference to the `func` part 

600 # of the call expression in the keyword one, 

601 # hence the following field. 

602 # We allow it to be None for backward compatibility. 

603 function: Expr | None = None 

604 """Expression referencing the function called with this parameter.""" 

605 

606 @property 

607 def canonical_path(self) -> str: 

608 """Path of the expressed keyword.""" 

609 if self.function: 

610 return f"{self.function.canonical_path}({self.name})" 

611 return super(ExprKeyword, self).canonical_path 

612 

613 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

614 yield self.name 

615 yield "=" 

616 yield from _yield(self.value, flat=flat) 

617 

618 

619@dataclass(eq=True, slots=True) 

620class ExprVarPositional(Expr): 

621 """Variadic positional parameters like `*args`.""" 

622 

623 value: Expr 

624 """Starred value.""" 

625 

626 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

627 yield "*" 

628 yield from _yield(self.value, flat=flat) 

629 

630 

631@dataclass(eq=True, slots=True) 

632class ExprVarKeyword(Expr): 

633 """Variadic keyword parameters like `**kwargs`.""" 

634 

635 value: Expr 

636 """Double-starred value.""" 

637 

638 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

639 yield "**" 

640 yield from _yield(self.value, flat=flat) 

641 

642 

643@dataclass(eq=True, slots=True) 

644class ExprLambda(Expr): 

645 """Lambda expressions like `lambda a: a.b`.""" 

646 

647 parameters: Sequence[ExprParameter] 

648 """Lambda's parameters.""" 

649 body: str | Expr 

650 """Lambda's body.""" 

651 

652 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

653 pos_only = False 

654 pos_or_kw = False 

655 kw_only = False 

656 length = len(self.parameters) 

657 yield "lambda" 

658 if length: 

659 yield " " 

660 for index, parameter in enumerate(self.parameters, 1): 

661 if parameter.kind is ParameterKind.positional_only: 

662 pos_only = True 

663 elif parameter.kind is ParameterKind.var_positional: 

664 yield "*" 

665 elif parameter.kind is ParameterKind.var_keyword: 

666 yield "**" 

667 elif parameter.kind is ParameterKind.positional_or_keyword and not pos_or_kw: 

668 pos_or_kw = True 

669 elif parameter.kind is ParameterKind.keyword_only and not kw_only: 

670 kw_only = True 

671 yield "*, " 

672 if parameter.kind is not ParameterKind.positional_only and pos_only: 

673 pos_only = False 

674 yield "/, " 

675 yield parameter.name 

676 if parameter.default and parameter.kind not in (ParameterKind.var_positional, ParameterKind.var_keyword): 

677 yield "=" 

678 yield from _yield(parameter.default, flat=flat) 

679 if index < length: 

680 yield ", " 

681 yield ": " 

682 # Body of lambda should not have parentheses, avoiding `lambda: a.b` 

683 yield from _yield(self.body, flat=flat, outer_precedence=_OperatorPrecedence.NONE) 

684 

685 

686@dataclass(eq=True, slots=True) 

687class ExprList(Expr): 

688 """Lists like `[0, 1, 2]`.""" 

689 

690 elements: Sequence[Expr] 

691 """List elements.""" 

692 

693 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

694 yield "[" 

695 yield from _join(self.elements, ", ", flat=flat) 

696 yield "]" 

697 

698 

699@dataclass(eq=True, slots=True) 

700class ExprListComp(Expr): 

701 """List comprehensions like `[a for b in c]`.""" 

702 

703 element: str | Expr 

704 """Target value.""" 

705 generators: Sequence[Expr] 

706 """Generators iterated on.""" 

707 

708 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

709 yield "[" 

710 yield from _yield(self.element, flat=flat) 

711 yield " " 

712 yield from _join(self.generators, " ", flat=flat) 

713 yield "]" 

714 

715 

716@dataclass(eq=False, slots=True) 

717class ExprName(Expr): # noqa: PLW1641 

718 """This class represents a Python object identified by a name in a given scope.""" 

719 

720 name: str 

721 """Actual name.""" 

722 parent: str | ExprName | Module | Class | Function | None = None 

723 """Parent (for resolution in its scope).""" 

724 member: str | None = None 

725 """Member name (for resolution in its scope).""" 

726 

727 def __eq__(self, other: object) -> bool: 

728 """Two name expressions are equal if they have the same `name` value (`parent` is ignored).""" 

729 if isinstance(other, ExprName): 

730 return self.name == other.name 

731 return NotImplemented 

732 

733 def iterate(self, *, flat: bool = True) -> Iterator[ExprName]: # noqa: ARG002 

734 yield self 

735 

736 def modernize(self) -> ExprName: 

737 if modern := _modern_types.get(self.canonical_path): 

738 return ExprName(modern, parent=self.parent) 

739 return self 

740 

741 @property 

742 def path(self) -> str: 

743 """The full, resolved name. 

744 

745 If it was given when creating the name, return that. 

746 If a callable was given, call it and return its result. 

747 It the name cannot be resolved, return the source. 

748 """ 

749 if isinstance(self.parent, ExprName): 

750 return f"{self.parent.path}.{self.name}" 

751 return self.name 

752 

753 @property 

754 def canonical_path(self) -> str: 

755 """The canonical name (resolved one, not alias name).""" 

756 if self.parent is None: 756 ↛ 757line 756 didn't jump to line 757 because the condition on line 756 was never true

757 return self.name 

758 if isinstance(self.parent, ExprName): 

759 return f"{self.parent.canonical_path}.{self.name}" 

760 if isinstance(self.parent, str): 760 ↛ 761line 760 didn't jump to line 761 because the condition on line 760 was never true

761 return f"{self.parent}.{self.name}" 

762 parent = self.parent.members.get(self.member, self.parent) # ty:ignore[no-matching-overload] 

763 try: 

764 return parent.resolve(self.name) 

765 except NameResolutionError: 

766 return self.name 

767 

768 @property 

769 def resolved(self) -> Module | Class | None: 

770 """The resolved object this name refers to.""" 

771 try: 

772 return self.parent.modules_collection[self.parent.resolve(self.name)] # ty:ignore[possibly-missing-attribute] 

773 except Exception: # noqa: BLE001 

774 return self.parent.resolved[self.name] # ty:ignore[not-subscriptable, possibly-missing-attribute] 

775 

776 @property 

777 def is_enum_class(self) -> bool: 

778 """Whether this name resolves to an enumeration class.""" 

779 try: 

780 bases = self.resolved.bases # ty:ignore[possibly-missing-attribute] 

781 except Exception: # noqa: BLE001 

782 return False 

783 

784 # TODO: Support inheritance? 

785 # TODO: Support `StrEnum` and `IntEnum`. 

786 return any(isinstance(base, Expr) and base.canonical_path == "enum.Enum" for base in bases) 

787 

788 @property 

789 def is_enum_instance(self) -> bool: 

790 """Whether this name resolves to an enumeration instance.""" 

791 try: 

792 return self.parent.is_enum_class # ty:ignore[possibly-missing-attribute] 

793 except Exception: # noqa: BLE001 

794 return False 

795 

796 @property 

797 def is_enum_value(self) -> bool: 

798 """Whether this name resolves to an enumeration value.""" 

799 try: 

800 return self.name == "value" and self.parent.is_enum_instance # ty:ignore[possibly-missing-attribute] 

801 except Exception: # noqa: BLE001 

802 return False 

803 

804 @property 

805 def is_type_parameter(self) -> bool: 

806 """Whether this name resolves to a type parameter.""" 

807 return "[" in self.canonical_path 

808 

809 

810@dataclass(eq=True, slots=True) 

811class ExprNamedExpr(Expr): 

812 """Named/assignment expressions like `a := b`.""" 

813 

814 target: Expr 

815 """Target name.""" 

816 value: str | Expr 

817 """Value.""" 

818 

819 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

820 yield from _yield(self.target, flat=flat) 

821 yield " := " 

822 yield from _yield(self.value, flat=flat) 

823 

824 

825@dataclass(eq=True, slots=True) 

826class ExprParameter(Expr): 

827 """Parameters in function signatures like `a: int = 0`.""" 

828 

829 name: str 

830 """Parameter name.""" 

831 kind: ParameterKind = ParameterKind.positional_or_keyword 

832 """Parameter kind.""" 

833 annotation: Expr | None = None 

834 """Parameter type.""" 

835 default: str | Expr | None = None 

836 """Parameter default.""" 

837 

838 

839@dataclass(eq=True, slots=True) 

840class ExprSet(Expr): 

841 """Sets like `{0, 1, 2}`.""" 

842 

843 elements: Sequence[str | Expr] 

844 """Set elements.""" 

845 

846 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

847 yield "{" 

848 yield from _join(self.elements, ", ", flat=flat) 

849 yield "}" 

850 

851 

852@dataclass(eq=True, slots=True) 

853class ExprSetComp(Expr): 

854 """Set comprehensions like `{a for b in c}`.""" 

855 

856 element: str | Expr 

857 """Target value.""" 

858 generators: Sequence[Expr] 

859 """Generators iterated on.""" 

860 

861 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

862 yield "{" 

863 yield from _yield(self.element, flat=flat) 

864 yield " " 

865 yield from _join(self.generators, " ", flat=flat) 

866 yield "}" 

867 

868 

869@dataclass(eq=True, slots=True) 

870class ExprSlice(Expr): 

871 """Slices like `[a:b:c]`.""" 

872 

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.""" 

879 

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) 

889 

890 

891@dataclass(eq=True, slots=True) 

892class ExprSubscript(Expr): 

893 """Subscripts like `a[b]`.""" 

894 

895 left: str | Expr 

896 """Left part.""" 

897 slice: str | Expr 

898 """Slice part.""" 

899 

900 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

901 yield from _yield(self.left, flat=flat, outer_precedence=_get_precedence(self)) 

902 yield "[" 

903 # Prevent parentheses from being added, avoiding `a[(b)]` 

904 yield from _yield(self.slice, flat=flat, outer_precedence=_OperatorPrecedence.NONE) 

905 yield "]" 

906 

907 def modernize(self) -> ExprBinOp | ExprSubscript: 

908 if self.canonical_path == "typing.Union": 

909 return self._to_binop(self.slice.elements, op="|") # ty:ignore[unresolved-attribute] 

910 if self.canonical_path == "typing.Optional": 

911 left = self.slice if isinstance(self.slice, str) else self.slice.modernize() 

912 return ExprBinOp(left=left, operator="|", right="None") 

913 return ExprSubscript( 

914 left=self.left if isinstance(self.left, str) else self.left.modernize(), 

915 slice=self.slice if isinstance(self.slice, str) else self.slice.modernize(), 

916 ) 

917 

918 @property 

919 def path(self) -> str: 

920 """The path of this subscript's left part.""" 

921 if isinstance(self.left, str): 

922 return self.left 

923 return self.left.path 

924 

925 @property 

926 def canonical_path(self) -> str: 

927 """The canonical path of this subscript's left part.""" 

928 if isinstance(self.left, str): 928 ↛ 929line 928 didn't jump to line 929 because the condition on line 928 was never true

929 return self.left 

930 return self.left.canonical_path 

931 

932 

933@dataclass(eq=True, slots=True) 

934class ExprTemplateStr(Expr): 

935 """Template strings like `t"a {name}"`.""" 

936 

937 values: Sequence[str | Expr] 

938 """Joined values.""" 

939 

940 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

941 yield "t'" 

942 yield from _join(self.values, "", flat=flat) 

943 yield "'" 

944 

945 

946@dataclass(eq=True, slots=True) 

947class ExprTuple(Expr): 

948 """Tuples like `(0, 1, 2)`.""" 

949 

950 elements: Sequence[str | Expr] 

951 """Tuple elements.""" 

952 implicit: bool = False 

953 """Whether the tuple is implicit (e.g. without parentheses in a subscript's slice).""" 

954 

955 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

956 if not self.implicit: 

957 yield "(" 

958 yield from _join(self.elements, ", ", flat=flat) 

959 if len(self.elements) == 1: 

960 yield "," 

961 if not self.implicit: 

962 yield ")" 

963 

964 def modernize(self) -> ExprTuple: 

965 return ExprTuple( 

966 elements=[el if isinstance(el, str) else el.modernize() for el in self.elements], 

967 implicit=self.implicit, 

968 ) 

969 

970 

971@dataclass(eq=True, slots=True) 

972class ExprUnaryOp(Expr): 

973 """Unary operations like `-1`.""" 

974 

975 operator: str 

976 """Unary operator.""" 

977 value: str | Expr 

978 """Value.""" 

979 

980 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

981 yield self.operator 

982 if self.operator == "not": 

983 yield " " 

984 yield from _yield(self.value, flat=flat, outer_precedence=_get_precedence(self)) 

985 

986 

987@dataclass(eq=True, slots=True) 

988class ExprYield(Expr): 

989 """Yield statements like `yield a`.""" 

990 

991 value: str | Expr | None = None 

992 """Yielded value.""" 

993 

994 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

995 yield "yield" 

996 if self.value is not None: 

997 yield " " 

998 yield from _yield(self.value, flat=flat) 

999 

1000 

1001@dataclass(eq=True, slots=True) 

1002class ExprYieldFrom(Expr): 

1003 """Yield statements like `yield from a`.""" 

1004 

1005 value: str | Expr 

1006 """Yielded-from value.""" 

1007 

1008 def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: 

1009 yield "yield from " 

1010 yield from _yield(self.value, flat=flat) 

1011 

1012 

1013_unary_op_map = { 

1014 ast.Invert: "~", 

1015 ast.Not: "not", 

1016 ast.UAdd: "+", 

1017 ast.USub: "-", 

1018} 

1019 

1020_binary_op_map = { 

1021 ast.Add: "+", 

1022 ast.BitAnd: "&", 

1023 ast.BitOr: "|", 

1024 ast.BitXor: "^", 

1025 ast.Div: "/", 

1026 ast.FloorDiv: "//", 

1027 ast.LShift: "<<", 

1028 ast.MatMult: "@", 

1029 ast.Mod: "%", 

1030 ast.Mult: "*", 

1031 ast.Pow: "**", 

1032 ast.RShift: ">>", 

1033 ast.Sub: "-", 

1034} 

1035 

1036_bool_op_map = { 

1037 ast.And: "and", 

1038 ast.Or: "or", 

1039} 

1040 

1041_compare_op_map = { 

1042 ast.Eq: "==", 

1043 ast.NotEq: "!=", 

1044 ast.Lt: "<", 

1045 ast.LtE: "<=", 

1046 ast.Gt: ">", 

1047 ast.GtE: ">=", 

1048 ast.Is: "is", 

1049 ast.IsNot: "is not", 

1050 ast.In: "in", 

1051 ast.NotIn: "not in", 

1052} 

1053 

1054# TODO: Support `ast.Await`. 

1055_precedence_map = { 

1056 # Literals and names. 

1057 ExprName: lambda _: _OperatorPrecedence.ATOMIC, 

1058 ExprConstant: lambda _: _OperatorPrecedence.ATOMIC, 

1059 ExprJoinedStr: lambda _: _OperatorPrecedence.ATOMIC, 

1060 ExprFormatted: lambda _: _OperatorPrecedence.ATOMIC, 

1061 # Container displays. 

1062 ExprList: lambda _: _OperatorPrecedence.ATOMIC, 

1063 ExprTuple: lambda _: _OperatorPrecedence.ATOMIC, 

1064 ExprSet: lambda _: _OperatorPrecedence.ATOMIC, 

1065 ExprDict: lambda _: _OperatorPrecedence.ATOMIC, 

1066 # Comprehensions are self-contained units that produce a container. 

1067 ExprListComp: lambda _: _OperatorPrecedence.ATOMIC, 

1068 ExprSetComp: lambda _: _OperatorPrecedence.ATOMIC, 

1069 ExprDictComp: lambda _: _OperatorPrecedence.ATOMIC, 

1070 ExprAttribute: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE, 

1071 ExprSubscript: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE, 

1072 ExprCall: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE, 

1073 ExprUnaryOp: lambda e: {"not": _OperatorPrecedence.NOT}.get(e.operator, _OperatorPrecedence.POS_NEG_BIT_NOT), 

1074 ExprBinOp: lambda e: { 

1075 "**": _OperatorPrecedence.EXPONENT, 

1076 "*": _OperatorPrecedence.MUL_DIV_REMAIN, 

1077 "@": _OperatorPrecedence.MUL_DIV_REMAIN, 

1078 "/": _OperatorPrecedence.MUL_DIV_REMAIN, 

1079 "//": _OperatorPrecedence.MUL_DIV_REMAIN, 

1080 "%": _OperatorPrecedence.MUL_DIV_REMAIN, 

1081 "+": _OperatorPrecedence.ADD_SUB, 

1082 "-": _OperatorPrecedence.ADD_SUB, 

1083 "<<": _OperatorPrecedence.LEFT_RIGHT_SHIFT, 

1084 ">>": _OperatorPrecedence.LEFT_RIGHT_SHIFT, 

1085 "&": _OperatorPrecedence.BIT_AND, 

1086 "^": _OperatorPrecedence.BIT_XOR, 

1087 "|": _OperatorPrecedence.BIT_OR, 

1088 }[e.operator], 

1089 ExprBoolOp: lambda e: {"and": _OperatorPrecedence.AND, "or": _OperatorPrecedence.OR}[e.operator], 

1090 ExprCompare: lambda _: _OperatorPrecedence.COMP_MEMB_ID, 

1091 ExprIfExp: lambda _: _OperatorPrecedence.IF_ELSE, 

1092 ExprNamedExpr: lambda _: _OperatorPrecedence.ASSIGN, 

1093 ExprLambda: lambda _: _OperatorPrecedence.LAMBDA, 

1094 # NOTE: Ruff categorizes as atomic, but `(a for a in b).c` implies its less than `CALL_ATTRIBUTE`. 

1095 ExprGeneratorExp: lambda _: _OperatorPrecedence.LAMBDA, 

1096 ExprVarPositional: lambda _: _OperatorPrecedence.STARRED, 

1097 ExprVarKeyword: lambda _: _OperatorPrecedence.STARRED, 

1098 ExprYield: lambda _: _OperatorPrecedence.YIELD, 

1099 ExprYieldFrom: lambda _: _OperatorPrecedence.YIELD, 

1100 # These are not standalone, they appear in specific contexts where precendence is not a concern. 

1101 # NOTE: `for ... in ... if` part, not the whole `[...]`. 

1102 ExprComprehension: lambda _: _OperatorPrecedence.NONE, 

1103 ExprExtSlice: lambda _: _OperatorPrecedence.NONE, 

1104 ExprKeyword: lambda _: _OperatorPrecedence.NONE, 

1105 ExprParameter: lambda _: _OperatorPrecedence.NONE, 

1106 ExprSlice: lambda _: _OperatorPrecedence.NONE, 

1107} 

1108 

1109 

1110def _get_precedence(expr: Expr) -> _OperatorPrecedence: 

1111 return _precedence_map.get(type(expr), lambda _: _OperatorPrecedence.NONE)(expr) 

1112 

1113 

1114def _build_attribute(node: ast.Attribute, parent: Module | Class, **kwargs: Any) -> Expr: 

1115 left = _build(node.value, parent, **kwargs) 

1116 if isinstance(left, ExprAttribute): 

1117 left.append(ExprName(node.attr)) 

1118 return left 

1119 if isinstance(left, ExprName): 

1120 return ExprAttribute([left, ExprName(node.attr, left)]) 

1121 if isinstance(left, str): 

1122 return ExprAttribute([left, ExprName(node.attr, "str")]) 

1123 return ExprAttribute([left, ExprName(node.attr)]) 

1124 

1125 

1126def _build_binop(node: ast.BinOp, parent: Module | Class, **kwargs: Any) -> Expr: 

1127 return ExprBinOp( 

1128 _build(node.left, parent, **kwargs), 

1129 _binary_op_map[type(node.op)], 

1130 _build(node.right, parent, **kwargs), 

1131 ) 

1132 

1133 

1134def _build_boolop(node: ast.BoolOp, parent: Module | Class, **kwargs: Any) -> Expr: 

1135 return ExprBoolOp( 

1136 _bool_op_map[type(node.op)], 

1137 [_build(value, parent, **kwargs) for value in node.values], 

1138 ) 

1139 

1140 

1141def _build_call(node: ast.Call, parent: Module | Class, **kwargs: Any) -> Expr: 

1142 function = _build(node.func, parent, **kwargs) 

1143 positional_args = [_build(arg, parent, **kwargs) for arg in node.args] 

1144 keyword_args = [_build(kwarg, parent, function=function, **kwargs) for kwarg in node.keywords] 

1145 return ExprCall(function, [*positional_args, *keyword_args]) 

1146 

1147 

1148def _build_compare(node: ast.Compare, parent: Module | Class, **kwargs: Any) -> Expr: 

1149 return ExprCompare( 

1150 _build(node.left, parent, **kwargs), 

1151 [_compare_op_map[type(op)] for op in node.ops], 

1152 [_build(comp, parent, **kwargs) for comp in node.comparators], 

1153 ) 

1154 

1155 

1156def _build_comprehension(node: ast.comprehension, parent: Module | Class, **kwargs: Any) -> Expr: 

1157 return ExprComprehension( 

1158 _build(node.target, parent, compr_target=True, **kwargs), 

1159 _build(node.iter, parent, **kwargs), 

1160 [_build(condition, parent, **kwargs) for condition in node.ifs], 

1161 is_async=bool(node.is_async), 

1162 ) 

1163 

1164 

1165def _build_constant( 

1166 node: ast.Constant, 

1167 parent: Module | Class, 

1168 *, 

1169 in_formatted_str: bool = False, 

1170 in_joined_str: bool = False, 

1171 parse_strings: bool = False, 

1172 literal_strings: bool = False, 

1173 **kwargs: Any, 

1174) -> str | Expr: 

1175 if isinstance(node.value, str): 

1176 if in_joined_str and not in_formatted_str: 

1177 # We're in a f-string, not in a formatted value, don't keep quotes. 

1178 return node.value 

1179 if parse_strings and not literal_strings: 

1180 # We're in a place where a string could be a type annotation 

1181 # (and not in a Literal[...] type annotation). 

1182 # We parse the string and build from the resulting nodes again. 

1183 # If we fail to parse it (syntax errors), we consider it's a literal string and log a message. 

1184 try: 

1185 parsed = compile( 

1186 node.value, 

1187 mode="eval", 

1188 filename="<string-annotation>", 

1189 flags=ast.PyCF_ONLY_AST, 

1190 optimize=1, 

1191 ) 

1192 except SyntaxError: 

1193 logger.debug( 

1194 "Tried and failed to parse %r as Python code, " 

1195 "falling back to using it as a string literal " 

1196 "(postponed annotations might help: https://peps.python.org/pep-0563/)", 

1197 node.value, 

1198 ) 

1199 else: 

1200 return _build(parsed.body, parent, **kwargs) # ty:ignore[unresolved-attribute] 

1201 return {type(...): lambda _: "..."}.get(type(node.value), repr)(node.value) 

1202 

1203 

1204def _build_dict(node: ast.Dict, parent: Module | Class, **kwargs: Any) -> Expr: 

1205 return ExprDict( 

1206 [None if key is None else _build(key, parent, **kwargs) for key in node.keys], 

1207 [_build(value, parent, **kwargs) for value in node.values], 

1208 ) 

1209 

1210 

1211def _build_dictcomp(node: ast.DictComp, parent: Module | Class, **kwargs: Any) -> Expr: 

1212 return ExprDictComp( 

1213 _build(node.key, parent, **kwargs), 

1214 _build(node.value, parent, **kwargs), 

1215 [_build(gen, parent, **kwargs) for gen in node.generators], 

1216 ) 

1217 

1218 

1219def _build_formatted( 

1220 node: ast.FormattedValue, 

1221 parent: Module | Class, 

1222 *, 

1223 in_formatted_str: bool = False, # noqa: ARG001 

1224 **kwargs: Any, 

1225) -> Expr: 

1226 return ExprFormatted(_build(node.value, parent, in_formatted_str=True, **kwargs)) 

1227 

1228 

1229def _build_generatorexp(node: ast.GeneratorExp, parent: Module | Class, **kwargs: Any) -> Expr: 

1230 return ExprGeneratorExp( 

1231 _build(node.elt, parent, **kwargs), 

1232 [_build(gen, parent, **kwargs) for gen in node.generators], 

1233 ) 

1234 

1235 

1236def _build_ifexp(node: ast.IfExp, parent: Module | Class, **kwargs: Any) -> Expr: 

1237 return ExprIfExp( 

1238 _build(node.body, parent, **kwargs), 

1239 _build(node.test, parent, **kwargs), 

1240 _build(node.orelse, parent, **kwargs), 

1241 ) 

1242 

1243 

1244def _build_joinedstr( 

1245 node: ast.JoinedStr, 

1246 parent: Module | Class, 

1247 *, 

1248 in_joined_str: bool = False, # noqa: ARG001 

1249 **kwargs: Any, 

1250) -> Expr: 

1251 return ExprJoinedStr([_build(value, parent, in_joined_str=True, **kwargs) for value in node.values]) 

1252 

1253 

1254def _build_keyword(node: ast.keyword, parent: Module | Class, function: Expr | None = None, **kwargs: Any) -> Expr: 

1255 if node.arg is None: 

1256 return ExprVarKeyword(_build(node.value, parent, **kwargs)) 

1257 return ExprKeyword(node.arg, _build(node.value, parent, **kwargs), function=function) 

1258 

1259 

1260def _build_lambda(node: ast.Lambda, parent: Module | Class, **kwargs: Any) -> Expr: 

1261 return ExprLambda( 

1262 parameters=[ 

1263 ExprParameter( 

1264 name=name, 

1265 kind=kind, 

1266 annotation=None, 

1267 default=default 

1268 if isinstance(default, str) 

1269 else safe_get_expression(default, parent=parent, parse_strings=False), 

1270 ) 

1271 for name, _, kind, default in get_parameters(node.args) 

1272 ], 

1273 body=_build(node.body, parent, **kwargs), 

1274 ) 

1275 

1276 

1277def _build_list(node: ast.List, parent: Module | Class, **kwargs: Any) -> Expr: 

1278 return ExprList([_build(el, parent, **kwargs) for el in node.elts]) 

1279 

1280 

1281def _build_listcomp(node: ast.ListComp, parent: Module | Class, **kwargs: Any) -> Expr: 

1282 return ExprListComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators]) 

1283 

1284 

1285def _build_name(node: ast.Name, parent: Module | Class, member: str | None = None, **kwargs: Any) -> Expr: # noqa: ARG001 

1286 return ExprName(node.id, parent, member) 

1287 

1288 

1289def _build_named_expr(node: ast.NamedExpr, parent: Module | Class, **kwargs: Any) -> Expr: 

1290 return ExprNamedExpr(_build(node.target, parent, **kwargs), _build(node.value, parent, **kwargs)) 

1291 

1292 

1293def _build_set(node: ast.Set, parent: Module | Class, **kwargs: Any) -> Expr: 

1294 return ExprSet([_build(el, parent, **kwargs) for el in node.elts]) 

1295 

1296 

1297def _build_setcomp(node: ast.SetComp, parent: Module | Class, **kwargs: Any) -> Expr: 

1298 return ExprSetComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators]) 

1299 

1300 

1301def _build_slice(node: ast.Slice, parent: Module | Class, **kwargs: Any) -> Expr: 

1302 return ExprSlice( 

1303 None if node.lower is None else _build(node.lower, parent, **kwargs), 

1304 None if node.upper is None else _build(node.upper, parent, **kwargs), 

1305 None if node.step is None else _build(node.step, parent, **kwargs), 

1306 ) 

1307 

1308 

1309def _build_starred(node: ast.Starred, parent: Module | Class, **kwargs: Any) -> Expr: 

1310 return ExprVarPositional(_build(node.value, parent, **kwargs)) 

1311 

1312 

1313def _build_subscript( 

1314 node: ast.Subscript, 

1315 parent: Module | Class, 

1316 *, 

1317 parse_strings: bool = False, 

1318 literal_strings: bool = False, 

1319 subscript_slice: bool = False, # noqa: ARG001 

1320 **kwargs: Any, 

1321) -> Expr: 

1322 left = _build(node.value, parent, **kwargs) 

1323 if parse_strings: 

1324 if isinstance(left, (ExprAttribute, ExprName)) and left.canonical_path in { 

1325 "typing.Literal", 

1326 "typing_extensions.Literal", 

1327 }: 

1328 literal_strings = True 

1329 slice_expr = _build( 

1330 node.slice, 

1331 parent, 

1332 parse_strings=True, 

1333 literal_strings=literal_strings, 

1334 subscript_slice=True, 

1335 **kwargs, 

1336 ) 

1337 else: 

1338 slice_expr = _build(node.slice, parent, subscript_slice=True, **kwargs) 

1339 return ExprSubscript(left, slice_expr) 

1340 

1341 

1342def _build_tuple( 

1343 node: ast.Tuple, 

1344 parent: Module | Class, 

1345 *, 

1346 subscript_slice: bool = False, 

1347 compr_target: bool = False, 

1348 **kwargs: Any, 

1349) -> Expr: 

1350 return ExprTuple([_build(el, parent, **kwargs) for el in node.elts], implicit=subscript_slice or compr_target) 

1351 

1352 

1353def _build_unaryop(node: ast.UnaryOp, parent: Module | Class, **kwargs: Any) -> Expr: 

1354 return ExprUnaryOp(_unary_op_map[type(node.op)], _build(node.operand, parent, **kwargs)) 

1355 

1356 

1357def _build_yield(node: ast.Yield, parent: Module | Class, **kwargs: Any) -> Expr: 

1358 return ExprYield(None if node.value is None else _build(node.value, parent, **kwargs)) 

1359 

1360 

1361def _build_yield_from(node: ast.YieldFrom, parent: Module | Class, **kwargs: Any) -> Expr: 

1362 return ExprYieldFrom(_build(node.value, parent, **kwargs)) 

1363 

1364 

1365class _BuildCallable(Protocol): 

1366 def __call__(self, node: Any, parent: Module | Class, **kwargs: Any) -> Expr: ... 1366 ↛ exitline 1366 didn't return from function '__call__' because

1367 

1368 

1369_node_map: dict[type, _BuildCallable] = { 

1370 ast.Attribute: _build_attribute, 

1371 ast.BinOp: _build_binop, 

1372 ast.BoolOp: _build_boolop, 

1373 ast.Call: _build_call, 

1374 ast.Compare: _build_compare, 

1375 ast.comprehension: _build_comprehension, 

1376 ast.Constant: _build_constant, 

1377 ast.Dict: _build_dict, 

1378 ast.DictComp: _build_dictcomp, 

1379 ast.FormattedValue: _build_formatted, 

1380 ast.GeneratorExp: _build_generatorexp, 

1381 ast.IfExp: _build_ifexp, 

1382 ast.JoinedStr: _build_joinedstr, 

1383 ast.keyword: _build_keyword, 

1384 ast.Lambda: _build_lambda, 

1385 ast.List: _build_list, 

1386 ast.ListComp: _build_listcomp, 

1387 ast.Name: _build_name, 

1388 ast.NamedExpr: _build_named_expr, 

1389 ast.Set: _build_set, 

1390 ast.SetComp: _build_setcomp, 

1391 ast.Slice: _build_slice, 

1392 ast.Starred: _build_starred, 

1393 ast.Subscript: _build_subscript, 

1394 ast.Tuple: _build_tuple, 

1395 ast.UnaryOp: _build_unaryop, 

1396 ast.Yield: _build_yield, 

1397 ast.YieldFrom: _build_yield_from, 

1398} # ty:ignore[invalid-assignment] 

1399 

1400if sys.version_info >= (3, 14): 

1401 

1402 def _build_interpolation(node: ast.Interpolation, parent: Module | Class, **kwargs: Any) -> Expr: 

1403 return ExprInterpolation(_build(node.value, parent, **kwargs)) 

1404 

1405 def _build_templatestr( 

1406 node: ast.TemplateStr, 

1407 parent: Module | Class, 

1408 **kwargs: Any, 

1409 ) -> Expr: 

1410 return ExprTemplateStr([_build(value, parent, in_joined_str=True, **kwargs) for value in node.values]) 

1411 

1412 _node_map.update( 

1413 { 

1414 ast.Interpolation: _build_interpolation, 

1415 ast.TemplateStr: _build_templatestr, 

1416 }, 

1417 ) 

1418 

1419 

1420def _build(node: ast.AST, parent: Module | Class, /, **kwargs: Any) -> Expr: 

1421 return _node_map[type(node)](node, parent, **kwargs) 

1422 

1423 

1424def get_expression( 

1425 node: ast.AST | None, 

1426 parent: Module | Class, 

1427 *, 

1428 member: str | None = None, 

1429 parse_strings: bool | None = None, 

1430) -> Expr | None: 

1431 """Build an expression from an AST. 

1432 

1433 Parameters: 

1434 node: The annotation node. 

1435 parent: The parent used to resolve the name. 

1436 member: The member name (for resolution in its scope). 

1437 parse_strings: Whether to try and parse strings as type annotations. 

1438 

1439 Returns: 

1440 A string or resovable name or expression. 

1441 """ 

1442 if node is None: 

1443 return None 

1444 if parse_strings is None: 

1445 try: 

1446 module = parent.module 

1447 except ValueError: 

1448 parse_strings = False 

1449 else: 

1450 parse_strings = not module.imports_future_annotations 

1451 return _build(node, parent, member=member, parse_strings=parse_strings) 

1452 

1453 

1454def safe_get_expression( 

1455 node: ast.AST | None, 

1456 parent: Module | Class, 

1457 *, 

1458 member: str | None = None, 

1459 parse_strings: bool | None = None, 

1460 log_level: LogLevel | None = LogLevel.error, 

1461 msg_format: str = "{path}:{lineno}: Failed to get expression from {node_class}: {error}", 

1462) -> Expr | None: 

1463 """Safely (no exception) build a resolvable annotation. 

1464 

1465 Parameters: 

1466 node: The annotation node. 

1467 parent: The parent used to resolve the name. 

1468 member: The member name (for resolution in its scope). 

1469 parse_strings: Whether to try and parse strings as type annotations. 

1470 log_level: Log level to use to log a message. None to disable logging. 

1471 msg_format: A format string for the log message. Available placeholders: 

1472 path, lineno, node, error. 

1473 

1474 Returns: 

1475 A string or resovable name or expression. 

1476 """ 

1477 try: 

1478 return get_expression(node, parent, member=member, parse_strings=parse_strings) 

1479 except Exception as error: # noqa: BLE001 

1480 if log_level is None: 1480 ↛ 1481line 1480 didn't jump to line 1481 because the condition on line 1480 was never true

1481 return None 

1482 node_class = node.__class__.__name__ 

1483 try: 

1484 path: Path | str = parent.relative_filepath 

1485 except ValueError: 

1486 path = "<in-memory>" 

1487 lineno = node.lineno # ty:ignore[unresolved-attribute] 

1488 error_str = f"{error.__class__.__name__}: {error}" 

1489 message = msg_format.format(path=path, lineno=lineno, node_class=node_class, error=error_str) 

1490 getattr(logger, log_level.value)(message) 

1491 return None 

1492 

1493 

1494_msg_format = "{path}:{lineno}: Failed to get %s expression from {node_class}: {error}" 

1495get_annotation = partial(get_expression, parse_strings=None) 

1496safe_get_annotation = partial( 

1497 safe_get_expression, 

1498 parse_strings=None, 

1499 msg_format=_msg_format % "annotation", 

1500) 

1501get_base_class = partial(get_expression, parse_strings=False) 

1502safe_get_base_class = partial( 

1503 safe_get_expression, 

1504 parse_strings=False, 

1505 msg_format=_msg_format % "base class", 

1506) 

1507get_class_keyword = partial(get_expression, parse_strings=False) 

1508safe_get_class_keyword = partial( 

1509 safe_get_expression, 

1510 parse_strings=False, 

1511 msg_format=_msg_format % "class keyword", 

1512) 

1513get_condition = partial(get_expression, parse_strings=False) 

1514safe_get_condition = partial( 

1515 safe_get_expression, 

1516 parse_strings=False, 

1517 msg_format=_msg_format % "condition", 

1518)