Coverage for packages / griffelib / src / griffe / _internal / agents / visitor.py: 97.85%

286 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-11 11:48 +0100

1# This module contains our static analysis agent, 

2# capable of parsing and visiting sources, statically. 

3 

4from __future__ import annotations 

5 

6import ast 

7import sys 

8from contextlib import suppress 

9from typing import TYPE_CHECKING, Final 

10 

11from griffe._internal.agents.nodes.assignments import get_instance_names, get_names 

12from griffe._internal.agents.nodes.ast import ( 

13 ast_children, 

14 ast_kind, 

15 ast_next, 

16) 

17from griffe._internal.agents.nodes.docstrings import get_docstring 

18from griffe._internal.agents.nodes.exports import safe_get__all__ 

19from griffe._internal.agents.nodes.imports import relative_to_absolute 

20from griffe._internal.agents.nodes.parameters import get_parameters 

21from griffe._internal.collections import LinesCollection, ModulesCollection 

22from griffe._internal.enumerations import Kind, TypeParameterKind 

23from griffe._internal.exceptions import AliasResolutionError, CyclicAliasError, LastNodeError 

24from griffe._internal.expressions import ( 

25 Expr, 

26 ExprName, 

27 safe_get_annotation, 

28 safe_get_base_class, 

29 safe_get_class_keyword, 

30 safe_get_condition, 

31 safe_get_expression, 

32) 

33from griffe._internal.extensions.base import Extensions, load_extensions 

34from griffe._internal.models import ( 

35 Alias, 

36 Attribute, 

37 Class, 

38 Decorator, 

39 Docstring, 

40 Function, 

41 Module, 

42 Parameter, 

43 Parameters, 

44 TypeAlias, 

45 TypeParameter, 

46 TypeParameters, 

47) 

48 

49if TYPE_CHECKING: 

50 from pathlib import Path 

51 

52 from griffe._internal.docstrings.parsers import DocstringOptions, DocstringStyle 

53 from griffe._internal.enumerations import Parser 

54 

55 

56builtin_decorators = { 

57 "property": "property", 

58 "staticmethod": "staticmethod", 

59 "classmethod": "classmethod", 

60} 

61"""Mapping of builtin decorators to labels.""" 

62 

63stdlib_decorators = { 

64 "abc.abstractmethod": {"abstractmethod"}, 

65 "functools.cache": {"cached"}, 

66 "functools.cached_property": {"cached", "property"}, 

67 "cached_property.cached_property": {"cached", "property"}, 

68 "functools.lru_cache": {"cached"}, 

69 "dataclasses.dataclass": {"dataclass"}, 

70} 

71"""Mapping of standard library decorators to labels.""" 

72 

73typing_overload = {"typing.overload", "typing_extensions.overload"} 

74"""Set of recognized typing overload decorators. 

75 

76When such a decorator is found, the decorated function becomes an overload. 

77""" 

78 

79 

80def visit( 

81 module_name: str, 

82 filepath: Path, 

83 code: str, 

84 *, 

85 extensions: Extensions | None = None, 

86 parent: Module | None = None, 

87 docstring_parser: DocstringStyle | Parser | None = None, 

88 docstring_options: DocstringOptions | None = None, 

89 lines_collection: LinesCollection | None = None, 

90 modules_collection: ModulesCollection | None = None, 

91) -> Module: 

92 """Parse and visit a module file. 

93 

94 We provide this function for static analysis. It uses a [`NodeVisitor`][ast.NodeVisitor]-like class, 

95 the [`Visitor`][griffe.Visitor], to compile and parse code (using [`compile`][]) 

96 then visit the resulting AST (Abstract Syntax Tree). 

97 

98 Important: 

99 This function is generally not used directly. 

100 In most cases, users can rely on the [`GriffeLoader`][griffe.GriffeLoader] 

101 and its accompanying [`load`][griffe.load] shortcut and their respective options 

102 to load modules using static analysis. 

103 

104 Parameters: 

105 module_name: The module name (as when importing [from] it). 

106 filepath: The module file path. 

107 code: The module contents. 

108 extensions: The extensions to use when visiting the AST. 

109 parent: The optional parent of this module. 

110 docstring_parser: The docstring parser to use. By default, no parsing is done. 

111 docstring_options: Docstring parsing options. 

112 lines_collection: A collection of source code lines. 

113 modules_collection: A collection of modules. 

114 

115 Returns: 

116 The module, with its members populated. 

117 """ 

118 return Visitor( 

119 module_name, 

120 filepath, 

121 code, 

122 extensions or load_extensions(), 

123 parent, 

124 docstring_parser=docstring_parser, 

125 docstring_options=docstring_options, 

126 lines_collection=lines_collection, 

127 modules_collection=modules_collection, 

128 ).get_module() 

129 

130 

131class Visitor: 

132 """This class is used to instantiate a visitor. 

133 

134 Visitors iterate on AST nodes to extract data from them. 

135 """ 

136 

137 def __init__( 

138 self, 

139 module_name: str, 

140 filepath: Path, 

141 code: str, 

142 extensions: Extensions, 

143 parent: Module | None = None, 

144 docstring_parser: DocstringStyle | Parser | None = None, 

145 docstring_options: DocstringOptions | None = None, 

146 lines_collection: LinesCollection | None = None, 

147 modules_collection: ModulesCollection | None = None, 

148 ) -> None: 

149 """Initialize the visitor. 

150 

151 Parameters: 

152 module_name: The module name. 

153 filepath: The module filepath. 

154 code: The module source code. 

155 extensions: The extensions to use when visiting. 

156 parent: An optional parent for the final module object. 

157 docstring_parser: The docstring parser to use. 

158 docstring_options: Docstring parsing options. 

159 lines_collection: A collection of source code lines. 

160 modules_collection: A collection of modules. 

161 """ 

162 super().__init__() 

163 

164 self.module_name: str = module_name 

165 """The module name.""" 

166 

167 self.filepath: Path = filepath 

168 """The module filepath.""" 

169 

170 self.code: str = code 

171 """The module source code.""" 

172 

173 self.extensions: Extensions = extensions 

174 """The extensions to use when visiting the AST.""" 

175 

176 self.parent: Module | None = parent 

177 """An optional parent for the final module object.""" 

178 

179 self.current: Module | Class = None 

180 """The current object being visited.""" 

181 

182 self.docstring_parser: DocstringStyle | Parser | None = docstring_parser 

183 """The docstring parser to use.""" 

184 

185 self.docstring_options: DocstringOptions = docstring_options or {} 

186 """The docstring parsing options.""" 

187 

188 self.lines_collection: LinesCollection = lines_collection or LinesCollection() 

189 """A collection of source code lines.""" 

190 

191 self.modules_collection: ModulesCollection = modules_collection or ModulesCollection() 

192 """A collection of modules.""" 

193 

194 self.type_guarded: bool = False 

195 """Whether the current code branch is type-guarded.""" 

196 

197 def _get_docstring(self, node: ast.AST, *, strict: bool = False) -> Docstring | None: 

198 value, lineno, endlineno = get_docstring(node, strict=strict) 

199 if value is None: 

200 return None 

201 return Docstring( 

202 value, 

203 lineno=lineno, 

204 endlineno=endlineno, 

205 parser=self.docstring_parser, 

206 parser_options=self.docstring_options, 

207 ) 

208 

209 # YORE: EOL 3.11: Replace block with lines 2-36. 

210 if sys.version_info >= (3, 12): 

211 _type_parameter_kind_map: Final[dict[type[ast.type_param], TypeParameterKind]] = { 

212 ast.TypeVar: TypeParameterKind.type_var, 

213 ast.TypeVarTuple: TypeParameterKind.type_var_tuple, 

214 ast.ParamSpec: TypeParameterKind.param_spec, 

215 } 

216 

217 def _get_type_parameters( 

218 self, 

219 node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.TypeAlias, 

220 *, 

221 scope: str | None = None, 

222 ) -> list[TypeParameter]: 

223 return [ 

224 TypeParameter( 

225 type_param.name, # ty:ignore[unresolved-attribute,unused-ignore-comment,unused-ignore-comment] 

226 kind=self._type_parameter_kind_map[type(type_param)], 

227 bound=safe_get_annotation(getattr(type_param, "bound", None), parent=self.current, member=scope), 

228 default=safe_get_annotation( 

229 getattr(type_param, "default_value", None), 

230 parent=self.current, 

231 member=scope, 

232 ), 

233 ) 

234 for type_param in node.type_params # ty:ignore[possibly-missing-attribute,unused-ignore-comment,unused-ignore-comment] 

235 ] 

236 else: 

237 

238 def _get_type_parameters( 

239 self, 

240 node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef, # noqa: ARG002, 

241 *, 

242 scope: str | None = None, # noqa: ARG002, 

243 ) -> list[TypeParameter]: 

244 return [] 

245 

246 def get_module(self) -> Module: 

247 """Build and return the object representing the module attached to this visitor. 

248 

249 This method triggers a complete visit of the module nodes. 

250 

251 Returns: 

252 A module instance. 

253 """ 

254 # Optimization: equivalent to `ast.parse`, but with `optimize=1` to remove assert statements. 

255 # TODO: With options, could use `optimize=2` to remove docstrings. 

256 top_node = compile(self.code, mode="exec", filename=str(self.filepath), flags=ast.PyCF_ONLY_AST, optimize=1) 

257 self.visit(top_node) 

258 return self.current.module 

259 

260 def visit(self, node: ast.AST) -> None: 

261 """Extend the base visit with extensions. 

262 

263 Parameters: 

264 node: The node to visit. 

265 """ 

266 getattr(self, f"visit_{ast_kind(node)}", self.generic_visit)(node) 

267 

268 def generic_visit(self, node: ast.AST) -> None: 

269 """Extend the base generic visit with extensions. 

270 

271 Parameters: 

272 node: The node to visit. 

273 """ 

274 for child in ast_children(node): 

275 self.visit(child) 

276 

277 def visit_module(self, node: ast.Module) -> None: 

278 """Visit a module node. 

279 

280 Parameters: 

281 node: The node to visit. 

282 """ 

283 self.extensions.call("on_node", node=node, agent=self) 

284 self.extensions.call("on_module_node", node=node, agent=self) 

285 self.current = module = Module( 

286 name=self.module_name, 

287 filepath=self.filepath, 

288 parent=self.parent, 

289 docstring=self._get_docstring(node), 

290 lines_collection=self.lines_collection, 

291 modules_collection=self.modules_collection, 

292 analysis="static", 

293 ) 

294 self.extensions.call("on_instance", node=node, obj=module, agent=self) 

295 self.extensions.call("on_module_instance", node=node, mod=module, agent=self) 

296 self.generic_visit(node) 

297 self.extensions.call("on_members", node=node, obj=module, agent=self) 

298 self.extensions.call("on_module_members", node=node, mod=module, agent=self) 

299 

300 def visit_classdef(self, node: ast.ClassDef) -> None: 

301 """Visit a class definition node. 

302 

303 Parameters: 

304 node: The node to visit. 

305 """ 

306 self.extensions.call("on_node", node=node, agent=self) 

307 self.extensions.call("on_class_node", node=node, agent=self) 

308 

309 # Handle decorators. 

310 decorators: list[Decorator] = [] 

311 if node.decorator_list: 

312 lineno = node.decorator_list[0].lineno 

313 decorators.extend( 

314 Decorator( 

315 safe_get_expression(decorator_node, parent=self.current, parse_strings=False), # ty:ignore[invalid-argument-type] 

316 lineno=decorator_node.lineno, 

317 endlineno=decorator_node.end_lineno, 

318 ) 

319 for decorator_node in node.decorator_list 

320 ) 

321 else: 

322 lineno = node.lineno 

323 

324 # Handle base classes and keywords. 

325 bases = [safe_get_base_class(base, parent=self.current, member=node.name) for base in node.bases] 

326 keywords = { 

327 kw.arg: safe_get_class_keyword(kw.value, parent=self.current) for kw in node.keywords if kw.arg is not None 

328 } 

329 

330 class_ = Class( 

331 name=node.name, 

332 lineno=lineno, 

333 endlineno=node.end_lineno, 

334 docstring=self._get_docstring(node), 

335 decorators=decorators, 

336 type_parameters=TypeParameters(*self._get_type_parameters(node, scope=node.name)), 

337 bases=bases, # ty:ignore[invalid-argument-type] 

338 keywords=keywords, 

339 runtime=not self.type_guarded, 

340 analysis="static", 

341 ) 

342 class_.labels |= self.decorators_to_labels(decorators) 

343 

344 self.current.set_member(node.name, class_) 

345 self.current = class_ 

346 self.extensions.call("on_instance", node=node, obj=class_, agent=self) 

347 self.extensions.call("on_class_instance", node=node, cls=class_, agent=self) 

348 self.generic_visit(node) 

349 self.extensions.call("on_members", node=node, obj=class_, agent=self) 

350 self.extensions.call("on_class_members", node=node, cls=class_, agent=self) 

351 self.current = self.current.parent # ty:ignore[invalid-assignment] 

352 

353 def decorators_to_labels(self, decorators: list[Decorator]) -> set[str]: 

354 """Build and return a set of labels based on decorators. 

355 

356 Parameters: 

357 decorators: The decorators to check. 

358 

359 Returns: 

360 A set of labels. 

361 """ 

362 labels = set() 

363 for decorator in decorators: 

364 callable_path = decorator.callable_path 

365 if callable_path in builtin_decorators: 

366 labels.add(builtin_decorators[callable_path]) 

367 elif callable_path in stdlib_decorators: 

368 labels |= stdlib_decorators[callable_path] 

369 return labels 

370 

371 def get_base_property(self, decorators: list[Decorator], function: Function) -> str | None: 

372 """Check decorators to return the base property in case of setters and deleters. 

373 

374 Parameters: 

375 decorators: The decorators to check. 

376 

377 Returns: 

378 base_property: The property for which the setter/deleted is set. 

379 property_function: Either `"setter"` or `"deleter"`. 

380 """ 

381 for decorator in decorators: 

382 try: 

383 path, prop_function = decorator.callable_path.rsplit(".", 1) 

384 except ValueError: 

385 continue 

386 property_setter_or_deleter = ( 

387 prop_function in {"setter", "deleter"} 

388 and path == function.path 

389 and self.current.get_member(function.name).has_labels("property") 

390 ) 

391 if property_setter_or_deleter: 

392 return prop_function 

393 return None 

394 

395 def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels: set | None = None) -> None: 

396 """Handle a function definition node. 

397 

398 Parameters: 

399 node: The node to visit. 

400 labels: Labels to add to the data object. 

401 """ 

402 self.extensions.call("on_node", node=node, agent=self) 

403 self.extensions.call("on_function_node", node=node, agent=self) 

404 

405 labels = labels or set() 

406 

407 # Handle decorators. 

408 decorators = [] 

409 overload = False 

410 if node.decorator_list: 

411 lineno = node.decorator_list[0].lineno 

412 for decorator_node in node.decorator_list: 

413 decorator_value = safe_get_expression(decorator_node, parent=self.current, parse_strings=False) 

414 if decorator_value is None: 414 ↛ 415line 414 didn't jump to line 415 because the condition on line 414 was never true

415 continue 

416 decorator = Decorator( 

417 decorator_value, 

418 lineno=decorator_node.lineno, 

419 endlineno=decorator_node.end_lineno, 

420 ) 

421 decorators.append(decorator) 

422 overload |= decorator.callable_path in typing_overload 

423 else: 

424 lineno = node.lineno 

425 

426 labels |= self.decorators_to_labels(decorators) 

427 

428 if "property" in labels: 

429 attribute = Attribute( 

430 name=node.name, 

431 value=None, 

432 annotation=safe_get_annotation(node.returns, parent=self.current, member=node.name), 

433 lineno=node.lineno, 

434 endlineno=node.end_lineno, 

435 docstring=self._get_docstring(node), 

436 runtime=not self.type_guarded, 

437 analysis="static", 

438 ) 

439 attribute.labels |= labels 

440 self.current.set_member(node.name, attribute) 

441 self.extensions.call("on_instance", node=node, obj=attribute, agent=self) 

442 self.extensions.call("on_attribute_instance", node=node, attr=attribute, agent=self) 

443 return 

444 

445 # Handle parameters. 

446 parameters = Parameters( 

447 *[ 

448 Parameter( 

449 name, 

450 kind=kind, 

451 annotation=safe_get_annotation(annotation, parent=self.current, member=node.name), 

452 default=default 

453 if isinstance(default, str) 

454 else safe_get_expression(default, parent=self.current, parse_strings=False), 

455 ) 

456 for name, annotation, kind, default in get_parameters(node.args) 

457 ], 

458 ) 

459 

460 function = Function( 

461 name=node.name, 

462 lineno=lineno, 

463 endlineno=node.end_lineno, 

464 parameters=parameters, 

465 returns=safe_get_annotation(node.returns, parent=self.current, member=node.name), 

466 decorators=decorators, 

467 type_parameters=TypeParameters(*self._get_type_parameters(node, scope=node.name)), 

468 docstring=self._get_docstring(node), 

469 runtime=not self.type_guarded, 

470 parent=self.current, 

471 analysis="static", 

472 ) 

473 

474 property_function = self.get_base_property(decorators, function) 

475 

476 if overload: 

477 self.current.overloads[function.name].append(function) 

478 elif property_function: 

479 base_property: Attribute = self.current.members[node.name] # ty:ignore[invalid-assignment] 

480 if property_function == "setter": 

481 base_property.setter = function 

482 base_property.labels.add("writable") 

483 elif property_function == "deleter": 483 ↛ 492line 483 didn't jump to line 492 because the condition on line 483 was always true

484 base_property.deleter = function 

485 base_property.labels.add("deletable") 

486 else: 

487 self.current.set_member(node.name, function) 

488 if self.current.kind in {Kind.MODULE, Kind.CLASS} and self.current.overloads[function.name]: 

489 function.overloads = self.current.overloads[function.name] 

490 del self.current.overloads[function.name] 

491 

492 function.labels |= labels 

493 

494 self.extensions.call("on_instance", node=node, obj=function, agent=self) 

495 self.extensions.call("on_function_instance", node=node, func=function, agent=self) 

496 if self.current.kind is Kind.CLASS and function.name == "__init__": 

497 self.current = function # ty:ignore[invalid-assignment] 

498 self.generic_visit(node) 

499 self.current = self.current.parent # ty:ignore[invalid-assignment] 

500 

501 def visit_functiondef(self, node: ast.FunctionDef) -> None: 

502 """Visit a function definition node. 

503 

504 Parameters: 

505 node: The node to visit. 

506 """ 

507 self.handle_function(node) 

508 

509 def visit_asyncfunctiondef(self, node: ast.AsyncFunctionDef) -> None: 

510 """Visit an async function definition node. 

511 

512 Parameters: 

513 node: The node to visit. 

514 """ 

515 self.handle_function(node, labels={"async"}) 

516 

517 # YORE: EOL 3.11: Replace block with lines 2-36. 

518 if sys.version_info >= (3, 12): 

519 

520 def visit_typealias(self, node: ast.TypeAlias) -> None: 

521 """Visit a type alias node. 

522 

523 Parameters: 

524 node: The node to visit. 

525 """ 

526 self.extensions.call("on_node", node=node, agent=self) 

527 self.extensions.call("on_type_alias_node", node=node, agent=self) 

528 

529 # A type alias's name attribute is syntactically a single NAME, 

530 # but represented as an expression in the AST. 

531 # https://jellezijlstra.github.io/pep695#ast 

532 

533 name = node.name.id 

534 

535 value = safe_get_expression(node.value, parent=self.current, member=name) 

536 

537 try: 

538 docstring = self._get_docstring(ast_next(node), strict=True) 

539 except (LastNodeError, AttributeError): 

540 docstring = None 

541 

542 type_alias = TypeAlias( 

543 name=name, 

544 value=value, 

545 lineno=node.lineno, 

546 endlineno=node.end_lineno, 

547 type_parameters=TypeParameters(*self._get_type_parameters(node, scope=name)), 

548 docstring=docstring, 

549 parent=self.current, 

550 analysis="static", 

551 ) 

552 self.current.set_member(name, type_alias) 

553 self.extensions.call("on_instance", node=node, obj=type_alias, agent=self) 

554 self.extensions.call("on_type_alias_instance", node=node, type_alias=type_alias, agent=self) 

555 

556 def visit_import(self, node: ast.Import) -> None: 

557 """Visit an import node. 

558 

559 Parameters: 

560 node: The node to visit. 

561 """ 

562 for name in node.names: 

563 alias_path = name.name if name.asname else name.name.split(".", 1)[0] 

564 alias_name = name.asname or alias_path.split(".", 1)[0] 

565 self.current.imports[alias_name] = alias_path 

566 alias = Alias( 

567 alias_name, 

568 alias_path, 

569 lineno=node.lineno, 

570 endlineno=node.end_lineno, 

571 runtime=not self.type_guarded, 

572 analysis="static", 

573 ) 

574 self.current.set_member(alias_name, alias) 

575 self.extensions.call("on_alias_instance", alias=alias, node=node, agent=self) 

576 

577 def visit_importfrom(self, node: ast.ImportFrom) -> None: 

578 """Visit an "import from" node. 

579 

580 Parameters: 

581 node: The node to visit. 

582 """ 

583 for name in node.names: 

584 if not node.module and node.level == 1 and not name.asname and self.current.module.is_init_module: 

585 # Special case: when being in `a/__init__.py` and doing `from . import b`, 

586 # we are effectively creating a member `b` in `a` that is pointing to `a.b` 

587 # -> cyclic alias! In that case, we just skip it, as both the member and module 

588 # have the same name and can be accessed the same way. 

589 continue 

590 

591 alias_path = relative_to_absolute(node, name, self.current.module) 

592 if name.name == "*": 

593 alias_name = alias_path.replace(".", "/") 

594 alias_path = alias_path.replace(".*", "") 

595 else: 

596 alias_name = name.asname or name.name 

597 self.current.imports[alias_name] = alias_path 

598 # Do not create aliases pointing to themselves (it happens with 

599 # `from package.current_module import Thing as Thing` or 

600 # `from . import thing as thing`). 

601 if alias_path != f"{self.current.path}.{alias_name}": 

602 alias = Alias( 

603 alias_name, 

604 alias_path, 

605 lineno=node.lineno, 

606 endlineno=node.end_lineno, 

607 runtime=not self.type_guarded, 

608 analysis="static", 

609 ) 

610 self.current.set_member(alias_name, alias) 

611 self.extensions.call("on_alias_instance", alias=alias, node=node, agent=self) 

612 

613 def handle_attribute( 

614 self, 

615 node: ast.Assign | ast.AnnAssign, 

616 annotation: str | Expr | None = None, 

617 ) -> None: 

618 """Handle an attribute (assignment) node. 

619 

620 Parameters: 

621 node: The node to visit. 

622 annotation: A potential annotation. 

623 """ 

624 self.extensions.call("on_node", node=node, agent=self) 

625 self.extensions.call("on_attribute_node", node=node, agent=self) 

626 parent = self.current 

627 labels = set() 

628 names = None 

629 

630 if parent.kind is Kind.MODULE: 

631 try: 

632 names = get_names(node) 

633 except KeyError: # Unsupported nodes, like subscript. 

634 return 

635 labels.add("module-attribute") 

636 elif parent.kind is Kind.CLASS: 

637 try: 

638 names = get_names(node) 

639 except KeyError: # Unsupported nodes, like subscript. 

640 return 

641 

642 if isinstance(annotation, Expr) and annotation.is_classvar: 

643 # Explicit `ClassVar`: class attribute only. 

644 annotation = annotation.slice # ty:ignore[unresolved-attribute] 

645 labels.add("class-attribute") 

646 elif node.value: 

647 # Attribute assigned at class-level: available in instances as well. 

648 labels.add("class-attribute") 

649 labels.add("instance-attribute") 

650 else: 

651 # Annotated attribute only: not available at class-level. 

652 labels.add("instance-attribute") 

653 

654 elif parent.kind is Kind.FUNCTION: 654 ↛ 666line 654 didn't jump to line 666 because the condition on line 654 was always true

655 if parent.name != "__init__": 655 ↛ 656line 655 didn't jump to line 656 because the condition on line 655 was never true

656 return 

657 try: 

658 names = get_instance_names(node) 

659 except KeyError: # Unsupported nodes, like subscript. 

660 return 

661 parent = parent.parent 

662 if parent is None: 662 ↛ 663line 662 didn't jump to line 663 because the condition on line 662 was never true

663 return 

664 labels.add("instance-attribute") 

665 

666 if not names: 

667 return 

668 

669 value = safe_get_expression(node.value, parent=self.current, parse_strings=False) 

670 

671 try: 

672 docstring = self._get_docstring(ast_next(node), strict=True) 

673 except (LastNodeError, AttributeError): 

674 docstring = None 

675 

676 for name in names: 

677 # TODO: Handle assigns like `x.y = z`. 

678 # We need to resolve `x.y` and add `z` in its members. 

679 if "." in name: 

680 continue 

681 

682 if name in parent.members: 

683 # Assigning multiple times. 

684 # TODO: Might be better to inspect. 

685 if isinstance(node.parent, (ast.If, ast.ExceptHandler)): # ty:ignore[unresolved-attribute] 

686 continue # Prefer "no-exception" case. 

687 

688 existing_member = parent.members[name] 

689 with suppress(AliasResolutionError, CyclicAliasError): 

690 labels |= existing_member.labels 

691 # Forward previous docstring and annotation instead of erasing them. 

692 if existing_member.docstring and not docstring: 

693 docstring = existing_member.docstring 

694 with suppress(AttributeError): 

695 if existing_member.annotation and not annotation: # ty:ignore[possibly-missing-attribute] 

696 annotation = existing_member.annotation # ty:ignore[possibly-missing-attribute] 

697 

698 attribute = Attribute( 

699 name=name, 

700 value=value, 

701 annotation=annotation, 

702 lineno=node.lineno, 

703 endlineno=node.end_lineno, 

704 docstring=docstring, 

705 runtime=not self.type_guarded, 

706 analysis="static", 

707 ) 

708 attribute.labels |= labels 

709 parent.set_member(name, attribute) 

710 

711 if name == "__all__": 

712 with suppress(AttributeError): 

713 parent.exports = [ 

714 name if isinstance(name, str) else ExprName(name.name, parent=name.parent) 

715 for name in safe_get__all__(node, self.current) # ty:ignore[invalid-argument-type] 

716 ] 

717 self.extensions.call("on_instance", node=node, obj=attribute, agent=self) 

718 self.extensions.call("on_attribute_instance", node=node, attr=attribute, agent=self) 

719 

720 def visit_assign(self, node: ast.Assign) -> None: 

721 """Visit an assignment node. 

722 

723 Parameters: 

724 node: The node to visit. 

725 """ 

726 self.handle_attribute(node) 

727 

728 def visit_annassign(self, node: ast.AnnAssign) -> None: 

729 """Visit an annotated assignment node. 

730 

731 Parameters: 

732 node: The node to visit. 

733 """ 

734 self.handle_attribute(node, safe_get_annotation(node.annotation, parent=self.current)) 

735 

736 def visit_augassign(self, node: ast.AugAssign) -> None: 

737 """Visit an augmented assignment node. 

738 

739 Parameters: 

740 node: The node to visit. 

741 """ 

742 with suppress(AttributeError): 

743 all_augment = ( 

744 node.target.id == "__all__" # ty:ignore[possibly-missing-attribute] 

745 and self.current.is_module 

746 and isinstance(node.op, ast.Add) 

747 ) 

748 if all_augment: 

749 # We assume `exports` is not `None` at this point. 

750 self.current.exports.extend( # ty:ignore[possibly-missing-attribute] 

751 [ 

752 name if isinstance(name, str) else ExprName(name.name, parent=name.parent) 

753 for name in safe_get__all__(node, self.current) # ty:ignore[invalid-argument-type] 

754 ], 

755 ) 

756 

757 def visit_if(self, node: ast.If) -> None: 

758 """Visit an "if" node. 

759 

760 Parameters: 

761 node: The node to visit. 

762 """ 

763 if isinstance(node.parent, (ast.Module, ast.ClassDef)): # ty:ignore[unresolved-attribute] 

764 condition = safe_get_condition(node.test, parent=self.current, log_level=None) 

765 if str(condition) in {"typing.TYPE_CHECKING", "TYPE_CHECKING"}: 

766 self.type_guarded = True 

767 self.generic_visit(node) 

768 self.type_guarded = False