Coverage for src/_griffe/expressions.py: 86.96%

627 statements  

« prev     ^ index     » next       coverage.py v7.6.2, created at 2024-10-12 01:34 +0200

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 functools import partial 

14from itertools import zip_longest 

15from typing import TYPE_CHECKING, Any, Callable 

16 

17from _griffe.agents.nodes.parameters import get_parameters 

18from _griffe.enumerations import LogLevel, ParameterKind 

19from _griffe.exceptions import NameResolutionError 

20from _griffe.logger import logger 

21 

22if TYPE_CHECKING: 

23 from collections.abc import Iterable, Iterator, Sequence 

24 from pathlib import Path 

25 

26 from _griffe.models import Class, Module 

27 

28 

29def _yield(element: str | Expr | tuple[str | Expr, ...], *, flat: bool = True) -> Iterator[str | Expr]: 

30 if isinstance(element, str): 

31 yield element 

32 elif isinstance(element, tuple): 

33 for elem in element: 

34 yield from _yield(elem, flat=flat) 

35 elif flat: 35 ↛ 38line 35 didn't jump to line 38 because the condition on line 35 was always true

36 yield from element.iterate(flat=True) 

37 else: 

38 yield element 

39 

40 

41def _join( 

42 elements: Iterable[str | Expr | tuple[str | Expr, ...]], 

43 joint: str | Expr, 

44 *, 

45 flat: bool = True, 

46) -> Iterator[str | Expr]: 

47 it = iter(elements) 

48 try: 

49 yield from _yield(next(it), flat=flat) 

50 except StopIteration: 

51 return 

52 for element in it: 

53 yield from _yield(joint, flat=flat) 

54 yield from _yield(element, flat=flat) 

55 

56 

57def _field_as_dict( 

58 element: str | bool | Expr | list[str | Expr] | None, 

59 **kwargs: Any, 

60) -> str | bool | None | list | dict: 

61 if isinstance(element, Expr): 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true

62 return _expr_as_dict(element, **kwargs) 

63 if isinstance(element, list): 

64 return [_field_as_dict(elem, **kwargs) for elem in element] 

65 return element 

66 

67 

68def _expr_as_dict(expression: Expr, **kwargs: Any) -> dict[str, Any]: 

69 fields = { 

70 field.name: _field_as_dict(getattr(expression, field.name), **kwargs) 

71 for field in sorted(getfields(expression), key=lambda f: f.name) 

72 if field.name != "parent" 

73 } 

74 fields["cls"] = expression.classname 

75 return fields 

76 

77 

78# YORE: EOL 3.9: Remove block. 

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

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

81 _dataclass_opts["slots"] = True 

82 

83 

84@dataclass 

85class Expr: 

86 """Base class for expressions.""" 

87 

88 def __str__(self) -> str: 

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

90 

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

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

93 yield from self.iterate(flat=False) 

94 

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

96 """Iterate on the expression elements. 

97 

98 Parameters: 

99 flat: Expressions are trees. 

100 

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

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

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

104 without them getting rendered as strings. 

105 

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

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

108 

109 Yields: 

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

111 """ 

112 yield from () 

113 

114 def modernize(self) -> Expr: 

115 """Modernize the expression. 

116 

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

118 

119 Returns: 

120 A modernized expression. 

121 """ 

122 return self 

123 

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

125 """Return the expression as a dictionary. 

126 

127 Parameters: 

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

129 

130 

131 Returns: 

132 A dictionary. 

133 """ 

134 return _expr_as_dict(self, **kwargs) 

135 

136 @property 

137 def classname(self) -> str: 

138 """The expression class name.""" 

139 return self.__class__.__name__ 

140 

141 @property 

142 def path(self) -> str: 

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

144 return str(self) 

145 

146 @property 

147 def canonical_path(self) -> str: 

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

149 return str(self) 

150 

151 @property 

152 def canonical_name(self) -> str: 

153 """Name of the expressed name/attribute.""" 

154 return self.canonical_path.rsplit(".", 1)[-1] 

155 

156 @property 

157 def is_classvar(self) -> bool: 

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

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

160 

161 @property 

162 def is_tuple(self) -> bool: 

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

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

165 

166 @property 

167 def is_iterator(self) -> bool: 

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

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

170 

171 @property 

172 def is_generator(self) -> bool: 

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

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

175 

176 

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

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

179class ExprAttribute(Expr): 

180 """Attributes like `a.b`.""" 

181 

182 values: list[str | Expr] 

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

184 

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

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

187 

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

189 """Append a name to this attribute. 

190 

191 Parameters: 

192 value: The expression name to append. 

193 """ 

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

195 value.parent = self.last 

196 self.values.append(value) 

197 

198 @property 

199 def last(self) -> ExprName: 

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

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

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

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

204 

205 @property 

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

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

208 return self.values[0] 

209 

210 @property 

211 def path(self) -> str: 

212 """The path of this attribute.""" 

213 return self.last.path 

214 

215 @property 

216 def canonical_path(self) -> str: 

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

218 return self.last.canonical_path 

219 

220 

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

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

223class ExprBinOp(Expr): 

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

225 

226 left: str | Expr 

227 """Left part.""" 

228 operator: str 

229 """Binary operator.""" 

230 right: str | Expr 

231 """Right part.""" 

232 

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

234 yield from _yield(self.left, flat=flat) 

235 yield f" {self.operator} " 

236 yield from _yield(self.right, flat=flat) 

237 

238 

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

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

241class ExprBoolOp(Expr): 

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

243 

244 operator: str 

245 """Boolean operator.""" 

246 values: Sequence[str | Expr] 

247 """Operands.""" 

248 

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

250 yield from _join(self.values, f" {self.operator} ", flat=flat) 

251 

252 

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

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

255class ExprCall(Expr): 

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

257 

258 function: Expr 

259 """Function called.""" 

260 arguments: Sequence[str | Expr] 

261 """Passed arguments.""" 

262 

263 @property 

264 def canonical_path(self) -> str: 

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

266 return self.function.canonical_path 

267 

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

269 yield from _yield(self.function, flat=flat) 

270 yield "(" 

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

272 yield ")" 

273 

274 

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

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

277class ExprCompare(Expr): 

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

279 

280 left: str | Expr 

281 """Left part.""" 

282 operators: Sequence[str] 

283 """Comparison operators.""" 

284 comparators: Sequence[str | Expr] 

285 """Things compared.""" 

286 

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

288 yield from _yield(self.left, flat=flat) 

289 yield " " 

290 yield from _join(zip_longest(self.operators, [], self.comparators, fillvalue=" "), " ", flat=flat) 

291 

292 

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

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

295class ExprComprehension(Expr): 

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

297 

298 target: str | Expr 

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

300 iterable: str | Expr 

301 """Value iterated on.""" 

302 conditions: Sequence[str | Expr] 

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

304 is_async: bool = False 

305 """Async comprehension or not.""" 

306 

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

308 if self.is_async: 

309 yield "async " 

310 yield "for " 

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

312 yield " in " 

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

314 if self.conditions: 

315 yield " if " 

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

317 

318 

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

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

321class ExprConstant(Expr): 

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

323 

324 value: str 

325 """Constant value.""" 

326 

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

328 yield self.value 

329 

330 

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

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

333class ExprDict(Expr): 

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

335 

336 keys: Sequence[str | Expr | None] 

337 """Dict keys.""" 

338 values: Sequence[str | Expr] 

339 """Dict values.""" 

340 

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

342 yield "{" 

343 yield from _join( 

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

345 ", ", 

346 flat=flat, 

347 ) 

348 yield "}" 

349 

350 

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

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

353class ExprDictComp(Expr): 

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

355 

356 key: str | Expr 

357 """Target key.""" 

358 value: str | Expr 

359 """Target value.""" 

360 generators: Sequence[Expr] 

361 """Generators iterated on.""" 

362 

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

364 yield "{" 

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

366 yield ": " 

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

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

369 yield "}" 

370 

371 

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

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

374class ExprExtSlice(Expr): 

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

376 

377 dims: Sequence[str | Expr] 

378 """Dims.""" 

379 

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

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

382 

383 

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

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

386class ExprFormatted(Expr): 

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

388 

389 value: str | Expr 

390 """Formatted value.""" 

391 

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

393 yield "{" 

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

395 yield "}" 

396 

397 

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

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

400class ExprGeneratorExp(Expr): 

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

402 

403 element: str | Expr 

404 """Yielded element.""" 

405 generators: Sequence[Expr] 

406 """Generators iterated on.""" 

407 

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

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

410 yield " " 

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

412 

413 

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

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

416class ExprIfExp(Expr): 

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

418 

419 body: str | Expr 

420 """Value if test.""" 

421 test: str | Expr 

422 """Condition.""" 

423 orelse: str | Expr 

424 """Other expression.""" 

425 

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

427 yield from _yield(self.body, flat=flat) 

428 yield " if " 

429 yield from _yield(self.test, flat=flat) 

430 yield " else " 

431 yield from _yield(self.orelse, flat=flat) 

432 

433 

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

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

436class ExprJoinedStr(Expr): 

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

438 

439 values: Sequence[str | Expr] 

440 """Joined values.""" 

441 

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

443 yield "f'" 

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

445 yield "'" 

446 

447 

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

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

450class ExprKeyword(Expr): 

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

452 

453 name: str 

454 """Name.""" 

455 value: str | Expr 

456 """Value.""" 

457 

458 # Griffe is designed around accessing Python objects 

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

460 # Function parameters were not taken into account 

461 # because they are not accessible the same way. 

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

463 # documentation of function parameters in downstream 

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

465 # for keyword expressions, where they get a meaningful 

466 # canonical path (contrary to most other expressions that 

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

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

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

470 # of the call expression in the keyword one, 

471 # hence the following field. 

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

473 function: Expr | None = None 

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

475 

476 @property 

477 def canonical_path(self) -> str: 

478 """Path of the expressed keyword.""" 

479 if self.function: 

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

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

482 

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

484 yield self.name 

485 yield "=" 

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

487 

488 

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

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

491class ExprVarPositional(Expr): 

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

493 

494 value: Expr 

495 """Starred value.""" 

496 

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

498 yield "*" 

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

500 

501 

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

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

504class ExprVarKeyword(Expr): 

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

506 

507 value: Expr 

508 """Double-starred value.""" 

509 

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

511 yield "**" 

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

513 

514 

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

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

517class ExprLambda(Expr): 

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

519 

520 parameters: Sequence[ExprParameter] 

521 """Lambda's parameters.""" 

522 body: str | Expr 

523 """Lambda's body.""" 

524 

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

526 pos_only = False 

527 pos_or_kw = False 

528 kw_only = False 

529 length = len(self.parameters) 

530 yield "lambda" 

531 if length: 531 ↛ 533line 531 didn't jump to line 533 because the condition on line 531 was always true

532 yield " " 

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

534 if parameter.kind is ParameterKind.positional_only: 

535 pos_only = True 

536 elif parameter.kind is ParameterKind.var_positional: 

537 yield "*" 

538 elif parameter.kind is ParameterKind.var_keyword: 

539 yield "**" 

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

541 pos_or_kw = True 

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

543 kw_only = True 

544 yield "*, " 

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

546 pos_only = False 

547 yield "/, " 

548 yield parameter.name 

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

550 yield "=" 

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

552 if index < length: 

553 yield ", " 

554 yield ": " 

555 yield from _yield(self.body, flat=flat) 

556 

557 

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

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

560class ExprList(Expr): 

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

562 

563 elements: Sequence[Expr] 

564 """List elements.""" 

565 

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

567 yield "[" 

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

569 yield "]" 

570 

571 

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

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

574class ExprListComp(Expr): 

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

576 

577 element: str | Expr 

578 """Target value.""" 

579 generators: Sequence[Expr] 

580 """Generators iterated on.""" 

581 

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

583 yield "[" 

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

585 yield " " 

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

587 yield "]" 

588 

589 

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

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

592class ExprName(Expr): 

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

594 

595 name: str 

596 """Actual name.""" 

597 parent: str | ExprName | Module | Class | None = None 

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

599 

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

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

602 if isinstance(other, ExprName): 

603 return self.name == other.name 

604 return NotImplemented 

605 

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

607 yield self 

608 

609 @property 

610 def path(self) -> str: 

611 """The full, resolved name. 

612 

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

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

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

616 """ 

617 if isinstance(self.parent, ExprName): 

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

619 return self.name 

620 

621 @property 

622 def canonical_path(self) -> str: 

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

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

625 return self.name 

626 if isinstance(self.parent, ExprName): 

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

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

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

630 try: 

631 return self.parent.resolve(self.name) 

632 except NameResolutionError: 

633 return self.name 

634 

635 @property 

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

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

638 try: 

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

640 except Exception: # noqa: BLE001 

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

642 

643 @property 

644 def is_enum_class(self) -> bool: 

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

646 try: 

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

648 except Exception: # noqa: BLE001 

649 return False 

650 

651 # TODO: Support inheritance? 

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

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

654 

655 @property 

656 def is_enum_instance(self) -> bool: 

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

658 try: 

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

660 except Exception: # noqa: BLE001 

661 return False 

662 

663 @property 

664 def is_enum_value(self) -> bool: 

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

666 try: 

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

668 except Exception: # noqa: BLE001 

669 return False 

670 

671 

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

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

674class ExprNamedExpr(Expr): 

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

676 

677 target: Expr 

678 """Target name.""" 

679 value: str | Expr 

680 """Value.""" 

681 

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

683 yield "(" 

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

685 yield " := " 

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

687 yield ")" 

688 

689 

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

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

692class ExprParameter(Expr): 

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

694 

695 name: str 

696 """Parameter name.""" 

697 kind: ParameterKind = ParameterKind.positional_or_keyword 

698 """Parameter kind.""" 

699 annotation: Expr | None = None 

700 """Parameter type.""" 

701 default: str | Expr | None = None 

702 """Parameter default.""" 

703 

704 

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

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

707class ExprSet(Expr): 

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

709 

710 elements: Sequence[str | Expr] 

711 """Set elements.""" 

712 

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

714 yield "{" 

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

716 yield "}" 

717 

718 

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

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

721class ExprSetComp(Expr): 

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

723 

724 element: str | Expr 

725 """Target value.""" 

726 generators: Sequence[Expr] 

727 """Generators iterated on.""" 

728 

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

730 yield "{" 

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

732 yield " " 

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

734 yield "}" 

735 

736 

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

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

739class ExprSlice(Expr): 

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

741 

742 lower: str | Expr | None = None 

743 """Lower bound.""" 

744 upper: str | Expr | None = None 

745 """Upper bound.""" 

746 step: str | Expr | None = None 

747 """Iteration step.""" 

748 

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

750 if self.lower is not None: 

751 yield from _yield(self.lower, flat=flat) 

752 yield ":" 

753 if self.upper is not None: 753 ↛ 755line 753 didn't jump to line 755 because the condition on line 753 was always true

754 yield from _yield(self.upper, flat=flat) 

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

756 yield ":" 

757 yield from _yield(self.step, flat=flat) 

758 

759 

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

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

762class ExprSubscript(Expr): 

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

764 

765 left: str | Expr 

766 """Left part.""" 

767 slice: Expr 

768 """Slice part.""" 

769 

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

771 yield from _yield(self.left, flat=flat) 

772 yield "[" 

773 yield from _yield(self.slice, flat=flat) 

774 yield "]" 

775 

776 @property 

777 def path(self) -> str: 

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

779 if isinstance(self.left, str): 

780 return self.left 

781 return self.left.path 

782 

783 @property 

784 def canonical_path(self) -> str: 

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

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

787 return self.left 

788 return self.left.canonical_path 

789 

790 

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

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

793class ExprTuple(Expr): 

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

795 

796 elements: Sequence[str | Expr] 

797 """Tuple elements.""" 

798 implicit: bool = False 

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

800 

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

802 if not self.implicit: 

803 yield "(" 

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

805 if not self.implicit: 

806 yield ")" 

807 

808 

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

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

811class ExprUnaryOp(Expr): 

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

813 

814 operator: str 

815 """Unary operator.""" 

816 value: str | Expr 

817 """Value.""" 

818 

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

820 yield self.operator 

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

822 

823 

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

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

826class ExprYield(Expr): 

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

828 

829 value: str | Expr | None = None 

830 """Yielded value.""" 

831 

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

833 yield "yield" 

834 if self.value is not None: 

835 yield " " 

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

837 

838 

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

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

841class ExprYieldFrom(Expr): 

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

843 

844 value: str | Expr 

845 """Yielded-from value.""" 

846 

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

848 yield "yield from " 

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

850 

851 

852_unary_op_map = { 

853 ast.Invert: "~", 

854 ast.Not: "not ", 

855 ast.UAdd: "+", 

856 ast.USub: "-", 

857} 

858 

859_binary_op_map = { 

860 ast.Add: "+", 

861 ast.BitAnd: "&", 

862 ast.BitOr: "|", 

863 ast.BitXor: "^", 

864 ast.Div: "/", 

865 ast.FloorDiv: "//", 

866 ast.LShift: "<<", 

867 ast.MatMult: "@", 

868 ast.Mod: "%", 

869 ast.Mult: "*", 

870 ast.Pow: "**", 

871 ast.RShift: ">>", 

872 ast.Sub: "-", 

873} 

874 

875_bool_op_map = { 

876 ast.And: "and", 

877 ast.Or: "or", 

878} 

879 

880_compare_op_map = { 

881 ast.Eq: "==", 

882 ast.NotEq: "!=", 

883 ast.Lt: "<", 

884 ast.LtE: "<=", 

885 ast.Gt: ">", 

886 ast.GtE: ">=", 

887 ast.Is: "is", 

888 ast.IsNot: "is not", 

889 ast.In: "in", 

890 ast.NotIn: "not in", 

891} 

892 

893 

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

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

896 if isinstance(left, ExprAttribute): 

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

898 return left 

899 if isinstance(left, ExprName): 

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

901 if isinstance(left, str): 

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

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

904 

905 

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

907 return ExprBinOp( 

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

909 _binary_op_map[type(node.op)], 

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

911 ) 

912 

913 

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

915 return ExprBoolOp( 

916 _bool_op_map[type(node.op)], 

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

918 ) 

919 

920 

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

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

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

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

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

926 

927 

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

929 return ExprCompare( 

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

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

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

933 ) 

934 

935 

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

937 return ExprComprehension( 

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

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

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

941 is_async=bool(node.is_async), 

942 ) 

943 

944 

945def _build_constant( 

946 node: ast.Constant, 

947 parent: Module | Class, 

948 *, 

949 in_formatted_str: bool = False, 

950 in_joined_str: bool = False, 

951 parse_strings: bool = False, 

952 literal_strings: bool = False, 

953 **kwargs: Any, 

954) -> str | Expr: 

955 if isinstance(node.value, str): 

956 if in_joined_str and not in_formatted_str: 

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

958 return node.value 

959 if parse_strings and not literal_strings: 

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

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

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

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

964 try: 

965 parsed = compile( 

966 node.value, 

967 mode="eval", 

968 filename="<string-annotation>", 

969 flags=ast.PyCF_ONLY_AST, 

970 optimize=1, 

971 ) 

972 except SyntaxError: 

973 logger.debug( 

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

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

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

977 node.value, 

978 ) 

979 else: 

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

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

982 

983 

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

985 return ExprDict( 

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

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

988 ) 

989 

990 

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

992 return ExprDictComp( 

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

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

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

996 ) 

997 

998 

999def _build_formatted( 

1000 node: ast.FormattedValue, 

1001 parent: Module | Class, 

1002 *, 

1003 in_formatted_str: bool = False, # noqa: ARG001 

1004 **kwargs: Any, 

1005) -> Expr: 

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

1007 

1008 

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

1010 return ExprGeneratorExp( 

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

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

1013 ) 

1014 

1015 

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

1017 return ExprIfExp( 

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

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

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

1021 ) 

1022 

1023 

1024def _build_joinedstr( 

1025 node: ast.JoinedStr, 

1026 parent: Module | Class, 

1027 *, 

1028 in_joined_str: bool = False, # noqa: ARG001 

1029 **kwargs: Any, 

1030) -> Expr: 

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

1032 

1033 

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

1035 if node.arg is None: 

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

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

1038 

1039 

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

1041 return ExprLambda( 

1042 parameters=[ 

1043 ExprParameter( 

1044 name=name, 

1045 kind=kind, 

1046 annotation=None, 

1047 default=default 

1048 if isinstance(default, str) 

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

1050 ) 

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

1052 ], 

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

1054 ) 

1055 

1056 

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

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

1059 

1060 

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

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

1063 

1064 

1065def _build_name(node: ast.Name, parent: Module | Class, **kwargs: Any) -> Expr: # noqa: ARG001 

1066 return ExprName(node.id, parent) 

1067 

1068 

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

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

1071 

1072 

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

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

1075 

1076 

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

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

1079 

1080 

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

1082 return ExprSlice( 

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

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

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

1086 ) 

1087 

1088 

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

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

1091 

1092 

1093def _build_subscript( 

1094 node: ast.Subscript, 

1095 parent: Module | Class, 

1096 *, 

1097 parse_strings: bool = False, 

1098 literal_strings: bool = False, 

1099 in_subscript: bool = False, # noqa: ARG001 

1100 **kwargs: Any, 

1101) -> Expr: 

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

1103 if parse_strings: 

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

1105 "typing.Literal", 

1106 "typing_extensions.Literal", 

1107 }: 

1108 literal_strings = True 

1109 slice = _build( 

1110 node.slice, 

1111 parent, 

1112 parse_strings=True, 

1113 literal_strings=literal_strings, 

1114 in_subscript=True, 

1115 **kwargs, 

1116 ) 

1117 else: 

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

1119 return ExprSubscript(left, slice) 

1120 

1121 

1122def _build_tuple( 

1123 node: ast.Tuple, 

1124 parent: Module | Class, 

1125 *, 

1126 in_subscript: bool = False, 

1127 **kwargs: Any, 

1128) -> Expr: 

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

1130 

1131 

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

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

1134 

1135 

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

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

1138 

1139 

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

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

1142 

1143 

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

1145 ast.Attribute: _build_attribute, 

1146 ast.BinOp: _build_binop, 

1147 ast.BoolOp: _build_boolop, 

1148 ast.Call: _build_call, 

1149 ast.Compare: _build_compare, 

1150 ast.comprehension: _build_comprehension, 

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

1152 ast.Dict: _build_dict, 

1153 ast.DictComp: _build_dictcomp, 

1154 ast.FormattedValue: _build_formatted, 

1155 ast.GeneratorExp: _build_generatorexp, 

1156 ast.IfExp: _build_ifexp, 

1157 ast.JoinedStr: _build_joinedstr, 

1158 ast.keyword: _build_keyword, 

1159 ast.Lambda: _build_lambda, 

1160 ast.List: _build_list, 

1161 ast.ListComp: _build_listcomp, 

1162 ast.Name: _build_name, 

1163 ast.NamedExpr: _build_named_expr, 

1164 ast.Set: _build_set, 

1165 ast.SetComp: _build_setcomp, 

1166 ast.Slice: _build_slice, 

1167 ast.Starred: _build_starred, 

1168 ast.Subscript: _build_subscript, 

1169 ast.Tuple: _build_tuple, 

1170 ast.UnaryOp: _build_unaryop, 

1171 ast.Yield: _build_yield, 

1172 ast.YieldFrom: _build_yield_from, 

1173} 

1174 

1175 

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

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

1178 

1179 

1180def get_expression( 

1181 node: ast.AST | None, 

1182 parent: Module | Class, 

1183 *, 

1184 parse_strings: bool | None = None, 

1185) -> Expr | None: 

1186 """Build an expression from an AST. 

1187 

1188 Parameters: 

1189 node: The annotation node. 

1190 parent: The parent used to resolve the name. 

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

1192 

1193 Returns: 

1194 A string or resovable name or expression. 

1195 """ 

1196 if node is None: 

1197 return None 

1198 if parse_strings is None: 

1199 try: 

1200 module = parent.module 

1201 except ValueError: 

1202 parse_strings = False 

1203 else: 

1204 parse_strings = not module.imports_future_annotations 

1205 return _build(node, parent, parse_strings=parse_strings) 

1206 

1207 

1208def safe_get_expression( 

1209 node: ast.AST | None, 

1210 parent: Module | Class, 

1211 *, 

1212 parse_strings: bool | None = None, 

1213 log_level: LogLevel | None = LogLevel.error, 

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

1215) -> Expr | None: 

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

1217 

1218 Parameters: 

1219 node: The annotation node. 

1220 parent: The parent used to resolve the name. 

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

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

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

1224 path, lineno, node, error. 

1225 

1226 Returns: 

1227 A string or resovable name or expression. 

1228 """ 

1229 try: 

1230 return get_expression(node, parent, parse_strings=parse_strings) 

1231 except Exception as error: # noqa: BLE001 

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

1233 return None 

1234 node_class = node.__class__.__name__ 

1235 try: 

1236 path: Path | str = parent.relative_filepath 

1237 except ValueError: 

1238 path = "<in-memory>" 

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

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

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

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

1243 return None 

1244 

1245 

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

1247get_annotation = partial(get_expression, parse_strings=None) 

1248safe_get_annotation = partial( 

1249 safe_get_expression, 

1250 parse_strings=None, 

1251 msg_format=_msg_format % "annotation", 

1252) 

1253get_base_class = partial(get_expression, parse_strings=False) 

1254safe_get_base_class = partial( 

1255 safe_get_expression, 

1256 parse_strings=False, 

1257 msg_format=_msg_format % "base class", 

1258) 

1259get_condition = partial(get_expression, parse_strings=False) 

1260safe_get_condition = partial( 

1261 safe_get_expression, 

1262 parse_strings=False, 

1263 msg_format=_msg_format % "condition", 

1264)