Coverage for src/griffe/_internal/expressions.py: 92.26%

728 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-11 13:44 +0200

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, Callable 

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# YORE: EOL 3.9: Remove block. 

161_dataclass_opts: dict[str, bool] = {} 

162if sys.version_info >= (3, 10): 

163 _dataclass_opts["slots"] = True 

164 

165 

166@dataclass 

167class Expr: 

168 """Base class for expressions.""" 

169 

170 def __str__(self) -> str: 

171 return "".join(elem if isinstance(elem, str) else elem.name for elem in self.iterate(flat=True)) # type: ignore[attr-defined] 

172 

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

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

175 yield from self.iterate(flat=False) 

176 

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

178 """Iterate on the expression elements. 

179 

180 Parameters: 

181 flat: Expressions are trees. 

182 

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

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

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

186 without them getting rendered as strings. 

187 

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

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

190 

191 Yields: 

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

193 """ 

194 yield from () 

195 

196 def modernize(self) -> Expr: 

197 """Modernize the expression. 

198 

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

200 

201 Returns: 

202 A modernized expression. 

203 """ 

204 return self 

205 

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

207 """Return the expression as a dictionary. 

208 

209 Parameters: 

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

211 

212 

213 Returns: 

214 A dictionary. 

215 """ 

216 return _expr_as_dict(self, **kwargs) 

217 

218 @property 

219 def classname(self) -> str: 

220 """The expression class name.""" 

221 return self.__class__.__name__ 

222 

223 @property 

224 def path(self) -> str: 

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

226 return str(self) 

227 

228 @property 

229 def canonical_path(self) -> str: 

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

231 return str(self) 

232 

233 @property 

234 def canonical_name(self) -> str: 

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

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

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

238 

239 @property 

240 def is_classvar(self) -> bool: 

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

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

243 

244 @property 

245 def is_tuple(self) -> bool: 

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

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

248 

249 @property 

250 def is_iterator(self) -> bool: 

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

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

253 

254 @property 

255 def is_generator(self) -> bool: 

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

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

258 

259 

260# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

261@dataclass(eq=True, **_dataclass_opts) 

262class ExprAttribute(Expr): 

263 """Attributes like `a.b`.""" 

264 

265 values: list[str | Expr] 

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

267 

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

269 precedence = _get_precedence(self) 

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

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

272 yield "." 

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

274 

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

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

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

278 return self 

279 

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

281 """Append a name to this attribute. 

282 

283 Parameters: 

284 value: The expression name to append. 

285 """ 

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

287 value.parent = self.last 

288 self.values.append(value) 

289 

290 @property 

291 def last(self) -> ExprName: 

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

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

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

295 return self.values[-1] # type: ignore[return-value] 

296 

297 @property 

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

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

300 return self.values[0] 

301 

302 @property 

303 def path(self) -> str: 

304 """The path of this attribute.""" 

305 return self.last.path 

306 

307 @property 

308 def canonical_path(self) -> str: 

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

310 return self.last.canonical_path 

311 

312 

313# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

314@dataclass(eq=True, **_dataclass_opts) 

315class ExprBinOp(Expr): 

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

317 

318 left: str | Expr 

319 """Left part.""" 

320 operator: str 

321 """Binary operator.""" 

322 right: str | Expr 

323 """Right part.""" 

324 

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

326 precedence = _get_precedence(self) 

327 right_precedence = precedence 

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

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

330 right_precedence = _OperatorPrecedence(precedence - 1) 

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

332 yield f" {self.operator} " 

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

334 

335 

336# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

337@dataclass(eq=True, **_dataclass_opts) 

338class ExprBoolOp(Expr): 

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

340 

341 operator: str 

342 """Boolean operator.""" 

343 values: Sequence[str | Expr] 

344 """Operands.""" 

345 

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

347 precedence = _get_precedence(self) 

348 it = iter(self.values) 

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

350 for value in it: 

351 yield f" {self.operator} " 

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

353 

354 

355# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

356@dataclass(eq=True, **_dataclass_opts) 

357class ExprCall(Expr): 

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

359 

360 function: Expr 

361 """Function called.""" 

362 arguments: Sequence[str | Expr] 

363 """Passed arguments.""" 

364 

365 @property 

366 def canonical_path(self) -> str: 

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

368 return self.function.canonical_path 

369 

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

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

372 yield "(" 

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

374 yield ")" 

375 

376 

377# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

378@dataclass(eq=True, **_dataclass_opts) 

379class ExprCompare(Expr): 

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

381 

382 left: str | Expr 

383 """Left part.""" 

384 operators: Sequence[str] 

385 """Comparison operators.""" 

386 comparators: Sequence[str | Expr] 

387 """Things compared.""" 

388 

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

390 precedence = _get_precedence(self) 

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

392 for op, comp in zip(self.operators, self.comparators): 

393 yield f" {op} " 

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

395 

396 

397# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

398@dataclass(eq=True, **_dataclass_opts) 

399class ExprComprehension(Expr): 

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

401 

402 target: str | Expr 

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

404 iterable: str | Expr 

405 """Value iterated on.""" 

406 conditions: Sequence[str | Expr] 

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

408 is_async: bool = False 

409 """Async comprehension or not.""" 

410 

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

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

413 yield "async " 

414 yield "for " 

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

416 yield " in " 

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

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

419 yield " if " 

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

421 

422 

423# TODO: `ExprConstant` is never instantiated, 

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

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

426# if used anyway. 

427# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

428@dataclass(eq=True, **_dataclass_opts) 

429class ExprConstant(Expr): 

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

431 

432 value: str 

433 """Constant value.""" 

434 

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

436 yield self.value 

437 

438 

439# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

440@dataclass(eq=True, **_dataclass_opts) 

441class ExprDict(Expr): 

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

443 

444 keys: Sequence[str | Expr | None] 

445 """Dict keys.""" 

446 values: Sequence[str | Expr] 

447 """Dict values.""" 

448 

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

450 yield "{" 

451 yield from _join( 

452 (("None" if key is None else key, ": ", value) for key, value in zip(self.keys, self.values)), 

453 ", ", 

454 flat=flat, 

455 ) 

456 yield "}" 

457 

458 

459# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

460@dataclass(eq=True, **_dataclass_opts) 

461class ExprDictComp(Expr): 

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

463 

464 key: str | Expr 

465 """Target key.""" 

466 value: str | Expr 

467 """Target value.""" 

468 generators: Sequence[Expr] 

469 """Generators iterated on.""" 

470 

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

472 yield "{" 

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

474 yield ": " 

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

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

477 yield "}" 

478 

479 

480# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

481@dataclass(eq=True, **_dataclass_opts) 

482class ExprExtSlice(Expr): 

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

484 

485 dims: Sequence[str | Expr] 

486 """Dims.""" 

487 

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

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

490 

491 

492# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

493@dataclass(eq=True, **_dataclass_opts) 

494class ExprFormatted(Expr): 

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

496 

497 value: str | Expr 

498 """Formatted value.""" 

499 

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

501 yield "{" 

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

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

504 yield "}" 

505 

506 

507# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

508@dataclass(eq=True, **_dataclass_opts) 

509class ExprGeneratorExp(Expr): 

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

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# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

524@dataclass(eq=True, **_dataclass_opts) 

525class ExprIfExp(Expr): 

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

527 

528 body: str | Expr 

529 """Value if test.""" 

530 test: str | Expr 

531 """Condition.""" 

532 orelse: str | Expr 

533 """Other expression.""" 

534 

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

536 precedence = _get_precedence(self) 

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

538 yield " if " 

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

540 # a parenthesis: force it. 

541 test_outer_precedence = _OperatorPrecedence(precedence + 1) 

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

543 yield " else " 

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

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

546 # flattened version without parentheses. 

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

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

549 else: 

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

551 

552 

553# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

554@dataclass(eq=True, **_dataclass_opts) 

555class ExprJoinedStr(Expr): 

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

557 

558 values: Sequence[str | Expr] 

559 """Joined values.""" 

560 

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

562 yield "f'" 

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

564 yield "'" 

565 

566 

567# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

568@dataclass(eq=True, **_dataclass_opts) 

569class ExprKeyword(Expr): 

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

571 

572 name: str 

573 """Name.""" 

574 value: str | Expr 

575 """Value.""" 

576 

577 # Griffe is designed around accessing Python objects 

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

579 # Function parameters were not taken into account 

580 # because they are not accessible the same way. 

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

582 # documentation of function parameters in downstream 

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

584 # for keyword expressions, where they get a meaningful 

585 # canonical path (contrary to most other expressions that 

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

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

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

589 # of the call expression in the keyword one, 

590 # hence the following field. 

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

592 function: Expr | None = None 

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

594 

595 @property 

596 def canonical_path(self) -> str: 

597 """Path of the expressed keyword.""" 

598 if self.function: 

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

600 return super(ExprKeyword, self).canonical_path # noqa: UP008 

601 

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

603 yield self.name 

604 yield "=" 

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

606 

607 

608# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

609@dataclass(eq=True, **_dataclass_opts) 

610class ExprVarPositional(Expr): 

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

612 

613 value: Expr 

614 """Starred value.""" 

615 

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

617 yield "*" 

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

619 

620 

621# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

622@dataclass(eq=True, **_dataclass_opts) 

623class ExprVarKeyword(Expr): 

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

625 

626 value: Expr 

627 """Double-starred value.""" 

628 

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

630 yield "**" 

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

632 

633 

634# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

635@dataclass(eq=True, **_dataclass_opts) 

636class ExprLambda(Expr): 

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

638 

639 parameters: Sequence[ExprParameter] 

640 """Lambda's parameters.""" 

641 body: str | Expr 

642 """Lambda's body.""" 

643 

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

645 pos_only = False 

646 pos_or_kw = False 

647 kw_only = False 

648 length = len(self.parameters) 

649 yield "lambda" 

650 if length: 

651 yield " " 

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

653 if parameter.kind is ParameterKind.positional_only: 

654 pos_only = True 

655 elif parameter.kind is ParameterKind.var_positional: 

656 yield "*" 

657 elif parameter.kind is ParameterKind.var_keyword: 

658 yield "**" 

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

660 pos_or_kw = True 

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

662 kw_only = True 

663 yield "*, " 

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

665 pos_only = False 

666 yield "/, " 

667 yield parameter.name 

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

669 yield "=" 

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

671 if index < length: 

672 yield ", " 

673 yield ": " 

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

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

676 

677 

678# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

679@dataclass(eq=True, **_dataclass_opts) 

680class ExprList(Expr): 

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

682 

683 elements: Sequence[Expr] 

684 """List elements.""" 

685 

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

687 yield "[" 

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

689 yield "]" 

690 

691 

692# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

693@dataclass(eq=True, **_dataclass_opts) 

694class ExprListComp(Expr): 

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

696 

697 element: str | Expr 

698 """Target value.""" 

699 generators: Sequence[Expr] 

700 """Generators iterated on.""" 

701 

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

703 yield "[" 

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

705 yield " " 

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

707 yield "]" 

708 

709 

710# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

711@dataclass(eq=False, **_dataclass_opts) 

712class ExprName(Expr): # noqa: PLW1641 

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

714 

715 name: str 

716 """Actual name.""" 

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

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

719 member: str | None = None 

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

721 

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

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

724 if isinstance(other, ExprName): 

725 return self.name == other.name 

726 return NotImplemented 

727 

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

729 yield self 

730 

731 def modernize(self) -> ExprName: 

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

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

734 return self 

735 

736 @property 

737 def path(self) -> str: 

738 """The full, resolved name. 

739 

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

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

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

743 """ 

744 if isinstance(self.parent, ExprName): 

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

746 return self.name 

747 

748 @property 

749 def canonical_path(self) -> str: 

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

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

752 return self.name 

753 if isinstance(self.parent, ExprName): 

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

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

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

757 parent = self.parent.members.get(self.member, self.parent) # type: ignore[arg-type] 

758 try: 

759 return parent.resolve(self.name) 

760 except NameResolutionError: 

761 return self.name 

762 

763 @property 

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

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

766 try: 

767 return self.parent.modules_collection[self.parent.resolve(self.name)] # type: ignore[union-attr] 

768 except Exception: # noqa: BLE001 

769 return self.parent.resolved[self.name] # type: ignore[union-attr,index] 

770 

771 @property 

772 def is_enum_class(self) -> bool: 

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

774 try: 

775 bases = self.resolved.bases # type: ignore[union-attr] 

776 except Exception: # noqa: BLE001 

777 return False 

778 

779 # TODO: Support inheritance? 

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

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

782 

783 @property 

784 def is_enum_instance(self) -> bool: 

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

786 try: 

787 return self.parent.is_enum_class # type: ignore[union-attr] 

788 except Exception: # noqa: BLE001 

789 return False 

790 

791 @property 

792 def is_enum_value(self) -> bool: 

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

794 try: 

795 return self.name == "value" and self.parent.is_enum_instance # type: ignore[union-attr] 

796 except Exception: # noqa: BLE001 

797 return False 

798 

799 @property 

800 def is_type_parameter(self) -> bool: 

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

802 return "[" in self.canonical_path 

803 

804 

805# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

806@dataclass(eq=True, **_dataclass_opts) 

807class ExprNamedExpr(Expr): 

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

809 

810 target: Expr 

811 """Target name.""" 

812 value: str | Expr 

813 """Value.""" 

814 

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

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

817 yield " := " 

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

819 

820 

821# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

822@dataclass(eq=True, **_dataclass_opts) 

823class ExprParameter(Expr): 

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

825 

826 name: str 

827 """Parameter name.""" 

828 kind: ParameterKind = ParameterKind.positional_or_keyword 

829 """Parameter kind.""" 

830 annotation: Expr | None = None 

831 """Parameter type.""" 

832 default: str | Expr | None = None 

833 """Parameter default.""" 

834 

835 

836# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

837@dataclass(eq=True, **_dataclass_opts) 

838class ExprSet(Expr): 

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

840 

841 elements: Sequence[str | Expr] 

842 """Set elements.""" 

843 

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

845 yield "{" 

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

847 yield "}" 

848 

849 

850# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

851@dataclass(eq=True, **_dataclass_opts) 

852class ExprSetComp(Expr): 

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

854 

855 element: str | Expr 

856 """Target value.""" 

857 generators: Sequence[Expr] 

858 """Generators iterated on.""" 

859 

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

861 yield "{" 

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

863 yield " " 

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

865 yield "}" 

866 

867 

868# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

869@dataclass(eq=True, **_dataclass_opts) 

870class ExprSlice(Expr): 

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

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# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

892@dataclass(eq=True, **_dataclass_opts) 

893class ExprSubscript(Expr): 

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

895 

896 left: str | Expr 

897 """Left part.""" 

898 slice: str | Expr 

899 """Slice part.""" 

900 

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

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

903 yield "[" 

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

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

906 yield "]" 

907 

908 @staticmethod 

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

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

911 left, right = elements 

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

913 left = left.modernize() 

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

915 right = right.modernize() 

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

917 

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

919 right = elements[-1] 

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

921 right = right.modernize() 

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

923 

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

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

926 return self._to_binop(self.slice.elements, op="|") # type: ignore[union-attr] 

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

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

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

930 return ExprSubscript( 

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

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

933 ) 

934 

935 @property 

936 def path(self) -> str: 

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

938 if isinstance(self.left, str): 

939 return self.left 

940 return self.left.path 

941 

942 @property 

943 def canonical_path(self) -> str: 

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

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

946 return self.left 

947 return self.left.canonical_path 

948 

949 

950# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

951@dataclass(eq=True, **_dataclass_opts) 

952class ExprTuple(Expr): 

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

954 

955 elements: Sequence[str | Expr] 

956 """Tuple elements.""" 

957 implicit: bool = False 

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

959 

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

961 if not self.implicit: 

962 yield "(" 

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

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

965 yield "," 

966 if not self.implicit: 

967 yield ")" 

968 

969 def modernize(self) -> ExprTuple: 

970 return ExprTuple( 

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

972 implicit=self.implicit, 

973 ) 

974 

975 

976# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

977@dataclass(eq=True, **_dataclass_opts) 

978class ExprUnaryOp(Expr): 

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

980 

981 operator: str 

982 """Unary operator.""" 

983 value: str | Expr 

984 """Value.""" 

985 

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

987 yield self.operator 

988 if self.operator == "not": 

989 yield " " 

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

991 

992 

993# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

994@dataclass(eq=True, **_dataclass_opts) 

995class ExprYield(Expr): 

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

997 

998 value: str | Expr | None = None 

999 """Yielded value.""" 

1000 

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

1002 yield "yield" 

1003 if self.value is not None: 

1004 yield " " 

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

1006 

1007 

1008# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. 

1009@dataclass(eq=True, **_dataclass_opts) 

1010class ExprYieldFrom(Expr): 

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

1012 

1013 value: str | Expr 

1014 """Yielded-from value.""" 

1015 

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

1017 yield "yield from " 

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

1019 

1020 

1021_unary_op_map = { 

1022 ast.Invert: "~", 

1023 ast.Not: "not", 

1024 ast.UAdd: "+", 

1025 ast.USub: "-", 

1026} 

1027 

1028_binary_op_map = { 

1029 ast.Add: "+", 

1030 ast.BitAnd: "&", 

1031 ast.BitOr: "|", 

1032 ast.BitXor: "^", 

1033 ast.Div: "/", 

1034 ast.FloorDiv: "//", 

1035 ast.LShift: "<<", 

1036 ast.MatMult: "@", 

1037 ast.Mod: "%", 

1038 ast.Mult: "*", 

1039 ast.Pow: "**", 

1040 ast.RShift: ">>", 

1041 ast.Sub: "-", 

1042} 

1043 

1044_bool_op_map = { 

1045 ast.And: "and", 

1046 ast.Or: "or", 

1047} 

1048 

1049_compare_op_map = { 

1050 ast.Eq: "==", 

1051 ast.NotEq: "!=", 

1052 ast.Lt: "<", 

1053 ast.LtE: "<=", 

1054 ast.Gt: ">", 

1055 ast.GtE: ">=", 

1056 ast.Is: "is", 

1057 ast.IsNot: "is not", 

1058 ast.In: "in", 

1059 ast.NotIn: "not in", 

1060} 

1061 

1062# TODO: Support `ast.Await`. 

1063_precedence_map = { 

1064 # Literals and names. 

1065 ExprName: lambda _: _OperatorPrecedence.ATOMIC, 

1066 ExprConstant: lambda _: _OperatorPrecedence.ATOMIC, 

1067 ExprJoinedStr: lambda _: _OperatorPrecedence.ATOMIC, 

1068 ExprFormatted: lambda _: _OperatorPrecedence.ATOMIC, 

1069 # Container displays. 

1070 ExprList: lambda _: _OperatorPrecedence.ATOMIC, 

1071 ExprTuple: lambda _: _OperatorPrecedence.ATOMIC, 

1072 ExprSet: lambda _: _OperatorPrecedence.ATOMIC, 

1073 ExprDict: lambda _: _OperatorPrecedence.ATOMIC, 

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

1075 ExprListComp: lambda _: _OperatorPrecedence.ATOMIC, 

1076 ExprSetComp: lambda _: _OperatorPrecedence.ATOMIC, 

1077 ExprDictComp: lambda _: _OperatorPrecedence.ATOMIC, 

1078 ExprAttribute: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE, 

1079 ExprSubscript: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE, 

1080 ExprCall: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE, 

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

1082 ExprBinOp: lambda e: { 

1083 "**": _OperatorPrecedence.EXPONENT, 

1084 "*": _OperatorPrecedence.MUL_DIV_REMAIN, 

1085 "@": _OperatorPrecedence.MUL_DIV_REMAIN, 

1086 "/": _OperatorPrecedence.MUL_DIV_REMAIN, 

1087 "//": _OperatorPrecedence.MUL_DIV_REMAIN, 

1088 "%": _OperatorPrecedence.MUL_DIV_REMAIN, 

1089 "+": _OperatorPrecedence.ADD_SUB, 

1090 "-": _OperatorPrecedence.ADD_SUB, 

1091 "<<": _OperatorPrecedence.LEFT_RIGHT_SHIFT, 

1092 ">>": _OperatorPrecedence.LEFT_RIGHT_SHIFT, 

1093 "&": _OperatorPrecedence.BIT_AND, 

1094 "^": _OperatorPrecedence.BIT_XOR, 

1095 "|": _OperatorPrecedence.BIT_OR, 

1096 }[e.operator], 

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

1098 ExprCompare: lambda _: _OperatorPrecedence.COMP_MEMB_ID, 

1099 ExprIfExp: lambda _: _OperatorPrecedence.IF_ELSE, 

1100 ExprNamedExpr: lambda _: _OperatorPrecedence.ASSIGN, 

1101 ExprLambda: lambda _: _OperatorPrecedence.LAMBDA, 

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

1103 ExprGeneratorExp: lambda _: _OperatorPrecedence.LAMBDA, 

1104 ExprVarPositional: lambda _: _OperatorPrecedence.STARRED, 

1105 ExprVarKeyword: lambda _: _OperatorPrecedence.STARRED, 

1106 ExprYield: lambda _: _OperatorPrecedence.YIELD, 

1107 ExprYieldFrom: lambda _: _OperatorPrecedence.YIELD, 

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

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

1110 ExprComprehension: lambda _: _OperatorPrecedence.NONE, 

1111 ExprExtSlice: lambda _: _OperatorPrecedence.NONE, 

1112 ExprKeyword: lambda _: _OperatorPrecedence.NONE, 

1113 ExprParameter: lambda _: _OperatorPrecedence.NONE, 

1114 ExprSlice: lambda _: _OperatorPrecedence.NONE, 

1115} 

1116 

1117 

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

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

1120 

1121 

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

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

1124 if isinstance(left, ExprAttribute): 

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

1126 return left 

1127 if isinstance(left, ExprName): 

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

1129 if isinstance(left, str): 

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

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

1132 

1133 

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

1135 return ExprBinOp( 

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

1137 _binary_op_map[type(node.op)], 

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

1139 ) 

1140 

1141 

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

1143 return ExprBoolOp( 

1144 _bool_op_map[type(node.op)], 

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

1146 ) 

1147 

1148 

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

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

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

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

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

1154 

1155 

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

1157 return ExprCompare( 

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

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

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

1161 ) 

1162 

1163 

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

1165 return ExprComprehension( 

1166 _build(node.target, parent, **kwargs), 

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

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

1169 is_async=bool(node.is_async), 

1170 ) 

1171 

1172 

1173def _build_constant( 

1174 node: ast.Constant, 

1175 parent: Module | Class, 

1176 *, 

1177 in_formatted_str: bool = False, 

1178 in_joined_str: bool = False, 

1179 parse_strings: bool = False, 

1180 literal_strings: bool = False, 

1181 **kwargs: Any, 

1182) -> str | Expr: 

1183 if isinstance(node.value, str): 

1184 if in_joined_str and not in_formatted_str: 

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

1186 return node.value 

1187 if parse_strings and not literal_strings: 

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

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

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

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

1192 try: 

1193 parsed = compile( 

1194 node.value, 

1195 mode="eval", 

1196 filename="<string-annotation>", 

1197 flags=ast.PyCF_ONLY_AST, 

1198 optimize=1, 

1199 ) 

1200 except SyntaxError: 

1201 logger.debug( 

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

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

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

1205 node.value, 

1206 ) 

1207 else: 

1208 return _build(parsed.body, parent, **kwargs) # type: ignore[attr-defined] 

1209 return {type(...): lambda _: "..."}.get(type(node.value), repr)(node.value) # type: ignore[arg-type] 

1210 

1211 

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

1213 return ExprDict( 

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

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

1216 ) 

1217 

1218 

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

1220 return ExprDictComp( 

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

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

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

1224 ) 

1225 

1226 

1227def _build_formatted( 

1228 node: ast.FormattedValue, 

1229 parent: Module | Class, 

1230 *, 

1231 in_formatted_str: bool = False, # noqa: ARG001 

1232 **kwargs: Any, 

1233) -> Expr: 

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

1235 

1236 

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

1238 return ExprGeneratorExp( 

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

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

1241 ) 

1242 

1243 

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

1245 return ExprIfExp( 

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

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

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

1249 ) 

1250 

1251 

1252def _build_joinedstr( 

1253 node: ast.JoinedStr, 

1254 parent: Module | Class, 

1255 *, 

1256 in_joined_str: bool = False, # noqa: ARG001 

1257 **kwargs: Any, 

1258) -> Expr: 

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

1260 

1261 

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

1263 if node.arg is None: 

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

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

1266 

1267 

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

1269 return ExprLambda( 

1270 parameters=[ 

1271 ExprParameter( 

1272 name=name, 

1273 kind=kind, 

1274 annotation=None, 

1275 default=default 

1276 if isinstance(default, str) 

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

1278 ) 

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

1280 ], 

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

1282 ) 

1283 

1284 

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

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

1287 

1288 

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

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

1291 

1292 

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

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

1295 

1296 

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

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

1299 

1300 

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

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

1303 

1304 

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

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

1307 

1308 

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

1310 return ExprSlice( 

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

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

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

1314 ) 

1315 

1316 

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

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

1319 

1320 

1321def _build_subscript( 

1322 node: ast.Subscript, 

1323 parent: Module | Class, 

1324 *, 

1325 parse_strings: bool = False, 

1326 literal_strings: bool = False, 

1327 in_subscript: bool = False, # noqa: ARG001 

1328 **kwargs: Any, 

1329) -> Expr: 

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

1331 if parse_strings: 

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

1333 "typing.Literal", 

1334 "typing_extensions.Literal", 

1335 }: 

1336 literal_strings = True 

1337 slice_expr = _build( 

1338 node.slice, 

1339 parent, 

1340 parse_strings=True, 

1341 literal_strings=literal_strings, 

1342 in_subscript=True, 

1343 **kwargs, 

1344 ) 

1345 else: 

1346 slice_expr = _build(node.slice, parent, in_subscript=True, **kwargs) 

1347 return ExprSubscript(left, slice_expr) 

1348 

1349 

1350def _build_tuple( 

1351 node: ast.Tuple, 

1352 parent: Module | Class, 

1353 *, 

1354 in_subscript: bool = False, 

1355 **kwargs: Any, 

1356) -> Expr: 

1357 return ExprTuple([_build(el, parent, **kwargs) for el in node.elts], implicit=in_subscript) 

1358 

1359 

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

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

1362 

1363 

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

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

1366 

1367 

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

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

1370 

1371 

1372_node_map: dict[type, Callable[[Any, Module | Class], Expr]] = { 

1373 ast.Attribute: _build_attribute, 

1374 ast.BinOp: _build_binop, 

1375 ast.BoolOp: _build_boolop, 

1376 ast.Call: _build_call, 

1377 ast.Compare: _build_compare, 

1378 ast.comprehension: _build_comprehension, 

1379 ast.Constant: _build_constant, # type: ignore[dict-item] 

1380 ast.Dict: _build_dict, 

1381 ast.DictComp: _build_dictcomp, 

1382 ast.FormattedValue: _build_formatted, 

1383 ast.GeneratorExp: _build_generatorexp, 

1384 ast.IfExp: _build_ifexp, 

1385 ast.JoinedStr: _build_joinedstr, 

1386 ast.keyword: _build_keyword, 

1387 ast.Lambda: _build_lambda, 

1388 ast.List: _build_list, 

1389 ast.ListComp: _build_listcomp, 

1390 ast.Name: _build_name, 

1391 ast.NamedExpr: _build_named_expr, 

1392 ast.Set: _build_set, 

1393 ast.SetComp: _build_setcomp, 

1394 ast.Slice: _build_slice, 

1395 ast.Starred: _build_starred, 

1396 ast.Subscript: _build_subscript, 

1397 ast.Tuple: _build_tuple, 

1398 ast.UnaryOp: _build_unaryop, 

1399 ast.Yield: _build_yield, 

1400 ast.YieldFrom: _build_yield_from, 

1401} 

1402 

1403 

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

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

1406 

1407 

1408def get_expression( 

1409 node: ast.AST | None, 

1410 parent: Module | Class, 

1411 *, 

1412 member: str | None = None, 

1413 parse_strings: bool | None = None, 

1414) -> Expr | None: 

1415 """Build an expression from an AST. 

1416 

1417 Parameters: 

1418 node: The annotation node. 

1419 parent: The parent used to resolve the name. 

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

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

1422 

1423 Returns: 

1424 A string or resovable name or expression. 

1425 """ 

1426 if node is None: 

1427 return None 

1428 if parse_strings is None: 

1429 try: 

1430 module = parent.module 

1431 except ValueError: 

1432 parse_strings = False 

1433 else: 

1434 parse_strings = not module.imports_future_annotations 

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

1436 

1437 

1438def safe_get_expression( 

1439 node: ast.AST | None, 

1440 parent: Module | Class, 

1441 *, 

1442 member: str | None = None, 

1443 parse_strings: bool | None = None, 

1444 log_level: LogLevel | None = LogLevel.error, 

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

1446) -> Expr | None: 

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

1448 

1449 Parameters: 

1450 node: The annotation node. 

1451 parent: The parent used to resolve the name. 

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

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

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

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

1456 path, lineno, node, error. 

1457 

1458 Returns: 

1459 A string or resovable name or expression. 

1460 """ 

1461 try: 

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

1463 except Exception as error: # noqa: BLE001 

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

1465 return None 

1466 node_class = node.__class__.__name__ 

1467 try: 

1468 path: Path | str = parent.relative_filepath 

1469 except ValueError: 

1470 path = "<in-memory>" 

1471 lineno = node.lineno # type: ignore[union-attr] 

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

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

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

1475 return None 

1476 

1477 

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

1479get_annotation = partial(get_expression, parse_strings=None) 

1480safe_get_annotation = partial( 

1481 safe_get_expression, 

1482 parse_strings=None, 

1483 msg_format=_msg_format % "annotation", 

1484) 

1485get_base_class = partial(get_expression, parse_strings=False) 

1486safe_get_base_class = partial( 

1487 safe_get_expression, 

1488 parse_strings=False, 

1489 msg_format=_msg_format % "base class", 

1490) 

1491get_condition = partial(get_expression, parse_strings=False) 

1492safe_get_condition = partial( 

1493 safe_get_expression, 

1494 parse_strings=False, 

1495 msg_format=_msg_format % "condition", 

1496)