Coverage for src/griffe/_internal/agents/visitor.py: 98.36%

282 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-11 13:44 +0200

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, Any, 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_condition, 

30 safe_get_expression, 

31) 

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

33from griffe._internal.models import ( 

34 Alias, 

35 Attribute, 

36 Class, 

37 Decorator, 

38 Docstring, 

39 Function, 

40 Module, 

41 Parameter, 

42 Parameters, 

43 TypeAlias, 

44 TypeParameter, 

45 TypeParameters, 

46) 

47 

48if TYPE_CHECKING: 

49 from pathlib import Path 

50 

51 from griffe._internal.docstrings.parsers import DocstringStyle 

52 from griffe._internal.enumerations import Parser 

53 

54 

55builtin_decorators = { 

56 "property": "property", 

57 "staticmethod": "staticmethod", 

58 "classmethod": "classmethod", 

59} 

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

61 

62stdlib_decorators = { 

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

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

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

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

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

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

69} 

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

71 

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

73"""Set of recognized typing overload decorators. 

74 

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

76""" 

77 

78 

79def visit( 

80 module_name: str, 

81 filepath: Path, 

82 code: str, 

83 *, 

84 extensions: Extensions | None = None, 

85 parent: Module | None = None, 

86 docstring_parser: DocstringStyle | Parser | None = None, 

87 docstring_options: dict[str, Any] | None = None, 

88 lines_collection: LinesCollection | None = None, 

89 modules_collection: ModulesCollection | None = None, 

90) -> Module: 

91 """Parse and visit a module file. 

92 

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

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

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

96 

97 Important: 

98 This function is generally not used directly. 

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

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

101 to load modules using static analysis. 

102 

103 Parameters: 

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

105 filepath: The module file path. 

106 code: The module contents. 

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

108 parent: The optional parent of this module. 

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

110 docstring_options: Additional docstring parsing options. 

111 lines_collection: A collection of source code lines. 

112 modules_collection: A collection of modules. 

113 

114 Returns: 

115 The module, with its members populated. 

116 """ 

117 return Visitor( 

118 module_name, 

119 filepath, 

120 code, 

121 extensions or load_extensions(), 

122 parent, 

123 docstring_parser=docstring_parser, 

124 docstring_options=docstring_options, 

125 lines_collection=lines_collection, 

126 modules_collection=modules_collection, 

127 ).get_module() 

128 

129 

130class Visitor: 

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

132 

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

134 """ 

135 

136 def __init__( 

137 self, 

138 module_name: str, 

139 filepath: Path, 

140 code: str, 

141 extensions: Extensions, 

142 parent: Module | None = None, 

143 docstring_parser: DocstringStyle | Parser | None = None, 

144 docstring_options: dict[str, Any] | None = None, 

145 lines_collection: LinesCollection | None = None, 

146 modules_collection: ModulesCollection | None = None, 

147 ) -> None: 

148 """Initialize the visitor. 

149 

150 Parameters: 

151 module_name: The module name. 

152 filepath: The module filepath. 

153 code: The module source code. 

154 extensions: The extensions to use when visiting. 

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

156 docstring_parser: The docstring parser to use. 

157 docstring_options: The docstring parsing options. 

158 lines_collection: A collection of source code lines. 

159 modules_collection: A collection of modules. 

160 """ 

161 super().__init__() 

162 

163 self.module_name: str = module_name 

164 """The module name.""" 

165 

166 self.filepath: Path = filepath 

167 """The module filepath.""" 

168 

169 self.code: str = code 

170 """The module source code.""" 

171 

172 self.extensions: Extensions = extensions 

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

174 

175 self.parent: Module | None = parent 

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

177 

178 self.current: Module | Class = None # type: ignore[assignment] 

179 """The current object being visited.""" 

180 

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

182 """The docstring parser to use.""" 

183 

184 self.docstring_options: dict[str, Any] = docstring_options or {} 

185 """The docstring parsing options.""" 

186 

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

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

189 

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

191 """A collection of modules.""" 

192 

193 self.type_guarded: bool = False 

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

195 

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

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

198 if value is None: 

199 return None 

200 return Docstring( 

201 value, 

202 lineno=lineno, 

203 endlineno=endlineno, 

204 parser=self.docstring_parser, 

205 parser_options=self.docstring_options, 

206 ) 

207 

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

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

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

211 ast.TypeVar: TypeParameterKind.type_var, 

212 ast.TypeVarTuple: TypeParameterKind.type_var_tuple, 

213 ast.ParamSpec: TypeParameterKind.param_spec, 

214 } 

215 

216 def _get_type_parameters( 

217 self, 

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

219 *, 

220 scope: str | None = None, 

221 ) -> list[TypeParameter]: 

222 return [ 

223 TypeParameter( 

224 type_param.name, # type: ignore[attr-defined] 

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

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

227 default=safe_get_annotation( 

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

229 parent=self.current, 

230 member=scope, 

231 ), 

232 ) 

233 for type_param in node.type_params 

234 ] 

235 else: 

236 

237 def _get_type_parameters( 

238 self, 

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

240 *, 

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

242 ) -> list[TypeParameter]: 

243 return [] 

244 

245 def get_module(self) -> Module: 

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

247 

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

249 

250 Returns: 

251 A module instance. 

252 """ 

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

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

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

256 self.visit(top_node) 

257 return self.current.module 

258 

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

260 """Extend the base visit with extensions. 

261 

262 Parameters: 

263 node: The node to visit. 

264 """ 

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

266 

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

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

269 

270 Parameters: 

271 node: The node to visit. 

272 """ 

273 for child in ast_children(node): 

274 self.visit(child) 

275 

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

277 """Visit a module node. 

278 

279 Parameters: 

280 node: The node to visit. 

281 """ 

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

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

284 self.current = module = Module( 

285 name=self.module_name, 

286 filepath=self.filepath, 

287 parent=self.parent, 

288 docstring=self._get_docstring(node), 

289 lines_collection=self.lines_collection, 

290 modules_collection=self.modules_collection, 

291 ) 

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

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

294 self.generic_visit(node) 

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

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

297 

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

299 """Visit a class definition node. 

300 

301 Parameters: 

302 node: The node to visit. 

303 """ 

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

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

306 

307 # Handle decorators. 

308 decorators: list[Decorator] = [] 

309 if node.decorator_list: 

310 lineno = node.decorator_list[0].lineno 

311 decorators.extend( 

312 Decorator( 

313 safe_get_expression(decorator_node, parent=self.current, parse_strings=False), # type: ignore[arg-type] 

314 lineno=decorator_node.lineno, 

315 endlineno=decorator_node.end_lineno, 

316 ) 

317 for decorator_node in node.decorator_list 

318 ) 

319 else: 

320 lineno = node.lineno 

321 

322 # Handle base classes. 

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

324 

325 class_ = Class( 

326 name=node.name, 

327 lineno=lineno, 

328 endlineno=node.end_lineno, 

329 docstring=self._get_docstring(node), 

330 decorators=decorators, 

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

332 bases=bases, # type: ignore[arg-type] 

333 runtime=not self.type_guarded, 

334 ) 

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

336 

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

338 self.current = class_ 

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

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

341 self.generic_visit(node) 

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

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

344 self.current = self.current.parent # type: ignore[assignment] 

345 

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

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

348 

349 Parameters: 

350 decorators: The decorators to check. 

351 

352 Returns: 

353 A set of labels. 

354 """ 

355 labels = set() 

356 for decorator in decorators: 

357 callable_path = decorator.callable_path 

358 if callable_path in builtin_decorators: 

359 labels.add(builtin_decorators[callable_path]) 

360 elif callable_path in stdlib_decorators: 

361 labels |= stdlib_decorators[callable_path] 

362 return labels 

363 

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

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

366 

367 Parameters: 

368 decorators: The decorators to check. 

369 

370 Returns: 

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

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

373 """ 

374 for decorator in decorators: 

375 try: 

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

377 except ValueError: 

378 continue 

379 property_setter_or_deleter = ( 

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

381 and path == function.path 

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

383 ) 

384 if property_setter_or_deleter: 

385 return prop_function 

386 return None 

387 

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

389 """Handle a function definition node. 

390 

391 Parameters: 

392 node: The node to visit. 

393 labels: Labels to add to the data object. 

394 """ 

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

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

397 

398 labels = labels or set() 

399 

400 # Handle decorators. 

401 decorators = [] 

402 overload = False 

403 if node.decorator_list: 

404 lineno = node.decorator_list[0].lineno 

405 for decorator_node in node.decorator_list: 

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

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

408 continue 

409 decorator = Decorator( 

410 decorator_value, 

411 lineno=decorator_node.lineno, 

412 endlineno=decorator_node.end_lineno, 

413 ) 

414 decorators.append(decorator) 

415 overload |= decorator.callable_path in typing_overload 

416 else: 

417 lineno = node.lineno 

418 

419 labels |= self.decorators_to_labels(decorators) 

420 

421 if "property" in labels: 

422 attribute = Attribute( 

423 name=node.name, 

424 value=None, 

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

426 lineno=node.lineno, 

427 endlineno=node.end_lineno, 

428 docstring=self._get_docstring(node), 

429 runtime=not self.type_guarded, 

430 ) 

431 attribute.labels |= labels 

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

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

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

435 return 

436 

437 # Handle parameters. 

438 parameters = Parameters( 

439 *[ 

440 Parameter( 

441 name, 

442 kind=kind, 

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

444 default=default 

445 if isinstance(default, str) 

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

447 ) 

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

449 ], 

450 ) 

451 

452 function = Function( 

453 name=node.name, 

454 lineno=lineno, 

455 endlineno=node.end_lineno, 

456 parameters=parameters, 

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

458 decorators=decorators, 

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

460 docstring=self._get_docstring(node), 

461 runtime=not self.type_guarded, 

462 parent=self.current, 

463 ) 

464 

465 property_function = self.get_base_property(decorators, function) 

466 

467 if overload: 

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

469 elif property_function: 

470 base_property: Attribute = self.current.members[node.name] # type: ignore[assignment] 

471 if property_function == "setter": 

472 base_property.setter = function 

473 base_property.labels.add("writable") 

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

475 base_property.deleter = function 

476 base_property.labels.add("deletable") 

477 else: 

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

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

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

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

482 

483 function.labels |= labels 

484 

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

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

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

488 self.current = function # type: ignore[assignment] 

489 self.generic_visit(node) 

490 self.current = self.current.parent # type: ignore[assignment] 

491 

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

493 """Visit a function definition node. 

494 

495 Parameters: 

496 node: The node to visit. 

497 """ 

498 self.handle_function(node) 

499 

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

501 """Visit an async function definition node. 

502 

503 Parameters: 

504 node: The node to visit. 

505 """ 

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

507 

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

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

510 

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

512 """Visit a type alias node. 

513 

514 Parameters: 

515 node: The node to visit. 

516 """ 

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

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

519 

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

521 # but represented as an expression in the AST. 

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

523 

524 name = node.name.id 

525 

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

527 

528 try: 

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

530 except (LastNodeError, AttributeError): 

531 docstring = None 

532 

533 type_alias = TypeAlias( 

534 name=name, 

535 value=value, 

536 lineno=node.lineno, 

537 endlineno=node.end_lineno, 

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

539 docstring=docstring, 

540 parent=self.current, 

541 ) 

542 self.current.set_member(name, type_alias) 

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

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

545 

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

547 """Visit an import node. 

548 

549 Parameters: 

550 node: The node to visit. 

551 """ 

552 for name in node.names: 

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

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

555 self.current.imports[alias_name] = alias_path 

556 alias = Alias( 

557 alias_name, 

558 alias_path, 

559 lineno=node.lineno, 

560 endlineno=node.end_lineno, 

561 runtime=not self.type_guarded, 

562 ) 

563 self.current.set_member(alias_name, alias) 

564 self.extensions.call("on_alias", alias=alias, node=node, agent=self) 

565 

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

567 """Visit an "import from" node. 

568 

569 Parameters: 

570 node: The node to visit. 

571 """ 

572 for name in node.names: 

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

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

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

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

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

578 continue 

579 

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

581 if name.name == "*": 

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

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

584 else: 

585 alias_name = name.asname or name.name 

586 self.current.imports[alias_name] = alias_path 

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

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

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

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

591 alias = Alias( 

592 alias_name, 

593 alias_path, 

594 lineno=node.lineno, 

595 endlineno=node.end_lineno, 

596 runtime=not self.type_guarded, 

597 ) 

598 self.current.set_member(alias_name, alias) 

599 self.extensions.call("on_alias", alias=alias, node=node, agent=self) 

600 

601 def handle_attribute( 

602 self, 

603 node: ast.Assign | ast.AnnAssign, 

604 annotation: str | Expr | None = None, 

605 ) -> None: 

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

607 

608 Parameters: 

609 node: The node to visit. 

610 annotation: A potential annotation. 

611 """ 

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

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

614 parent = self.current 

615 labels = set() 

616 

617 if parent.kind is Kind.MODULE: 

618 try: 

619 names = get_names(node) 

620 except KeyError: # Unsupported nodes, like subscript. 

621 return 

622 labels.add("module-attribute") 

623 elif parent.kind is Kind.CLASS: 

624 try: 

625 names = get_names(node) 

626 except KeyError: # Unsupported nodes, like subscript. 

627 return 

628 

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

630 # Explicit `ClassVar`: class attribute only. 

631 annotation = annotation.slice # type: ignore[attr-defined] 

632 labels.add("class-attribute") 

633 elif node.value: 

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

635 labels.add("class-attribute") 

636 labels.add("instance-attribute") 

637 else: 

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

639 labels.add("instance-attribute") 

640 

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

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

643 return 

644 try: 

645 names = get_instance_names(node) 

646 except KeyError: # Unsupported nodes, like subscript. 

647 return 

648 parent = parent.parent # type: ignore[assignment] 

649 labels.add("instance-attribute") 

650 

651 if not names: 

652 return 

653 

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

655 

656 try: 

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

658 except (LastNodeError, AttributeError): 

659 docstring = None 

660 

661 for name in names: 

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

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

664 if "." in name: 

665 continue 

666 

667 if name in parent.members: 

668 # Assigning multiple times. 

669 # TODO: Might be better to inspect. 

670 if isinstance(node.parent, (ast.If, ast.ExceptHandler)): # type: ignore[union-attr] 

671 continue # Prefer "no-exception" case. 

672 

673 existing_member = parent.members[name] 

674 with suppress(AliasResolutionError, CyclicAliasError): 

675 labels |= existing_member.labels 

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

677 if existing_member.docstring and not docstring: 

678 docstring = existing_member.docstring 

679 with suppress(AttributeError): 

680 if existing_member.annotation and not annotation: # type: ignore[union-attr] 

681 annotation = existing_member.annotation # type: ignore[union-attr] 

682 

683 attribute = Attribute( 

684 name=name, 

685 value=value, 

686 annotation=annotation, 

687 lineno=node.lineno, 

688 endlineno=node.end_lineno, 

689 docstring=docstring, 

690 runtime=not self.type_guarded, 

691 ) 

692 attribute.labels |= labels 

693 parent.set_member(name, attribute) 

694 

695 if name == "__all__": 

696 with suppress(AttributeError): 

697 parent.exports = [ 

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

699 for name in safe_get__all__(node, self.current) # type: ignore[arg-type] 

700 ] 

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

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

703 

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

705 """Visit an assignment node. 

706 

707 Parameters: 

708 node: The node to visit. 

709 """ 

710 self.handle_attribute(node) 

711 

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

713 """Visit an annotated assignment node. 

714 

715 Parameters: 

716 node: The node to visit. 

717 """ 

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

719 

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

721 """Visit an augmented assignment node. 

722 

723 Parameters: 

724 node: The node to visit. 

725 """ 

726 with suppress(AttributeError): 

727 all_augment = ( 

728 node.target.id == "__all__" # type: ignore[union-attr] 

729 and self.current.is_module 

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

731 ) 

732 if all_augment: 

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

734 self.current.exports.extend( # type: ignore[union-attr] 

735 [ 

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

737 for name in safe_get__all__(node, self.current) # type: ignore[arg-type] 

738 ], 

739 ) 

740 

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

742 """Visit an "if" node. 

743 

744 Parameters: 

745 node: The node to visit. 

746 """ 

747 if isinstance(node.parent, (ast.Module, ast.ClassDef)): # type: ignore[attr-defined] 

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

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

750 self.type_guarded = True 

751 self.generic_visit(node) 

752 self.type_guarded = False