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

634 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-15 16:47 +0200

1# This module contains the data classes that represent resolvable names and expressions. 

2# First we declare data classes for each kind of expression, mostly corresponding to Python's AST nodes. 

3# Then we declare builder methods, that iterate AST nodes and build the corresponding data classes, 

4# and two utilities `_yield` and `_join` to help iterate on expressions. 

5# Finally we declare a few public helpersto safely get expressions from AST nodes in different scenarios. 

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, Iterable, Iterator, Sequence 

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 pathlib import Path 

24 

25 from _griffe.models import Class, Module 

26 

27 

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

29 if isinstance(element, str): 

30 yield element 

31 elif isinstance(element, tuple): 

32 for elem in element: 

33 yield from _yield(elem, flat=flat) 

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

35 yield from element.iterate(flat=True) 

36 else: 

37 yield element 

38 

39 

40def _join( 

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

42 joint: str | Expr, 

43 *, 

44 flat: bool = True, 

45) -> Iterator[str | Expr]: 

46 it = iter(elements) 

47 try: 

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

49 except StopIteration: 

50 return 

51 for element in it: 

52 yield from _yield(joint, flat=flat) 

53 yield from _yield(element, flat=flat) 

54 

55 

56def _field_as_dict( 

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

58 **kwargs: Any, 

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

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

61 return _expr_as_dict(element, **kwargs) 

62 if isinstance(element, list): 

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

64 return element 

65 

66 

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

68 fields = { 

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

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

71 if field.name != "parent" 

72 } 

73 fields["cls"] = expression.classname 

74 return fields 

75 

76 

77# YORE: EOL 3.9: Remove block. 

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

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

80 _dataclass_opts["slots"] = True 

81 

82 

83@dataclass 

84class Expr: 

85 """Base class for expressions.""" 

86 

87 def __str__(self) -> str: 

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

89 

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

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

92 yield from self.iterate(flat=False) 

93 

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

95 """Iterate on the expression elements. 

96 

97 Parameters: 

98 flat: Expressions are trees. 

99 

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

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

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

103 without them getting rendered as strings. 

104 

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

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

107 

108 Yields: 

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

110 """ 

111 yield from () 

112 

113 def modernize(self) -> Expr: 

114 """Modernize the expression. 

115 

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

117 

118 Returns: 

119 A modernized expression. 

120 """ 

121 return self 

122 

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

124 """Return the expression as a dictionary. 

125 

126 Parameters: 

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

128 

129 

130 Returns: 

131 A dictionary. 

132 """ 

133 return _expr_as_dict(self, **kwargs) 

134 

135 @property 

136 def classname(self) -> str: 

137 """The expression class name.""" 

138 return self.__class__.__name__ 

139 

140 @property 

141 def path(self) -> str: 

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

143 return str(self) 

144 

145 @property 

146 def canonical_path(self) -> str: 

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

148 return str(self) 

149 

150 @property 

151 def canonical_name(self) -> str: 

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

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

154 

155 @property 

156 def is_classvar(self) -> bool: 

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

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

159 

160 @property 

161 def is_tuple(self) -> bool: 

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

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

164 

165 @property 

166 def is_iterator(self) -> bool: 

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

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

169 

170 @property 

171 def is_generator(self) -> bool: 

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

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

174 

175 

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

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

178class ExprAttribute(Expr): 

179 """Attributes like `a.b`.""" 

180 

181 values: list[str | Expr] 

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

183 

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

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

186 

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

188 """Append a name to this attribute. 

189 

190 Parameters: 

191 value: The expression name to append. 

192 """ 

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

194 value.parent = self.last 

195 self.values.append(value) 

196 

197 @property 

198 def last(self) -> ExprName: 

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

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

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

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

203 

204 @property 

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

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

207 return self.values[0] 

208 

209 @property 

210 def path(self) -> str: 

211 """The path of this attribute.""" 

212 return self.last.path 

213 

214 @property 

215 def canonical_path(self) -> str: 

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

217 return self.last.canonical_path 

218 

219 

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

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

222class ExprBinOp(Expr): 

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

224 

225 left: str | Expr 

226 """Left part.""" 

227 operator: str 

228 """Binary operator.""" 

229 right: str | Expr 

230 """Right part.""" 

231 

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

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

234 yield f" {self.operator} " 

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

236 

237 

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

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

240class ExprBoolOp(Expr): 

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

242 

243 operator: str 

244 """Boolean operator.""" 

245 values: Sequence[str | Expr] 

246 """Operands.""" 

247 

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

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

250 

251 

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

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

254class ExprCall(Expr): 

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

256 

257 function: Expr 

258 """Function called.""" 

259 arguments: Sequence[str | Expr] 

260 """Passed arguments.""" 

261 

262 @property 

263 def canonical_path(self) -> str: 

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

265 return self.function.canonical_path 

266 

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

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

269 yield "(" 

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

271 yield ")" 

272 

273 

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

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

276class ExprCompare(Expr): 

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

278 

279 left: str | Expr 

280 """Left part.""" 

281 operators: Sequence[str] 

282 """Comparison operators.""" 

283 comparators: Sequence[str | Expr] 

284 """Things compared.""" 

285 

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

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

288 yield " " 

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

290 

291 

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

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

294class ExprComprehension(Expr): 

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

296 

297 target: str | Expr 

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

299 iterable: str | Expr 

300 """Value iterated on.""" 

301 conditions: Sequence[str | Expr] 

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

303 is_async: bool = False 

304 """Async comprehension or not.""" 

305 

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

307 if self.is_async: 

308 yield "async " 

309 yield "for " 

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

311 yield " in " 

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

313 if self.conditions: 

314 yield " if " 

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

316 

317 

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

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

320class ExprConstant(Expr): 

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

322 

323 value: str 

324 """Constant value.""" 

325 

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

327 yield self.value 

328 

329 

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

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

332class ExprDict(Expr): 

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

334 

335 keys: Sequence[str | Expr | None] 

336 """Dict keys.""" 

337 values: Sequence[str | Expr] 

338 """Dict values.""" 

339 

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

341 yield "{" 

342 yield from _join( 

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

344 ", ", 

345 flat=flat, 

346 ) 

347 yield "}" 

348 

349 

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

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

352class ExprDictComp(Expr): 

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

354 

355 key: str | Expr 

356 """Target key.""" 

357 value: str | Expr 

358 """Target value.""" 

359 generators: Sequence[Expr] 

360 """Generators iterated on.""" 

361 

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

363 yield "{" 

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

365 yield ": " 

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

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

368 yield "}" 

369 

370 

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

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

373class ExprExtSlice(Expr): 

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

375 

376 dims: Sequence[str | Expr] 

377 """Dims.""" 

378 

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

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

381 

382 

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

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

385class ExprFormatted(Expr): 

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

387 

388 value: str | Expr 

389 """Formatted value.""" 

390 

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

392 yield "{" 

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

394 yield "}" 

395 

396 

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

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

399class ExprGeneratorExp(Expr): 

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

401 

402 element: str | Expr 

403 """Yielded element.""" 

404 generators: Sequence[Expr] 

405 """Generators iterated on.""" 

406 

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

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

409 yield " " 

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

411 

412 

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

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

415class ExprIfExp(Expr): 

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

417 

418 body: str | Expr 

419 """Value if test.""" 

420 test: str | Expr 

421 """Condition.""" 

422 orelse: str | Expr 

423 """Other expression.""" 

424 

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

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

427 yield " if " 

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

429 yield " else " 

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

431 

432 

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

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

435class ExprJoinedStr(Expr): 

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

437 

438 values: Sequence[str | Expr] 

439 """Joined values.""" 

440 

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

442 yield "f'" 

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

444 yield "'" 

445 

446 

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

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

449class ExprKeyword(Expr): 

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

451 

452 name: str 

453 """Name.""" 

454 value: str | Expr 

455 """Value.""" 

456 

457 # Griffe is designed around accessing Python objects 

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

459 # Function parameters were not taken into account 

460 # because they are not accessible the same way. 

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

462 # documentation of function parameters in downstream 

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

464 # for keyword expressions, where they get a meaningful 

465 # canonical path (contrary to most other expressions that 

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

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

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

469 # of the call expression in the keyword one, 

470 # hence the following field. 

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

472 function: Expr | None = None 

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

474 

475 @property 

476 def canonical_path(self) -> str: 

477 """Path of the expressed keyword.""" 

478 if self.function: 

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

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

481 

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

483 yield self.name 

484 yield "=" 

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

486 

487 

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

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

490class ExprVarPositional(Expr): 

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

492 

493 value: Expr 

494 """Starred value.""" 

495 

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

497 yield "*" 

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

499 

500 

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

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

503class ExprVarKeyword(Expr): 

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

505 

506 value: Expr 

507 """Double-starred value.""" 

508 

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

510 yield "**" 

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

512 

513 

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

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

516class ExprLambda(Expr): 

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

518 

519 parameters: Sequence[ExprParameter] 

520 """Lambda's parameters.""" 

521 body: str | Expr 

522 """Lambda's body.""" 

523 

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

525 pos_only = False 

526 pos_or_kw = False 

527 kw_only = False 

528 length = len(self.parameters) 

529 yield "lambda" 

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

531 yield " " 

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

533 if parameter.kind is ParameterKind.positional_only: 

534 pos_only = True 

535 elif parameter.kind is ParameterKind.var_positional: 

536 yield "*" 

537 elif parameter.kind is ParameterKind.var_keyword: 

538 yield "**" 

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

540 pos_or_kw = True 

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

542 kw_only = True 

543 yield "*, " 

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

545 pos_only = False 

546 yield "/, " 

547 yield parameter.name 

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

549 yield "=" 

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

551 if index < length: 

552 yield ", " 

553 yield ": " 

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

555 

556 

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

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

559class ExprList(Expr): 

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

561 

562 elements: Sequence[Expr] 

563 """List elements.""" 

564 

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

566 yield "[" 

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

568 yield "]" 

569 

570 

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

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

573class ExprListComp(Expr): 

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

575 

576 element: str | Expr 

577 """Target value.""" 

578 generators: Sequence[Expr] 

579 """Generators iterated on.""" 

580 

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

582 yield "[" 

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

584 yield " " 

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

586 yield "]" 

587 

588 

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

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

591class ExprName(Expr): 

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

593 

594 name: str 

595 """Actual name.""" 

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

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

598 

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

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

601 if isinstance(other, ExprName): 

602 return self.name == other.name 

603 return NotImplemented 

604 

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

606 yield self 

607 

608 @property 

609 def path(self) -> str: 

610 """The full, resolved name. 

611 

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

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

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

615 """ 

616 if isinstance(self.parent, ExprName): 

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

618 return self.name 

619 

620 @property 

621 def canonical_path(self) -> str: 

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

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

624 return self.name 

625 if isinstance(self.parent, ExprName): 

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

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

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

629 try: 

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

631 except NameResolutionError: 

632 return self.name 

633 

634 @property 

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

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

637 try: 

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

639 except Exception: # noqa: BLE001 

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

641 

642 @property 

643 def is_enum_class(self) -> bool: 

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

645 try: 

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

647 except Exception: # noqa: BLE001 

648 return False 

649 

650 # TODO: Support inheritance? 

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

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

653 

654 @property 

655 def is_enum_instance(self) -> bool: 

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

657 try: 

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

659 except Exception: # noqa: BLE001 

660 return False 

661 

662 @property 

663 def is_enum_value(self) -> bool: 

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

665 try: 

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

667 except Exception: # noqa: BLE001 

668 return False 

669 

670 

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

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

673class ExprNamedExpr(Expr): 

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

675 

676 target: Expr 

677 """Target name.""" 

678 value: str | Expr 

679 """Value.""" 

680 

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

682 yield "(" 

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

684 yield " := " 

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

686 yield ")" 

687 

688 

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

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

691class ExprParameter(Expr): 

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

693 

694 name: str 

695 """Parameter name.""" 

696 kind: ParameterKind = ParameterKind.positional_or_keyword 

697 """Parameter kind.""" 

698 annotation: Expr | None = None 

699 """Parameter type.""" 

700 default: str | Expr | None = None 

701 """Parameter default.""" 

702 

703 

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

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

706class ExprSet(Expr): 

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

708 

709 elements: Sequence[str | Expr] 

710 """Set elements.""" 

711 

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

713 yield "{" 

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

715 yield "}" 

716 

717 

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

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

720class ExprSetComp(Expr): 

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

722 

723 element: str | Expr 

724 """Target value.""" 

725 generators: Sequence[Expr] 

726 """Generators iterated on.""" 

727 

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

729 yield "{" 

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

731 yield " " 

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

733 yield "}" 

734 

735 

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

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

738class ExprSlice(Expr): 

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

740 

741 lower: str | Expr | None = None 

742 """Lower bound.""" 

743 upper: str | Expr | None = None 

744 """Upper bound.""" 

745 step: str | Expr | None = None 

746 """Iteration step.""" 

747 

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

749 if self.lower is not None: 

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

751 yield ":" 

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

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

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

755 yield ":" 

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

757 

758 

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

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

761class ExprSubscript(Expr): 

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

763 

764 left: str | Expr 

765 """Left part.""" 

766 slice: Expr 

767 """Slice part.""" 

768 

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

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

771 yield "[" 

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

773 yield "]" 

774 

775 @property 

776 def path(self) -> str: 

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

778 if isinstance(self.left, str): 

779 return self.left 

780 return self.left.path 

781 

782 @property 

783 def canonical_path(self) -> str: 

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

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

786 return self.left 

787 return self.left.canonical_path 

788 

789 

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

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

792class ExprTuple(Expr): 

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

794 

795 elements: Sequence[str | Expr] 

796 """Tuple elements.""" 

797 implicit: bool = False 

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

799 

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

801 if not self.implicit: 

802 yield "(" 

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

804 if not self.implicit: 

805 yield ")" 

806 

807 

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

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

810class ExprUnaryOp(Expr): 

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

812 

813 operator: str 

814 """Unary operator.""" 

815 value: str | Expr 

816 """Value.""" 

817 

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

819 yield self.operator 

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

821 

822 

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

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

825class ExprYield(Expr): 

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

827 

828 value: str | Expr | None = None 

829 """Yielded value.""" 

830 

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

832 yield "yield" 

833 if self.value is not None: 

834 yield " " 

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

836 

837 

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

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

840class ExprYieldFrom(Expr): 

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

842 

843 value: str | Expr 

844 """Yielded-from value.""" 

845 

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

847 yield "yield from " 

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

849 

850 

851_unary_op_map = { 

852 ast.Invert: "~", 

853 ast.Not: "not ", 

854 ast.UAdd: "+", 

855 ast.USub: "-", 

856} 

857 

858_binary_op_map = { 

859 ast.Add: "+", 

860 ast.BitAnd: "&", 

861 ast.BitOr: "|", 

862 ast.BitXor: "^", 

863 ast.Div: "/", 

864 ast.FloorDiv: "//", 

865 ast.LShift: "<<", 

866 ast.MatMult: "@", 

867 ast.Mod: "%", 

868 ast.Mult: "*", 

869 ast.Pow: "**", 

870 ast.RShift: ">>", 

871 ast.Sub: "-", 

872} 

873 

874_bool_op_map = { 

875 ast.And: "and", 

876 ast.Or: "or", 

877} 

878 

879_compare_op_map = { 

880 ast.Eq: "==", 

881 ast.NotEq: "!=", 

882 ast.Lt: "<", 

883 ast.LtE: "<=", 

884 ast.Gt: ">", 

885 ast.GtE: ">=", 

886 ast.Is: "is", 

887 ast.IsNot: "is not", 

888 ast.In: "in", 

889 ast.NotIn: "not in", 

890} 

891 

892 

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

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

895 if isinstance(left, ExprAttribute): 

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

897 return left 

898 if isinstance(left, ExprName): 

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

900 if isinstance(left, str): 

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

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

903 

904 

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

906 return ExprBinOp( 

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

908 _binary_op_map[type(node.op)], 

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

910 ) 

911 

912 

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

914 return ExprBoolOp( 

915 _bool_op_map[type(node.op)], 

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

917 ) 

918 

919 

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

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

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

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

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

925 

926 

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

928 return ExprCompare( 

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

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

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

932 ) 

933 

934 

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

936 return ExprComprehension( 

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

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

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

940 is_async=bool(node.is_async), 

941 ) 

942 

943 

944def _build_constant( 

945 node: ast.Constant, 

946 parent: Module | Class, 

947 *, 

948 in_formatted_str: bool = False, 

949 in_joined_str: bool = False, 

950 parse_strings: bool = False, 

951 literal_strings: bool = False, 

952 **kwargs: Any, 

953) -> str | Expr: 

954 if isinstance(node.value, str): 

955 if in_joined_str and not in_formatted_str: 

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

957 return node.value 

958 if parse_strings and not literal_strings: 

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

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

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

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

963 try: 

964 parsed = compile( 

965 node.value, 

966 mode="eval", 

967 filename="<string-annotation>", 

968 flags=ast.PyCF_ONLY_AST, 

969 optimize=1, 

970 ) 

971 except SyntaxError: 

972 logger.debug( 

973 f"Tried and failed to parse {node.value!r} as Python code, " 

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

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

976 ) 

977 else: 

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

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

980 

981 

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

983 return ExprDict( 

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

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

986 ) 

987 

988 

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

990 return ExprDictComp( 

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

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

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

994 ) 

995 

996 

997def _build_formatted( 

998 node: ast.FormattedValue, 

999 parent: Module | Class, 

1000 *, 

1001 in_formatted_str: bool = False, # noqa: ARG001 

1002 **kwargs: Any, 

1003) -> Expr: 

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

1005 

1006 

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

1008 return ExprGeneratorExp( 

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

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

1011 ) 

1012 

1013 

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

1015 return ExprIfExp( 

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

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

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

1019 ) 

1020 

1021 

1022def _build_joinedstr( 

1023 node: ast.JoinedStr, 

1024 parent: Module | Class, 

1025 *, 

1026 in_joined_str: bool = False, # noqa: ARG001 

1027 **kwargs: Any, 

1028) -> Expr: 

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

1030 

1031 

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

1033 if node.arg is None: 

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

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

1036 

1037 

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

1039 return ExprLambda( 

1040 parameters=[ 

1041 ExprParameter( 

1042 name=name, 

1043 kind=kind, 

1044 annotation=None, 

1045 default=default 

1046 if isinstance(default, str) 

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

1048 ) 

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

1050 ], 

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

1052 ) 

1053 

1054 

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

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

1057 

1058 

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

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

1061 

1062 

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

1064 return ExprName(node.id, parent) 

1065 

1066 

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

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

1069 

1070 

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

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

1073 

1074 

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

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

1077 

1078 

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

1080 return ExprSlice( 

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

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

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

1084 ) 

1085 

1086 

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

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

1089 

1090 

1091def _build_subscript( 

1092 node: ast.Subscript, 

1093 parent: Module | Class, 

1094 *, 

1095 parse_strings: bool = False, 

1096 literal_strings: bool = False, 

1097 in_subscript: bool = False, # noqa: ARG001 

1098 **kwargs: Any, 

1099) -> Expr: 

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

1101 if parse_strings: 

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

1103 "typing.Literal", 

1104 "typing_extensions.Literal", 

1105 }: 

1106 literal_strings = True 

1107 slice = _build( 

1108 node.slice, 

1109 parent, 

1110 parse_strings=True, 

1111 literal_strings=literal_strings, 

1112 in_subscript=True, 

1113 **kwargs, 

1114 ) 

1115 else: 

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

1117 return ExprSubscript(left, slice) 

1118 

1119 

1120def _build_tuple( 

1121 node: ast.Tuple, 

1122 parent: Module | Class, 

1123 *, 

1124 in_subscript: bool = False, 

1125 **kwargs: Any, 

1126) -> Expr: 

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

1128 

1129 

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

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

1132 

1133 

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

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

1136 

1137 

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

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

1140 

1141 

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

1143 ast.Attribute: _build_attribute, 

1144 ast.BinOp: _build_binop, 

1145 ast.BoolOp: _build_boolop, 

1146 ast.Call: _build_call, 

1147 ast.Compare: _build_compare, 

1148 ast.comprehension: _build_comprehension, 

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

1150 ast.Dict: _build_dict, 

1151 ast.DictComp: _build_dictcomp, 

1152 ast.FormattedValue: _build_formatted, 

1153 ast.GeneratorExp: _build_generatorexp, 

1154 ast.IfExp: _build_ifexp, 

1155 ast.JoinedStr: _build_joinedstr, 

1156 ast.keyword: _build_keyword, 

1157 ast.Lambda: _build_lambda, 

1158 ast.List: _build_list, 

1159 ast.ListComp: _build_listcomp, 

1160 ast.Name: _build_name, 

1161 ast.NamedExpr: _build_named_expr, 

1162 ast.Set: _build_set, 

1163 ast.SetComp: _build_setcomp, 

1164 ast.Slice: _build_slice, 

1165 ast.Starred: _build_starred, 

1166 ast.Subscript: _build_subscript, 

1167 ast.Tuple: _build_tuple, 

1168 ast.UnaryOp: _build_unaryop, 

1169 ast.Yield: _build_yield, 

1170 ast.YieldFrom: _build_yield_from, 

1171} 

1172 

1173# YORE: EOL 3.8: Remove block. 

1174if sys.version_info < (3, 9): 

1175 

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

1177 return ExprExtSlice([_build(dim, parent, **kwargs) for dim in node.dims]) 

1178 

1179 def _build_index(node: ast.Index, parent: Module | Class, **kwargs: Any) -> Expr: 

1180 return _build(node.value, parent, **kwargs) 

1181 

1182 _node_map[ast.ExtSlice] = _build_extslice 

1183 _node_map[ast.Index] = _build_index 

1184 

1185 

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

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

1188 

1189 

1190def get_expression( 

1191 node: ast.AST | None, 

1192 parent: Module | Class, 

1193 *, 

1194 parse_strings: bool | None = None, 

1195) -> Expr | None: 

1196 """Build an expression from an AST. 

1197 

1198 Parameters: 

1199 node: The annotation node. 

1200 parent: The parent used to resolve the name. 

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

1202 

1203 Returns: 

1204 A string or resovable name or expression. 

1205 """ 

1206 if node is None: 

1207 return None 

1208 if parse_strings is None: 

1209 try: 

1210 module = parent.module 

1211 except ValueError: 

1212 parse_strings = False 

1213 else: 

1214 parse_strings = not module.imports_future_annotations 

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

1216 

1217 

1218def safe_get_expression( 

1219 node: ast.AST | None, 

1220 parent: Module | Class, 

1221 *, 

1222 parse_strings: bool | None = None, 

1223 log_level: LogLevel | None = LogLevel.error, 

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

1225) -> Expr | None: 

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

1227 

1228 Parameters: 

1229 node: The annotation node. 

1230 parent: The parent used to resolve the name. 

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

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

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

1234 path, lineno, node, error. 

1235 

1236 Returns: 

1237 A string or resovable name or expression. 

1238 """ 

1239 try: 

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

1241 except Exception as error: # noqa: BLE001 

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

1243 return None 

1244 node_class = node.__class__.__name__ 

1245 try: 

1246 path: Path | str = parent.relative_filepath 

1247 except ValueError: 

1248 path = "<in-memory>" 

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

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

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

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

1253 return None 

1254 

1255 

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

1257get_annotation = partial(get_expression, parse_strings=None) 

1258safe_get_annotation = partial( 

1259 safe_get_expression, 

1260 parse_strings=None, 

1261 msg_format=_msg_format % "annotation", 

1262) 

1263get_base_class = partial(get_expression, parse_strings=False) 

1264safe_get_base_class = partial( 

1265 safe_get_expression, 

1266 parse_strings=False, 

1267 msg_format=_msg_format % "base class", 

1268) 

1269get_condition = partial(get_expression, parse_strings=False) 

1270safe_get_condition = partial( 

1271 safe_get_expression, 

1272 parse_strings=False, 

1273 msg_format=_msg_format % "condition", 

1274)