Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/astroid/brain/brain_dataclasses.py: 14%

311 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:53 +0000

1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html 

2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE 

3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt 

4 

5""" 

6Astroid hook for the dataclasses library. 

7 

8Support built-in dataclasses, pydantic.dataclasses, and marshmallow_dataclass-annotated 

9dataclasses. References: 

10- https://docs.python.org/3/library/dataclasses.html 

11- https://pydantic-docs.helpmanual.io/usage/dataclasses/ 

12- https://lovasoa.github.io/marshmallow_dataclass/ 

13""" 

14 

15from __future__ import annotations 

16 

17from collections.abc import Iterator 

18from typing import Literal, Tuple, Union 

19 

20from astroid import bases, context, helpers, nodes 

21from astroid.builder import parse 

22from astroid.const import PY39_PLUS, PY310_PLUS 

23from astroid.exceptions import AstroidSyntaxError, InferenceError, UseInferenceDefault 

24from astroid.inference_tip import inference_tip 

25from astroid.manager import AstroidManager 

26from astroid.typing import InferenceResult 

27from astroid.util import Uninferable, UninferableBase 

28 

29_FieldDefaultReturn = Union[ 

30 None, 

31 Tuple[Literal["default"], nodes.NodeNG], 

32 Tuple[Literal["default_factory"], nodes.Call], 

33] 

34 

35DATACLASSES_DECORATORS = frozenset(("dataclass",)) 

36FIELD_NAME = "field" 

37DATACLASS_MODULES = frozenset( 

38 ("dataclasses", "marshmallow_dataclass", "pydantic.dataclasses") 

39) 

40DEFAULT_FACTORY = "_HAS_DEFAULT_FACTORY" # based on typing.py 

41 

42 

43def is_decorated_with_dataclass( 

44 node: nodes.ClassDef, decorator_names: frozenset[str] = DATACLASSES_DECORATORS 

45) -> bool: 

46 """Return True if a decorated node has a `dataclass` decorator applied.""" 

47 if not isinstance(node, nodes.ClassDef) or not node.decorators: 

48 return False 

49 

50 return any( 

51 _looks_like_dataclass_decorator(decorator_attribute, decorator_names) 

52 for decorator_attribute in node.decorators.nodes 

53 ) 

54 

55 

56def dataclass_transform(node: nodes.ClassDef) -> None: 

57 """Rewrite a dataclass to be easily understood by pylint.""" 

58 node.is_dataclass = True 

59 

60 for assign_node in _get_dataclass_attributes(node): 

61 name = assign_node.target.name 

62 

63 rhs_node = nodes.Unknown( 

64 lineno=assign_node.lineno, 

65 col_offset=assign_node.col_offset, 

66 parent=assign_node, 

67 ) 

68 rhs_node = AstroidManager().visit_transforms(rhs_node) 

69 node.instance_attrs[name] = [rhs_node] 

70 

71 if not _check_generate_dataclass_init(node): 

72 return 

73 

74 kw_only_decorated = False 

75 if PY310_PLUS and node.decorators.nodes: 

76 for decorator in node.decorators.nodes: 

77 if not isinstance(decorator, nodes.Call): 

78 kw_only_decorated = False 

79 break 

80 for keyword in decorator.keywords: 

81 if keyword.arg == "kw_only": 

82 kw_only_decorated = keyword.value.bool_value() 

83 

84 init_str = _generate_dataclass_init( 

85 node, 

86 list(_get_dataclass_attributes(node, init=True)), 

87 kw_only_decorated, 

88 ) 

89 

90 try: 

91 init_node = parse(init_str)["__init__"] 

92 except AstroidSyntaxError: 

93 pass 

94 else: 

95 init_node.parent = node 

96 init_node.lineno, init_node.col_offset = None, None 

97 node.locals["__init__"] = [init_node] 

98 

99 root = node.root() 

100 if DEFAULT_FACTORY not in root.locals: 

101 new_assign = parse(f"{DEFAULT_FACTORY} = object()").body[0] 

102 new_assign.parent = root 

103 root.locals[DEFAULT_FACTORY] = [new_assign.targets[0]] 

104 

105 

106def _get_dataclass_attributes( 

107 node: nodes.ClassDef, init: bool = False 

108) -> Iterator[nodes.AnnAssign]: 

109 """Yield the AnnAssign nodes of dataclass attributes for the node. 

110 

111 If init is True, also include InitVars. 

112 """ 

113 for assign_node in node.body: 

114 if not isinstance(assign_node, nodes.AnnAssign) or not isinstance( 

115 assign_node.target, nodes.AssignName 

116 ): 

117 continue 

118 

119 # Annotation is never None 

120 if _is_class_var(assign_node.annotation): # type: ignore[arg-type] 

121 continue 

122 

123 if _is_keyword_only_sentinel(assign_node.annotation): 

124 continue 

125 

126 # Annotation is never None 

127 if not init and _is_init_var(assign_node.annotation): # type: ignore[arg-type] 

128 continue 

129 

130 yield assign_node 

131 

132 

133def _check_generate_dataclass_init(node: nodes.ClassDef) -> bool: 

134 """Return True if we should generate an __init__ method for node. 

135 

136 This is True when: 

137 - node doesn't define its own __init__ method 

138 - the dataclass decorator was called *without* the keyword argument init=False 

139 """ 

140 if "__init__" in node.locals: 

141 return False 

142 

143 found = None 

144 

145 for decorator_attribute in node.decorators.nodes: 

146 if not isinstance(decorator_attribute, nodes.Call): 

147 continue 

148 

149 if _looks_like_dataclass_decorator(decorator_attribute): 

150 found = decorator_attribute 

151 

152 if found is None: 

153 return True 

154 

155 # Check for keyword arguments of the form init=False 

156 return not any( 

157 keyword.arg == "init" 

158 and not keyword.value.bool_value() # type: ignore[union-attr] # value is never None 

159 for keyword in found.keywords 

160 ) 

161 

162 

163def _find_arguments_from_base_classes( 

164 node: nodes.ClassDef, 

165) -> tuple[ 

166 dict[str, tuple[str | None, str | None]], dict[str, tuple[str | None, str | None]] 

167]: 

168 """Iterate through all bases and get their typing and defaults.""" 

169 pos_only_store: dict[str, tuple[str | None, str | None]] = {} 

170 kw_only_store: dict[str, tuple[str | None, str | None]] = {} 

171 # See TODO down below 

172 # all_have_defaults = True 

173 

174 for base in reversed(node.mro()): 

175 if not base.is_dataclass: 

176 continue 

177 try: 

178 base_init: nodes.FunctionDef = base.locals["__init__"][0] 

179 except KeyError: 

180 continue 

181 

182 pos_only, kw_only = base_init.args._get_arguments_data() 

183 for posarg, data in pos_only.items(): 

184 # if data[1] is None: 

185 # if all_have_defaults and pos_only_store: 

186 # # TODO: This should return an Uninferable as this would raise 

187 # # a TypeError at runtime. However, transforms can't return 

188 # # Uninferables currently. 

189 # pass 

190 # all_have_defaults = False 

191 pos_only_store[posarg] = data 

192 

193 for kwarg, data in kw_only.items(): 

194 kw_only_store[kwarg] = data 

195 return pos_only_store, kw_only_store 

196 

197 

198def _parse_arguments_into_strings( 

199 pos_only_store: dict[str, tuple[str | None, str | None]], 

200 kw_only_store: dict[str, tuple[str | None, str | None]], 

201) -> tuple[str, str]: 

202 """Parse positional and keyword arguments into strings for an __init__ method.""" 

203 pos_only, kw_only = "", "" 

204 for pos_arg, data in pos_only_store.items(): 

205 pos_only += pos_arg 

206 if data[0]: 

207 pos_only += ": " + data[0] 

208 if data[1]: 

209 pos_only += " = " + data[1] 

210 pos_only += ", " 

211 for kw_arg, data in kw_only_store.items(): 

212 kw_only += kw_arg 

213 if data[0]: 

214 kw_only += ": " + data[0] 

215 if data[1]: 

216 kw_only += " = " + data[1] 

217 kw_only += ", " 

218 

219 return pos_only, kw_only 

220 

221 

222def _get_previous_field_default(node: nodes.ClassDef, name: str) -> nodes.NodeNG | None: 

223 """Get the default value of a previously defined field.""" 

224 for base in reversed(node.mro()): 

225 if not base.is_dataclass: 

226 continue 

227 if name in base.locals: 

228 for assign in base.locals[name]: 

229 if ( 

230 isinstance(assign.parent, nodes.AnnAssign) 

231 and assign.parent.value 

232 and isinstance(assign.parent.value, nodes.Call) 

233 and _looks_like_dataclass_field_call(assign.parent.value) 

234 ): 

235 default = _get_field_default(assign.parent.value) 

236 if default: 

237 return default[1] 

238 return None 

239 

240 

241def _generate_dataclass_init( # pylint: disable=too-many-locals 

242 node: nodes.ClassDef, assigns: list[nodes.AnnAssign], kw_only_decorated: bool 

243) -> str: 

244 """Return an init method for a dataclass given the targets.""" 

245 params: list[str] = [] 

246 kw_only_params: list[str] = [] 

247 assignments: list[str] = [] 

248 

249 prev_pos_only_store, prev_kw_only_store = _find_arguments_from_base_classes(node) 

250 

251 for assign in assigns: 

252 name, annotation, value = assign.target.name, assign.annotation, assign.value 

253 

254 # Check whether this assign is overriden by a property assignment 

255 property_node: nodes.FunctionDef | None = None 

256 for additional_assign in node.locals[name]: 

257 if not isinstance(additional_assign, nodes.FunctionDef): 

258 continue 

259 if not additional_assign.decorators: 

260 continue 

261 if "builtins.property" in additional_assign.decoratornames(): 

262 property_node = additional_assign 

263 break 

264 

265 is_field = isinstance(value, nodes.Call) and _looks_like_dataclass_field_call( 

266 value, check_scope=False 

267 ) 

268 

269 if is_field: 

270 # Skip any fields that have `init=False` 

271 if any( 

272 keyword.arg == "init" and not keyword.value.bool_value() 

273 for keyword in value.keywords # type: ignore[union-attr] # value is never None 

274 ): 

275 # Also remove the name from the previous arguments to be inserted later 

276 prev_pos_only_store.pop(name, None) 

277 prev_kw_only_store.pop(name, None) 

278 continue 

279 

280 if _is_init_var(annotation): # type: ignore[arg-type] # annotation is never None 

281 init_var = True 

282 if isinstance(annotation, nodes.Subscript): 

283 annotation = annotation.slice 

284 else: 

285 # Cannot determine type annotation for parameter from InitVar 

286 annotation = None 

287 assignment_str = "" 

288 else: 

289 init_var = False 

290 assignment_str = f"self.{name} = {name}" 

291 

292 ann_str, default_str = None, None 

293 if annotation is not None: 

294 ann_str = annotation.as_string() 

295 

296 if value: 

297 if is_field: 

298 result = _get_field_default(value) # type: ignore[arg-type] 

299 if result: 

300 default_type, default_node = result 

301 if default_type == "default": 

302 default_str = default_node.as_string() 

303 elif default_type == "default_factory": 

304 default_str = DEFAULT_FACTORY 

305 assignment_str = ( 

306 f"self.{name} = {default_node.as_string()} " 

307 f"if {name} is {DEFAULT_FACTORY} else {name}" 

308 ) 

309 else: 

310 default_str = value.as_string() 

311 elif property_node: 

312 # We set the result of the property call as default 

313 # This hides the fact that this would normally be a 'property object' 

314 # But we can't represent those as string 

315 try: 

316 # Call str to make sure also Uninferable gets stringified 

317 default_str = str( 

318 next(property_node.infer_call_result(None)).as_string() 

319 ) 

320 except (InferenceError, StopIteration): 

321 pass 

322 else: 

323 # Even with `init=False` the default value still can be propogated to 

324 # later assignments. Creating weird signatures like: 

325 # (self, a: str = 1) -> None 

326 previous_default = _get_previous_field_default(node, name) 

327 if previous_default: 

328 default_str = previous_default.as_string() 

329 

330 # Construct the param string to add to the init if necessary 

331 param_str = name 

332 if ann_str is not None: 

333 param_str += f": {ann_str}" 

334 if default_str is not None: 

335 param_str += f" = {default_str}" 

336 

337 # If the field is a kw_only field, we need to add it to the kw_only_params 

338 # This overwrites whether or not the class is kw_only decorated 

339 if is_field: 

340 kw_only = [k for k in value.keywords if k.arg == "kw_only"] # type: ignore[union-attr] 

341 if kw_only: 

342 if kw_only[0].value.bool_value(): 

343 kw_only_params.append(param_str) 

344 else: 

345 params.append(param_str) 

346 continue 

347 # If kw_only decorated, we need to add all parameters to the kw_only_params 

348 if kw_only_decorated: 

349 if name in prev_kw_only_store: 

350 prev_kw_only_store[name] = (ann_str, default_str) 

351 else: 

352 kw_only_params.append(param_str) 

353 else: 

354 # If the name was previously seen, overwrite that data 

355 # pylint: disable-next=else-if-used 

356 if name in prev_pos_only_store: 

357 prev_pos_only_store[name] = (ann_str, default_str) 

358 elif name in prev_kw_only_store: 

359 params = [name, *params] 

360 prev_kw_only_store.pop(name) 

361 else: 

362 params.append(param_str) 

363 

364 if not init_var: 

365 assignments.append(assignment_str) 

366 

367 prev_pos_only, prev_kw_only = _parse_arguments_into_strings( 

368 prev_pos_only_store, prev_kw_only_store 

369 ) 

370 

371 # Construct the new init method paramter string 

372 # First we do the positional only parameters, making sure to add the 

373 # the self parameter and the comma to allow adding keyword only parameters 

374 params_string = "" if "self" in prev_pos_only else "self, " 

375 params_string += prev_pos_only + ", ".join(params) 

376 if not params_string.endswith(", "): 

377 params_string += ", " 

378 

379 # Then we add the keyword only parameters 

380 if prev_kw_only or kw_only_params: 

381 params_string += "*, " 

382 params_string += f"{prev_kw_only}{', '.join(kw_only_params)}" 

383 

384 assignments_string = "\n ".join(assignments) if assignments else "pass" 

385 return f"def __init__({params_string}) -> None:\n {assignments_string}" 

386 

387 

388def infer_dataclass_attribute( 

389 node: nodes.Unknown, ctx: context.InferenceContext | None = None 

390) -> Iterator[InferenceResult]: 

391 """Inference tip for an Unknown node that was dynamically generated to 

392 represent a dataclass attribute. 

393 

394 In the case that a default value is provided, that is inferred first. 

395 Then, an Instance of the annotated class is yielded. 

396 """ 

397 assign = node.parent 

398 if not isinstance(assign, nodes.AnnAssign): 

399 yield Uninferable 

400 return 

401 

402 annotation, value = assign.annotation, assign.value 

403 if value is not None: 

404 yield from value.infer(context=ctx) 

405 if annotation is not None: 

406 yield from _infer_instance_from_annotation(annotation, ctx=ctx) 

407 else: 

408 yield Uninferable 

409 

410 

411def infer_dataclass_field_call( 

412 node: nodes.Call, ctx: context.InferenceContext | None = None 

413) -> Iterator[InferenceResult]: 

414 """Inference tip for dataclass field calls.""" 

415 if not isinstance(node.parent, (nodes.AnnAssign, nodes.Assign)): 

416 raise UseInferenceDefault 

417 result = _get_field_default(node) 

418 if not result: 

419 yield Uninferable 

420 else: 

421 default_type, default = result 

422 if default_type == "default": 

423 yield from default.infer(context=ctx) 

424 else: 

425 new_call = parse(default.as_string()).body[0].value 

426 new_call.parent = node.parent 

427 yield from new_call.infer(context=ctx) 

428 

429 

430def _looks_like_dataclass_decorator( 

431 node: nodes.NodeNG, decorator_names: frozenset[str] = DATACLASSES_DECORATORS 

432) -> bool: 

433 """Return True if node looks like a dataclass decorator. 

434 

435 Uses inference to lookup the value of the node, and if that fails, 

436 matches against specific names. 

437 """ 

438 if isinstance(node, nodes.Call): # decorator with arguments 

439 node = node.func 

440 try: 

441 inferred = next(node.infer()) 

442 except (InferenceError, StopIteration): 

443 inferred = Uninferable 

444 

445 if isinstance(inferred, UninferableBase): 

446 if isinstance(node, nodes.Name): 

447 return node.name in decorator_names 

448 if isinstance(node, nodes.Attribute): 

449 return node.attrname in decorator_names 

450 

451 return False 

452 

453 return ( 

454 isinstance(inferred, nodes.FunctionDef) 

455 and inferred.name in decorator_names 

456 and inferred.root().name in DATACLASS_MODULES 

457 ) 

458 

459 

460def _looks_like_dataclass_attribute(node: nodes.Unknown) -> bool: 

461 """Return True if node was dynamically generated as the child of an AnnAssign 

462 statement. 

463 """ 

464 parent = node.parent 

465 if not parent: 

466 return False 

467 

468 scope = parent.scope() 

469 return ( 

470 isinstance(parent, nodes.AnnAssign) 

471 and isinstance(scope, nodes.ClassDef) 

472 and is_decorated_with_dataclass(scope) 

473 ) 

474 

475 

476def _looks_like_dataclass_field_call( 

477 node: nodes.Call, check_scope: bool = True 

478) -> bool: 

479 """Return True if node is calling dataclasses field or Field 

480 from an AnnAssign statement directly in the body of a ClassDef. 

481 

482 If check_scope is False, skips checking the statement and body. 

483 """ 

484 if check_scope: 

485 stmt = node.statement(future=True) 

486 scope = stmt.scope() 

487 if not ( 

488 isinstance(stmt, nodes.AnnAssign) 

489 and stmt.value is not None 

490 and isinstance(scope, nodes.ClassDef) 

491 and is_decorated_with_dataclass(scope) 

492 ): 

493 return False 

494 

495 try: 

496 inferred = next(node.func.infer()) 

497 except (InferenceError, StopIteration): 

498 return False 

499 

500 if not isinstance(inferred, nodes.FunctionDef): 

501 return False 

502 

503 return inferred.name == FIELD_NAME and inferred.root().name in DATACLASS_MODULES 

504 

505 

506def _get_field_default(field_call: nodes.Call) -> _FieldDefaultReturn: 

507 """Return a the default value of a field call, and the corresponding keyword 

508 argument name. 

509 

510 field(default=...) results in the ... node 

511 field(default_factory=...) results in a Call node with func ... and no arguments 

512 

513 If neither or both arguments are present, return ("", None) instead, 

514 indicating that there is not a valid default value. 

515 """ 

516 default, default_factory = None, None 

517 for keyword in field_call.keywords: 

518 if keyword.arg == "default": 

519 default = keyword.value 

520 elif keyword.arg == "default_factory": 

521 default_factory = keyword.value 

522 

523 if default is not None and default_factory is None: 

524 return "default", default 

525 

526 if default is None and default_factory is not None: 

527 new_call = nodes.Call( 

528 lineno=field_call.lineno, 

529 col_offset=field_call.col_offset, 

530 parent=field_call.parent, 

531 end_lineno=field_call.end_lineno, 

532 end_col_offset=field_call.end_col_offset, 

533 ) 

534 new_call.postinit(func=default_factory, args=[], keywords=[]) 

535 return "default_factory", new_call 

536 

537 return None 

538 

539 

540def _is_class_var(node: nodes.NodeNG) -> bool: 

541 """Return True if node is a ClassVar, with or without subscripting.""" 

542 if PY39_PLUS: 

543 try: 

544 inferred = next(node.infer()) 

545 except (InferenceError, StopIteration): 

546 return False 

547 

548 return getattr(inferred, "name", "") == "ClassVar" 

549 

550 # Before Python 3.9, inference returns typing._SpecialForm instead of ClassVar. 

551 # Our backup is to inspect the node's structure. 

552 return isinstance(node, nodes.Subscript) and ( 

553 isinstance(node.value, nodes.Name) 

554 and node.value.name == "ClassVar" 

555 or isinstance(node.value, nodes.Attribute) 

556 and node.value.attrname == "ClassVar" 

557 ) 

558 

559 

560def _is_keyword_only_sentinel(node: nodes.NodeNG) -> bool: 

561 """Return True if node is the KW_ONLY sentinel.""" 

562 if not PY310_PLUS: 

563 return False 

564 inferred = helpers.safe_infer(node) 

565 return ( 

566 isinstance(inferred, bases.Instance) 

567 and inferred.qname() == "dataclasses._KW_ONLY_TYPE" 

568 ) 

569 

570 

571def _is_init_var(node: nodes.NodeNG) -> bool: 

572 """Return True if node is an InitVar, with or without subscripting.""" 

573 try: 

574 inferred = next(node.infer()) 

575 except (InferenceError, StopIteration): 

576 return False 

577 

578 return getattr(inferred, "name", "") == "InitVar" 

579 

580 

581# Allowed typing classes for which we support inferring instances 

582_INFERABLE_TYPING_TYPES = frozenset( 

583 ( 

584 "Dict", 

585 "FrozenSet", 

586 "List", 

587 "Set", 

588 "Tuple", 

589 ) 

590) 

591 

592 

593def _infer_instance_from_annotation( 

594 node: nodes.NodeNG, ctx: context.InferenceContext | None = None 

595) -> Iterator[UninferableBase | bases.Instance]: 

596 """Infer an instance corresponding to the type annotation represented by node. 

597 

598 Currently has limited support for the typing module. 

599 """ 

600 klass = None 

601 try: 

602 klass = next(node.infer(context=ctx)) 

603 except (InferenceError, StopIteration): 

604 yield Uninferable 

605 if not isinstance(klass, nodes.ClassDef): 

606 yield Uninferable 

607 elif klass.root().name in { 

608 "typing", 

609 "_collections_abc", 

610 "", 

611 }: # "" because of synthetic nodes in brain_typing.py 

612 if klass.name in _INFERABLE_TYPING_TYPES: 

613 yield klass.instantiate_class() 

614 else: 

615 yield Uninferable 

616 else: 

617 yield klass.instantiate_class() 

618 

619 

620AstroidManager().register_transform( 

621 nodes.ClassDef, dataclass_transform, is_decorated_with_dataclass 

622) 

623 

624AstroidManager().register_transform( 

625 nodes.Call, 

626 inference_tip(infer_dataclass_field_call, raise_on_overwrite=True), 

627 _looks_like_dataclass_field_call, 

628) 

629 

630AstroidManager().register_transform( 

631 nodes.Unknown, 

632 inference_tip(infer_dataclass_attribute, raise_on_overwrite=True), 

633 _looks_like_dataclass_attribute, 

634)