Coverage for pymend\file_parser.py: 94%

225 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-04-20 21:33 +0200

1"""Module for parsing input file and walking ast.""" 

2 

3import ast 

4import re 

5import sys 

6from typing import Optional, Union, get_args, overload 

7 

8from typing_extensions import TypeGuard 

9 

10from .const import DEFAULT_EXCEPTION 

11from .types import ( 

12 BodyTypes, 

13 ClassDocstring, 

14 DocstringInfo, 

15 ElementDocstring, 

16 FixerSettings, 

17 FunctionBody, 

18 FunctionDocstring, 

19 FunctionSignature, 

20 ModuleDocstring, 

21 NodeOfInterest, 

22 Parameter, 

23 ReturnValue, 

24) 

25 

26__author__ = "J-E. Nitschke" 

27__copyright__ = "Copyright 2023-2024" 

28__licence__ = "GPL3" 

29__version__ = "1.0.0" 

30__maintainer__ = "J-E. Nitschke" 

31 

32 

33@overload 

34def ast_unparse(node: None) -> None: ... 34 ↛ exitline 34 didn't return from function 'ast_unparse'

35 

36 

37@overload 

38def ast_unparse(node: ast.AST) -> str: ... 38 ↛ exitline 38 didn't return from function 'ast_unparse'

39 

40 

41def ast_unparse(node: Optional[ast.AST]) -> Optional[str]: 

42 """Convert the AST node to source code as a string. 

43 

44 Parameters 

45 ---------- 

46 node : Optional[ast.AST] 

47 Node to unparse. 

48 

49 Returns 

50 ------- 

51 Optional[str] 

52 `None` if `node` was `None`. 

53 Otherwise the unparsed node. 

54 """ 

55 if node is None: 

56 return None 

57 return ast.unparse(node) 

58 

59 

60class AstAnalyzer: 

61 """Walk ast and extract module, class and function information.""" 

62 

63 def __init__(self, file_content: str, *, settings: FixerSettings) -> None: 

64 """Initialize the Analyzer with the file contents. 

65 

66 The only reason this is a class is to have the raw 

67 file_contents available at any point of the analysis to double check 

68 something. Currently used for the module docstring and docstring 

69 modifiers. 

70 

71 Parameters 

72 ---------- 

73 file_content : str 

74 File contents to store. 

75 settings : FixerSettings 

76 Settings for what to fix and when. 

77 """ 

78 self.file_content = file_content 

79 self.settings = settings 

80 

81 def parse_from_ast( 

82 self, 

83 ) -> list[ElementDocstring]: 

84 """Walk AST of the input file extract info about module, classes and functions. 

85 

86 For the module and classes, the raw docstring 

87 and its line numbers are extracted. 

88 

89 For functions the raw docstring and its line numbers are extracted. 

90 Additionally the signature is parsed for parameters and return value. 

91 

92 Returns 

93 ------- 

94 list[ElementDocstring] 

95 List of information about module, classes and functions. 

96 

97 Raises 

98 ------ 

99 AssertionError 

100 If the source file content could not be parsed into an ast. 

101 """ 

102 nodes_of_interest: list[ElementDocstring] = [] 

103 try: 

104 file_ast = ast.parse(self.file_content) 

105 except Exception as exc: # noqa: BLE001 

106 msg = f"Failed to parse source file AST: {exc}\n" 

107 raise AssertionError(msg) from exc 

108 for node in ast.walk(file_ast): 

109 if isinstance(node, ast.Module): 

110 nodes_of_interest.append(self.handle_module(node)) 

111 elif isinstance(node, ast.ClassDef): 

112 if node.name in self.settings.ignored_classes: 

113 continue 

114 nodes_of_interest.append(self.handle_class(node)) 

115 elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): 

116 if ( 

117 any( 

118 name.id in self.settings.ignored_decorators 

119 for name in node.decorator_list 

120 if isinstance(name, ast.Name) 

121 ) 

122 or node.name in self.settings.ignored_functions 

123 ): 

124 continue 

125 nodes_of_interest.append(self.handle_function(node)) 

126 return nodes_of_interest 

127 

128 def handle_module(self, module: ast.Module) -> ModuleDocstring: 

129 """Extract information about module. 

130 

131 Parameters 

132 ---------- 

133 module : ast.Module 

134 Node representing the full module. 

135 

136 Returns 

137 ------- 

138 ModuleDocstring 

139 Docstring representation for the module. 

140 """ 

141 docstring_info = self.get_docstring_info(module) 

142 if docstring_info is None: 

143 docstring_line = self._get_docstring_line() 

144 return ModuleDocstring( 

145 "Module", 

146 docstring="", 

147 lines=(docstring_line, docstring_line), 

148 modifier="", 

149 issues=[], 

150 ) 

151 return ModuleDocstring( 

152 name=docstring_info.name, 

153 docstring=docstring_info.docstring, 

154 lines=docstring_info.lines, 

155 modifier=docstring_info.modifier, 

156 issues=docstring_info.issues, 

157 ) 

158 

159 def handle_class(self, cls: ast.ClassDef) -> ClassDocstring: 

160 """Extract information about class docstring. 

161 

162 Parameters 

163 ---------- 

164 cls : ast.ClassDef 

165 Node representing a class definition. 

166 

167 Returns 

168 ------- 

169 ClassDocstring 

170 Docstring representation for a class. 

171 """ 

172 docstring = self.handle_elem_docstring(cls) 

173 attributes, methods = self.handle_class_body(cls) 

174 return ClassDocstring( 

175 name=docstring.name, 

176 docstring=docstring.docstring, 

177 lines=docstring.lines, 

178 modifier=docstring.modifier, 

179 issues=docstring.issues, 

180 attributes=attributes, 

181 methods=methods, 

182 ) 

183 

184 def handle_function( 

185 self, 

186 func: Union[ast.FunctionDef, ast.AsyncFunctionDef], 

187 ) -> FunctionDocstring: 

188 """Extract information from signature and docstring. 

189 

190 Parameters 

191 ---------- 

192 func : Union[ast.FunctionDef, ast.AsyncFunctionDef] 

193 Node representing a function definition. 

194 

195 Returns 

196 ------- 

197 FunctionDocstring 

198 Docstring representation of a function. 

199 """ 

200 docstring = self.handle_elem_docstring(func) 

201 signature = self.handle_function_signature(func) 

202 body = self.handle_function_body(func) 

203 # Minus one because the function counts the passed node itself 

204 # Which is correct for each nested node but not the main one. 

205 length = self._get_block_length(func) - 1 

206 return FunctionDocstring( 

207 name=docstring.name, 

208 docstring=docstring.docstring, 

209 lines=docstring.lines, 

210 modifier=docstring.modifier, 

211 issues=docstring.issues, 

212 signature=signature, 

213 body=body, 

214 length=length, 

215 ) 

216 

217 def handle_elem_docstring( 

218 self, 

219 elem: Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef], 

220 ) -> DocstringInfo: 

221 """Extract information about the docstring of the function. 

222 

223 Parameters 

224 ---------- 

225 elem : Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef] 

226 Element representing a function or class definition. 

227 

228 Returns 

229 ------- 

230 DocstringInfo 

231 Return general information about the docstring of the element. 

232 

233 Raises 

234 ------ 

235 ValueError 

236 If the element did not have a body at all. This should not happen 

237 for valid functions or classes. 

238 """ 

239 docstring_info = self.get_docstring_info(elem) 

240 if docstring_info is None: 

241 if not elem.body: 241 ↛ 242line 241 didn't jump to line 242, because the condition on line 241 was never true

242 msg = "Function body was unexpectedly completely empty." 

243 raise ValueError(msg) 

244 lines = (elem.body[0].lineno, elem.body[0].lineno) 

245 return DocstringInfo( 

246 name=elem.name, docstring="", lines=lines, modifier="", issues=[] 

247 ) 

248 return docstring_info 

249 

250 def get_docstring_info(self, node: NodeOfInterest) -> Optional[DocstringInfo]: 

251 """Get docstring and line number if available. 

252 

253 Parameters 

254 ---------- 

255 node : NodeOfInterest 

256 Get general information about the docstring of any node 

257 if interest. 

258 

259 Returns 

260 ------- 

261 Optional[DocstringInfo] 

262 Information about the docstring if the element contains one. 

263 Or `None` if there was no docstring at all. 

264 

265 Raises 

266 ------ 

267 ValueError 

268 If the first element of the body is not a docstring after 

269 `ast.get_docstring()` returned one. 

270 """ 

271 if ast.get_docstring(node): 

272 if not ( 272 ↛ 278line 272 didn't jump to line 278

273 node.body 

274 and isinstance(first_element := node.body[0], ast.Expr) 

275 and isinstance(docnode := first_element.value, ast.Constant) 

276 and isinstance(docnode.value, str) 

277 ): 

278 msg = ( 

279 "Expected first entry in body to be the " 

280 "docstring, but found nothing or something else." 

281 ) 

282 raise ValueError(msg) 

283 modifier = self._get_modifier( 

284 self.file_content.splitlines()[docnode.lineno - 1] 

285 ) 

286 return DocstringInfo( 

287 # Can not use DefinitionNodes in isinstance checks before 3.10 

288 name=( 

289 node.name 

290 if isinstance( 

291 node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) 

292 ) 

293 else "Module" 

294 ), 

295 docstring=str(docnode.value), 

296 lines=(docnode.lineno, docnode.end_lineno), 

297 modifier=modifier, 

298 issues=[], 

299 ) 

300 return None 

301 

302 def _get_modifier(self, line: str) -> str: 

303 """Get the string modifier from the start of a docstring. 

304 

305 Parameters 

306 ---------- 

307 line : str 

308 Line to check 

309 

310 Returns 

311 ------- 

312 str 

313 Modifier of the string. 

314 """ 

315 line = line.strip() 

316 delimiters = ['"""', "'''"] 

317 modifiers = ["r", "u"] 

318 if not line: 318 ↛ 319line 318 didn't jump to line 319, because the condition on line 318 was never true

319 return "" 

320 if line[:3] in delimiters: 

321 return "" 

322 if line[0].lower() in modifiers and line[1:4] in delimiters: 322 ↛ 324line 322 didn't jump to line 324, because the condition on line 322 was never false

323 return line[0] 

324 return "" 

325 

326 def _get_docstring_line(self) -> int: 

327 """Get the line where the module docstring should start. 

328 

329 Returns 

330 ------- 

331 int 

332 Starting line (starts at 1) of the docstring. 

333 """ 

334 shebang_encoding_lines = 2 

335 for index, line in enumerate( 

336 self.file_content.splitlines()[:shebang_encoding_lines] 

337 ): 

338 if not self.is_shebang_or_pragma(line): 

339 # List indices start at 0 but file lines are counted from 1 

340 return index + 1 

341 return shebang_encoding_lines + 1 

342 

343 def _has_body(self, node: ast.AST) -> TypeGuard[BodyTypes]: 

344 """Check that the node is one of those that have a body.""" 

345 return isinstance( 

346 node, 

347 (get_args(BodyTypes)), 

348 ) and hasattr(node, "body") 

349 

350 def _get_block_length(self, node: ast.AST) -> int: 

351 """Get the number of statements in a block. 

352 

353 Recursively count the number of statements in a blocks body. 

354 

355 Parameters 

356 ---------- 

357 node : ast.AST 

358 Node representing to count the number of statements for. 

359 

360 Returns 

361 ------- 

362 int 

363 Total number of (nested) statements in the block. 

364 """ 

365 # pylint: disable=no-member 

366 if sys.version_info >= (3, 11): 366 ↛ 369line 366 didn't jump to line 369, because the condition on line 366 was never false

367 try_nodes = (ast.Try, ast.TryStar) 

368 else: 

369 try_nodes = (ast.Try,) 

370 length = 1 

371 if self._has_body(node) and node.body: 

372 length += sum(self._get_block_length(child) for child in node.body) 

373 # Decorators add complexity, so lets count them for now 

374 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): 

375 length += len(node.decorator_list) 

376 elif isinstance(node, (ast.For, ast.AsyncFor, ast.While, ast.If, *try_nodes)): 

377 length += sum(self._get_block_length(child) for child in node.orelse) 

378 if isinstance(node, try_nodes): 

379 length += sum(self._get_block_length(child) for child in node.finalbody) 

380 length += sum(self._get_block_length(child) for child in node.handlers) 

381 elif sys.version_info >= (3, 10) and isinstance(node, ast.Match): 381 ↛ 385line 381 didn't jump to line 385, because the condition on line 381 was never true

382 # Each case counts itself + its body. 

383 # This is intended for now as compared to if/else there is a lot 

384 # of logic actually still happening in the case matching. 

385 length += sum(self._get_block_length(child) for child in node.cases) 

386 

387 # We do not want to count the docstring 

388 if ( 

389 length 

390 and isinstance( 

391 node, 

392 (ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef, ast.Module), 

393 ) 

394 and ast.get_docstring(node) 

395 ): 

396 length -= 1 

397 return length 

398 

399 def handle_class_body(self, cls: ast.ClassDef) -> tuple[list[Parameter], list[str]]: 

400 """Extract attributes and methods from class body. 

401 

402 Will walk the AST of the ClassDef node and add each function encountered 

403 as a method. 

404 

405 If the `__init__` method is encountered walk its body for attribute 

406 definitions. 

407 

408 Parameters 

409 ---------- 

410 cls : ast.ClassDef 

411 Node representing a class definition. 

412 

413 Returns 

414 ------- 

415 attributes : list[Parameter] 

416 List of the parameters that make up the classes attributes. 

417 methods : list[str] 

418 List of the method names in the class. 

419 """ 

420 attributes: list[Parameter] = [] 

421 methods: list[str] = [] 

422 for node in cls.body: 

423 if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): 

424 continue 

425 # Extract attributes from init method. 

426 if node.name == "__init__": 

427 attributes.extend(self._get_attributes_from_init(node)) 

428 # Skip dunder methods for method extraction 

429 if node.name.startswith("__") and node.name.endswith("__"): 

430 continue 

431 # Optionally skip private methods. 

432 if self.settings.ignore_privates and node.name.startswith("_"): 

433 continue 

434 # Handle properties as attributes 

435 if "property" in { 

436 decorator.id 

437 for decorator in node.decorator_list 

438 if isinstance(decorator, ast.Name) 

439 }: 

440 return_value = self.get_return_value_sig(node) 

441 attributes.append(Parameter(node.name, return_value.type_name, None)) 

442 # Handle normal methods except for those with some specific decorators 

443 # Like statismethod, classmethod, property or getters/setters. 

444 elif not self._has_excluding_decorator(node): 

445 methods.append(self._get_method_signature(node)) 

446 # Exclude some like staticmethods and properties 

447 

448 # Remove duplicates from attributes while maintaining order 

449 return list(Parameter.uniquefy(attributes)), methods 

450 

451 def handle_function_signature( 

452 self, 

453 func: Union[ast.FunctionDef, ast.AsyncFunctionDef], 

454 ) -> FunctionSignature: 

455 """Extract information about the signature of the function. 

456 

457 Parameters 

458 ---------- 

459 func : Union[ast.FunctionDef, ast.AsyncFunctionDef] 

460 Node representing a function definition 

461 

462 Returns 

463 ------- 

464 FunctionSignature 

465 Information extracted from the function signature 

466 """ 

467 parameters = self.get_parameters_sig(func) 

468 if parameters and parameters[0].arg_name == "self": 

469 parameters.pop(0) 

470 return_value = self.get_return_value_sig(func) 

471 return FunctionSignature(parameters, return_value) 

472 

473 def handle_function_body( 

474 self, 

475 func: Union[ast.FunctionDef, ast.AsyncFunctionDef], 

476 ) -> FunctionBody: 

477 """Check the function body for yields, raises and value returns. 

478 

479 Parameters 

480 ---------- 

481 func : Union[ast.FunctionDef, ast.AsyncFunctionDef] 

482 Node representing a function definition 

483 

484 Returns 

485 ------- 

486 FunctionBody 

487 Information extracted from the function body. 

488 """ 

489 returns: set[tuple[str, ...]] = set() 

490 returns_value = False 

491 yields: set[tuple[str, ...]] = set() 

492 yields_value = False 

493 raises: list[str] = [] 

494 for node in ast.walk(func): 

495 if isinstance(node, ast.Return) and node.value is not None: 

496 returns_value = True 

497 if isinstance(node.value, ast.Tuple) and all( 

498 isinstance(value, ast.Name) for value in node.value.elts 

499 ): 

500 returns.add(self._get_ids_from_returns(node.value.elts)) 

501 elif isinstance(node, (ast.Yield, ast.YieldFrom)): 

502 yields_value = True 

503 if ( 

504 isinstance(node, ast.Yield) 

505 and isinstance(node.value, ast.Tuple) 

506 and all(isinstance(value, ast.Name) for value in node.value.elts) 

507 ): 

508 yields.add(self._get_ids_from_returns(node.value.elts)) 

509 elif isinstance(node, ast.Raise): 

510 pascal_case_regex = r"^(?:[A-Z][a-z]+)+$" 

511 if not node.exc: 

512 raises.append(DEFAULT_EXCEPTION) 

513 elif isinstance(node.exc, ast.Name) and re.match( 

514 pascal_case_regex, node.exc.id 

515 ): 

516 raises.append(node.exc.id) 

517 elif ( 

518 isinstance(node.exc, ast.Call) 

519 and isinstance(node.exc.func, ast.Name) 

520 and re.match(pascal_case_regex, node.exc.func.id) 

521 ): 

522 raises.append(node.exc.func.id) 

523 else: 

524 raises.append(DEFAULT_EXCEPTION) 

525 return FunctionBody( 

526 returns_value=returns_value, 

527 returns=returns, 

528 yields_value=yields_value, 

529 yields=yields, 

530 raises=raises, 

531 ) 

532 

533 def get_return_value_sig( 

534 self, func: Union[ast.FunctionDef, ast.AsyncFunctionDef] 

535 ) -> ReturnValue: 

536 """Get information about return value from signature. 

537 

538 Parameters 

539 ---------- 

540 func : Union[ast.FunctionDef, ast.AsyncFunctionDef] 

541 Node representing a function definition 

542 

543 Returns 

544 ------- 

545 ReturnValue 

546 Return information extracted from the function signature. 

547 """ 

548 return ReturnValue(type_name=ast_unparse(func.returns)) 

549 

550 def get_parameters_sig( 

551 self, func: Union[ast.FunctionDef, ast.AsyncFunctionDef] 

552 ) -> list[Parameter]: 

553 """Get information about function parameters. 

554 

555 Parameters 

556 ---------- 

557 func : Union[ast.FunctionDef, ast.AsyncFunctionDef] 

558 Node representing a function definition 

559 

560 Returns 

561 ------- 

562 list[Parameter] 

563 Parameter information from the function signature. 

564 """ 

565 arguments: list[Parameter] = [] 

566 pos_defaults = self.get_padded_args_defaults(func) 

567 

568 pos_only_args = [ 

569 Parameter(arg.arg, ast_unparse(arg.annotation), None) 

570 for arg in func.args.posonlyargs 

571 ] 

572 arguments += pos_only_args 

573 general_args = [ 

574 Parameter(arg.arg, ast_unparse(arg.annotation), default) 

575 for arg, default in zip(func.args.args, pos_defaults) 

576 ] 

577 arguments += general_args 

578 if vararg := func.args.vararg: 

579 arguments.append( 

580 Parameter(f"*{vararg.arg}", ast_unparse(vararg.annotation), None) 

581 ) 

582 kw_only_args = [ 

583 Parameter( 

584 arg.arg, 

585 ast_unparse(arg.annotation), 

586 ast_unparse(default), 

587 ) 

588 for arg, default in zip(func.args.kwonlyargs, func.args.kw_defaults) 

589 ] 

590 arguments += kw_only_args 

591 if kwarg := func.args.kwarg: 

592 arguments.append( 

593 Parameter(f"**{kwarg.arg}", ast_unparse(kwarg.annotation), None) 

594 ) 

595 # Filter out unused arguments. 

596 return ( 

597 [ 

598 argument 

599 for argument in arguments 

600 if not argument.arg_name.startswith("_") 

601 ] 

602 if self.settings.ignore_unused_arguments 

603 else arguments 

604 ) 

605 

606 @staticmethod 

607 def is_shebang_or_pragma(line: str) -> bool: 

608 """Check if a given line contains encoding or shebang. 

609 

610 Parameters 

611 ---------- 

612 line : str 

613 Line to check 

614 

615 Returns 

616 ------- 

617 bool 

618 Whether the given line contains encoding or shebang 

619 """ 

620 shebang_regex = r"^#!(.*)" 

621 if re.search(shebang_regex, line) is not None: 

622 return True 

623 pragma_regex = r"^#.*coding[=:]\s*([-\w.]+)" 

624 return re.search(pragma_regex, line) is not None 

625 

626 def get_padded_args_defaults( 

627 self, 

628 func: Union[ast.FunctionDef, ast.AsyncFunctionDef], 

629 ) -> list[Optional[str]]: 

630 """Left-Pad the general args defaults to the length of the args. 

631 

632 Parameters 

633 ---------- 

634 func : Union[ast.FunctionDef, ast.AsyncFunctionDef] 

635 Node representing a function definition 

636 

637 Returns 

638 ------- 

639 list[Optional[str]] 

640 Left padded (with `None`) list of function arguments. 

641 """ 

642 pos_defaults = [ast_unparse(default) for default in func.args.defaults] 

643 return [None] * (len(func.args.args) - len(pos_defaults)) + pos_defaults 

644 

645 def _has_excluding_decorator( 

646 self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef] 

647 ) -> bool: 

648 """Exclude function with some decorators. 

649 

650 Currently excluded: 

651 staticmethod 

652 classmethod 

653 property (and related) 

654 

655 Parameters 

656 ---------- 

657 node : Union[ast.FunctionDef, ast.AsyncFunctionDef] 

658 Node representing a function definition 

659 

660 Returns 

661 ------- 

662 bool 

663 Whether the function as any decorators that exclude it from 

664 being recognized as a standard method. 

665 """ 

666 decorators = node.decorator_list 

667 excluded_decorators = {"staticmethod", "classmethod", "property"} 

668 for decorator in decorators: 

669 if isinstance(decorator, ast.Name) and decorator.id in excluded_decorators: 

670 return True 

671 # Handle property related decorators like in 

672 # @x.setter 

673 # def x(self, value): 

674 # self._x = value # noqa: ERA001 

675 

676 # @x.deleter 

677 # def x(self): 

678 # del self._x 

679 if ( 679 ↛ 668line 679 didn't jump to line 668

680 isinstance(decorator, ast.Attribute) 

681 and isinstance(decorator.value, ast.Name) 

682 and decorator.value.id == node.name 

683 ): 

684 return True 

685 return False 

686 

687 def _check_if_node_is_self_attributes( 

688 self, node: ast.expr 

689 ) -> TypeGuard[ast.Attribute]: 

690 """Check whether the node represents a public attribute of self (self.abc). 

691 

692 Parameters 

693 ---------- 

694 node : ast.expr 

695 Node representing the expression to be checked. 

696 

697 Returns 

698 ------- 

699 TypeGuard[ast.Attribute] 

700 True if the node represents a public attribute of self. 

701 """ 

702 return ( 

703 isinstance(node, ast.Attribute) 

704 and isinstance(node.value, ast.Name) 

705 and node.value.id == "self" 

706 and not (self.settings.ignore_privates and node.attr.startswith("_")) 

707 ) 

708 

709 def _check_and_handle_assign_node( 

710 self, target: ast.expr, attributes: list[Parameter] 

711 ) -> None: 

712 """Check if the assignment node contains assignments to self.X. 

713 

714 Add it to the list of attributes if that is the case. 

715 

716 Parameters 

717 ---------- 

718 target : ast.expr 

719 Node representing an assignment 

720 attributes : list[Parameter] 

721 List of attributes the node attribute should be added to. 

722 """ 

723 if isinstance(target, (ast.Tuple, ast.List)): 

724 for node in target.elts: 

725 if self._check_if_node_is_self_attributes(node): 725 ↛ 724line 725 didn't jump to line 724, because the condition on line 725 was never false

726 attributes.append(Parameter(node.attr, "_type_", None)) 

727 elif self._check_if_node_is_self_attributes(target): 

728 attributes.append(Parameter(target.attr, "_type_", None)) 

729 

730 def _get_attributes_from_init( 

731 self, init: Union[ast.FunctionDef, ast.AsyncFunctionDef] 

732 ) -> list[Parameter]: 

733 """Iterate over body and grab every assignment `self.abc = XYZ`. 

734 

735 Parameters 

736 ---------- 

737 init : Union[ast.FunctionDef, ast.AsyncFunctionDef] 

738 Init function node to extract attributes from. 

739 

740 Returns 

741 ------- 

742 list[Parameter] 

743 List of attributes extracted from the init function. 

744 """ 

745 attributes: list[Parameter] = [] 

746 for node in init.body: 

747 if isinstance(node, ast.Assign): 

748 # Targets is a list in case of multiple assignent 

749 # a = b = 3 # noqa: ERA001 

750 for target in node.targets: 

751 self._check_and_handle_assign_node(target, attributes) 

752 # Also handle annotated assignments 

753 # c: int = "Test" # noqa: ERA001 

754 elif isinstance(node, ast.AnnAssign): 

755 self._check_and_handle_assign_node(node.target, attributes) 

756 return attributes 

757 

758 def _get_method_signature( 

759 self, func: Union[ast.FunctionDef, ast.AsyncFunctionDef] 

760 ) -> str: 

761 """Remove self from signature and return the unparsed string. 

762 

763 Parameters 

764 ---------- 

765 func : Union[ast.FunctionDef, ast.AsyncFunctionDef] 

766 Node representing a function definition. 

767 

768 Returns 

769 ------- 

770 str 

771 String of the method signature with `self` removed. 

772 """ 

773 arguments = func.args 

774 if arguments.posonlyargs: 

775 arguments.posonlyargs = [ 

776 arg for arg in arguments.posonlyargs if arg.arg != "self" 

777 ] 

778 elif arguments.args: 778 ↛ 780line 778 didn't jump to line 780, because the condition on line 778 was never false

779 arguments.args = [arg for arg in arguments.args if arg.arg != "self"] 

780 return f"{func.name}({ast.unparse(arguments)})" 

781 

782 def _get_ids_from_returns(self, values: list[ast.expr]) -> tuple[str, ...]: 

783 """Get the ids/names for all the expressions in the list. 

784 

785 Parameters 

786 ---------- 

787 values : list[ast.expr] 

788 List of expressions to extract the ids from. 

789 

790 Returns 

791 ------- 

792 tuple[str, ...] 

793 Tuple of ids of the original expressions. 

794 """ 

795 return tuple( 

796 value.id 

797 for value in values 

798 # Needed again for type checker 

799 if isinstance(value, ast.Name) 

800 )