Coverage for packages / griffelib / src / griffe / _internal / extensions / base.py: 84.66%

123 statements  

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

1# This module contains the base class for extensions 

2# and the functions to load them. 

3 

4from __future__ import annotations 

5 

6import os 

7import sys 

8from importlib.util import module_from_spec, spec_from_file_location 

9from inspect import isclass 

10from pathlib import Path 

11from typing import TYPE_CHECKING, Any 

12 

13from griffe._internal.agents.nodes.ast import ast_children, ast_kind 

14from griffe._internal.exceptions import ExtensionNotLoadedError 

15from griffe._internal.importer import dynamic_import 

16 

17if TYPE_CHECKING: 

18 import ast 

19 from types import ModuleType 

20 

21 from griffe._internal.agents.inspector import Inspector 

22 from griffe._internal.agents.nodes.runtime import ObjectNode 

23 from griffe._internal.agents.visitor import Visitor 

24 from griffe._internal.loader import GriffeLoader 

25 from griffe._internal.models import Alias, Attribute, Class, Function, Module, Object, TypeAlias 

26 

27 

28class Extension: 

29 """Base class for Griffe extensions.""" 

30 

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

32 """Visit a node. 

33 

34 Parameters: 

35 node: The node to visit. 

36 """ 

37 getattr(self, f"visit_{ast_kind(node)}", lambda _: None)(node) 

38 

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

40 """Visit children nodes. 

41 

42 Parameters: 

43 node: The node to visit the children of. 

44 """ 

45 for child in ast_children(node): 

46 self.visit(child) 

47 

48 def inspect(self, node: ObjectNode) -> None: 

49 """Inspect a node. 

50 

51 Parameters: 

52 node: The node to inspect. 

53 """ 

54 getattr(self, f"inspect_{node.kind}", lambda _: None)(node) 

55 

56 def generic_inspect(self, node: ObjectNode) -> None: 

57 """Extend the base generic inspection with extensions. 

58 

59 Parameters: 

60 node: The node to inspect. 

61 """ 

62 for child in node.children: 

63 if not child.alias_target_path: 

64 self.inspect(child) 

65 

66 def on_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: 

67 """Run when visiting a new node during static/dynamic analysis. 

68 

69 Parameters: 

70 node: The currently visited node. 

71 """ 

72 

73 def on_instance( 

74 self, 

75 *, 

76 node: ast.AST | ObjectNode, 

77 obj: Object, 

78 agent: Visitor | Inspector, 

79 **kwargs: Any, 

80 ) -> None: 

81 """Run when an Object has been created. 

82 

83 Warning: 

84 This method runs while the object tree is still being constructed: 

85 data might be incomplete (class inheritance, alias resolution, etc.). 

86 Only hook onto this event if you know what you're doing. 

87 

88 Parameters: 

89 node: The currently visited node. 

90 obj: The object instance. 

91 agent: The analysis agent currently running. 

92 **kwargs: For forward-compatibility. 

93 """ 

94 

95 def on_object(self, *, obj: Object, loader: GriffeLoader, **kwargs: Any) -> None: 

96 """Run on objects (every kind) once the object tree has been fully constructed. 

97 

98 Note: 

99 This method runs once the object tree has been fully constructed: 

100 data is therefore complete and you can safely hook onto this event. 

101 

102 Parameters: 

103 obj: The object instance. 

104 loader: The loader currently in use. 

105 **kwargs: For forward-compatibility. 

106 """ 

107 

108 def on_members(self, *, node: ast.AST | ObjectNode, obj: Object, agent: Visitor | Inspector, **kwargs: Any) -> None: 

109 """Run when members of an Object have been loaded. 

110 

111 Warning: 

112 This method runs while the object tree is still being constructed: 

113 data might be incomplete (class inheritance, alias resolution, etc.). 

114 Only hook onto this event if you know what you're doing. 

115 

116 Parameters: 

117 node: The currently visited node. 

118 obj: The object instance. 

119 agent: The analysis agent currently running. 

120 **kwargs: For forward-compatibility. 

121 """ 

122 

123 def on_module_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: 

124 """Run when visiting a new module node during static/dynamic analysis. 

125 

126 Parameters: 

127 node: The currently visited node. 

128 agent: The analysis agent currently running. 

129 **kwargs: For forward-compatibility. 

130 """ 

131 

132 def on_module_instance( 

133 self, 

134 *, 

135 node: ast.AST | ObjectNode, 

136 mod: Module, 

137 agent: Visitor | Inspector, 

138 **kwargs: Any, 

139 ) -> None: 

140 """Run when a Module has been created. 

141 

142 Warning: 

143 This method runs while the object tree is still being constructed: 

144 data might be incomplete (class inheritance, alias resolution, etc.). 

145 Only hook onto this event if you know what you're doing. 

146 

147 Parameters: 

148 node: The currently visited node. 

149 mod: The module instance. 

150 agent: The analysis agent currently running. 

151 **kwargs: For forward-compatibility. 

152 """ 

153 

154 def on_module(self, *, mod: Module, loader: GriffeLoader, **kwargs: Any) -> None: 

155 """Run on modules once the object tree has been fully constructed. 

156 

157 Note: 

158 This method runs once the object tree has been fully constructed: 

159 data is therefore complete and you can safely hook onto this event. 

160 

161 Parameters: 

162 mod: The module instance. 

163 loader: The loader currently in use. 

164 **kwargs: For forward-compatibility. 

165 """ 

166 

167 def on_module_members( 

168 self, 

169 *, 

170 node: ast.AST | ObjectNode, 

171 mod: Module, 

172 agent: Visitor | Inspector, 

173 **kwargs: Any, 

174 ) -> None: 

175 """Run when members of a Module have been loaded. 

176 

177 Warning: 

178 This method runs while the object tree is still being constructed: 

179 data might be incomplete (class inheritance, alias resolution, etc.). 

180 Only hook onto this event if you know what you're doing. 

181 

182 Parameters: 

183 node: The currently visited node. 

184 mod: The module instance. 

185 agent: The analysis agent currently running. 

186 **kwargs: For forward-compatibility. 

187 """ 

188 

189 def on_class_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: 

190 """Run when visiting a new class node during static/dynamic analysis. 

191 

192 Parameters: 

193 node: The currently visited node. 

194 agent: The analysis agent currently running. 

195 **kwargs: For forward-compatibility. 

196 """ 

197 

198 def on_class_instance( 

199 self, 

200 *, 

201 node: ast.AST | ObjectNode, 

202 cls: Class, 

203 agent: Visitor | Inspector, 

204 **kwargs: Any, 

205 ) -> None: 

206 """Run when a Class has been created. 

207 

208 Warning: 

209 This method runs while the object tree is still being constructed: 

210 data might be incomplete (class inheritance, alias resolution, etc.). 

211 Only hook onto this event if you know what you're doing. 

212 

213 Parameters: 

214 node: The currently visited node. 

215 cls: The class instance. 

216 agent: The analysis agent currently running. 

217 **kwargs: For forward-compatibility. 

218 """ 

219 

220 def on_class(self, *, cls: Class, loader: GriffeLoader, **kwargs: Any) -> None: 

221 """Run on classes once the object tree has been fully constructed. 

222 

223 Note: 

224 This method runs once the object tree has been fully constructed: 

225 data is therefore complete and you can safely hook onto this event. 

226 

227 Parameters: 

228 cls: The class instance. 

229 loader: The loader currently in use. 

230 **kwargs: For forward-compatibility. 

231 """ 

232 

233 def on_class_members( 

234 self, 

235 *, 

236 node: ast.AST | ObjectNode, 

237 cls: Class, 

238 agent: Visitor | Inspector, 

239 **kwargs: Any, 

240 ) -> None: 

241 """Run when members of a Class have been loaded. 

242 

243 Warning: 

244 This method runs while the object tree is still being constructed: 

245 data might be incomplete (class inheritance, alias resolution, etc.). 

246 Only hook onto this event if you know what you're doing. 

247 

248 Parameters: 

249 node: The currently visited node. 

250 cls: The class instance. 

251 agent: The analysis agent currently running. 

252 **kwargs: For forward-compatibility. 

253 """ 

254 

255 def on_function_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: 

256 """Run when visiting a new function node during static/dynamic analysis. 

257 

258 Parameters: 

259 node: The currently visited node. 

260 agent: The analysis agent currently running. 

261 **kwargs: For forward-compatibility. 

262 """ 

263 

264 def on_function_instance( 

265 self, 

266 *, 

267 node: ast.AST | ObjectNode, 

268 func: Function, 

269 agent: Visitor | Inspector, 

270 **kwargs: Any, 

271 ) -> None: 

272 """Run when a Function has been created. 

273 

274 Warning: 

275 This method runs while the object tree is still being constructed: 

276 data might be incomplete (class inheritance, alias resolution, etc.). 

277 Only hook onto this event if you know what you're doing. 

278 

279 Parameters: 

280 node: The currently visited node. 

281 func: The function instance. 

282 agent: The analysis agent currently running. 

283 **kwargs: For forward-compatibility. 

284 """ 

285 

286 def on_function(self, *, func: Function, loader: GriffeLoader, **kwargs: Any) -> None: 

287 """Run on functions once the object tree has been fully constructed. 

288 

289 Note: 

290 This method runs once the object tree has been fully constructed: 

291 data is therefore complete and you can safely hook onto this event. 

292 

293 Parameters: 

294 func: The function instance. 

295 loader: The loader currently in use. 

296 **kwargs: For forward-compatibility. 

297 """ 

298 

299 def on_attribute_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: 

300 """Run when visiting a new attribute node during static/dynamic analysis. 

301 

302 Parameters: 

303 node: The currently visited node. 

304 agent: The analysis agent currently running. 

305 **kwargs: For forward-compatibility. 

306 """ 

307 

308 def on_attribute_instance( 

309 self, 

310 *, 

311 node: ast.AST | ObjectNode, 

312 attr: Attribute, 

313 agent: Visitor | Inspector, 

314 **kwargs: Any, 

315 ) -> None: 

316 """Run when an Attribute has been created. 

317 

318 Warning: 

319 This method runs while the object tree is still being constructed: 

320 data might be incomplete (class inheritance, alias resolution, etc.). 

321 Only hook onto this event if you know what you're doing. 

322 

323 Parameters: 

324 node: The currently visited node. 

325 attr: The attribute instance. 

326 agent: The analysis agent currently running. 

327 **kwargs: For forward-compatibility. 

328 """ 

329 

330 def on_attribute(self, *, attr: Attribute, loader: GriffeLoader, **kwargs: Any) -> None: 

331 """Run on attributes once the object tree has been fully constructed. 

332 

333 Note: 

334 This method runs once the object tree has been fully constructed: 

335 data is therefore complete and you can safely hook onto this event. 

336 

337 Parameters: 

338 attr: The attribute instance. 

339 loader: The loader currently in use. 

340 **kwargs: For forward-compatibility. 

341 """ 

342 

343 def on_type_alias_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: 

344 """Run when visiting a new type alias node during static/dynamic analysis. 

345 

346 Parameters: 

347 node: The currently visited node. 

348 agent: The analysis agent currently running. 

349 **kwargs: For forward-compatibility. 

350 """ 

351 

352 def on_type_alias_instance( 

353 self, 

354 *, 

355 node: ast.AST | ObjectNode, 

356 type_alias: TypeAlias, 

357 agent: Visitor | Inspector, 

358 **kwargs: Any, 

359 ) -> None: 

360 """Run when a TypeAlias has been created. 

361 

362 Warning: 

363 This method runs while the object tree is still being constructed: 

364 data might be incomplete (class inheritance, alias resolution, etc.). 

365 Only hook onto this event if you know what you're doing. 

366 

367 Parameters: 

368 node: The currently visited node. 

369 type_alias: The type alias instance. 

370 agent: The analysis agent currently running. 

371 **kwargs: For forward-compatibility. 

372 """ 

373 

374 def on_type_alias(self, *, type_alias: TypeAlias, loader: GriffeLoader, **kwargs: Any) -> None: 

375 """Run on type aliases once the object tree has been fully constructed. 

376 

377 Note: 

378 This method runs once the object tree has been fully constructed: 

379 data is therefore complete and you can safely hook onto this event. 

380 

381 Parameters: 

382 type_alias: The type alias instance. 

383 loader: The loader currently in use. 

384 **kwargs: For forward-compatibility. 

385 """ 

386 

387 def on_alias_instance( 

388 self, 

389 *, 

390 node: ast.AST | ObjectNode, 

391 alias: Alias, 

392 agent: Visitor | Inspector, 

393 **kwargs: Any, 

394 ) -> None: 

395 """Run when an Alias has been created. 

396 

397 Warning: 

398 This method runs while the object tree is still being constructed: 

399 data might be incomplete (class inheritance, alias resolution, etc.). 

400 Only hook onto this event if you know what you're doing. 

401 

402 Parameters: 

403 node: The currently visited node. 

404 alias: The alias instance. 

405 agent: The analysis agent currently running. 

406 **kwargs: For forward-compatibility. 

407 """ 

408 if getattr(self, "__old_on_alias", False): 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true

409 self.on_alias(node=node, alias=alias, agent=agent, **kwargs) 

410 

411 def on_alias(self, *, alias: Alias, loader: GriffeLoader, **kwargs: Any) -> None: 

412 """Run on aliases once the object tree has been fully constructed. 

413 

414 Note: 

415 This method runs once the object tree has been fully constructed: 

416 data is therefore complete and you can safely hook onto this event. 

417 

418 Parameters: 

419 alias: The alias instance. 

420 loader: The loader currently in use. 

421 **kwargs: For forward-compatibility. 

422 """ 

423 

424 def on_package(self, *, pkg: Module, loader: GriffeLoader, **kwargs: Any) -> None: 

425 """Run when a package has been completely loaded. 

426 

427 Note: 

428 This method runs once the object tree has been fully constructed: 

429 data is therefore complete and you can safely hook onto this event. 

430 

431 Parameters: 

432 pkg: The package (Module) instance. 

433 loader: The loader currently in use. 

434 **kwargs: For forward-compatibility. 

435 """ 

436 

437 

438LoadableExtensionType = str | dict[str, Any] | Extension | type[Extension] 

439"""All the types that can be passed to `load_extensions`.""" 

440 

441 

442class Extensions: 

443 """This class helps iterating on extensions that should run at different times.""" 

444 

445 def __init__(self, *extensions: Extension) -> None: 

446 """Initialize the extensions container. 

447 

448 Parameters: 

449 *extensions: The extensions to add. 

450 """ 

451 self._extensions: list[Extension] = [] 

452 self.add(*extensions) 

453 

454 def add(self, *extensions: Extension) -> None: 

455 """Add extensions to this container. 

456 

457 Parameters: 

458 *extensions: The extensions to add. 

459 """ 

460 for extension in extensions: 

461 self._extensions.append(extension) 

462 

463 def _noop(self, **kwargs: Any) -> None: 

464 """No-op method for extension hooks.""" 

465 

466 def call(self, event: str, **kwargs: Any) -> None: 

467 """Call the extension hook for the given event. 

468 

469 Parameters: 

470 event: The triggered event. 

471 **kwargs: Arguments passed to the hook. 

472 """ 

473 for extension in self._extensions: 

474 getattr(extension, event, self._noop)(**kwargs) 

475 

476 

477builtin_extensions: set[str] = { 

478 "dataclasses", 

479 "unpack_typeddict", 

480} 

481"""The names of built-in Griffe extensions.""" 

482 

483 

484def _load_extension_path(path: str) -> ModuleType: 

485 module_name = os.path.basename(path).rsplit(".", 1)[0] # noqa: PTH119 

486 spec = spec_from_file_location(module_name, path) 

487 if not spec: 487 ↛ 488line 487 didn't jump to line 488 because the condition on line 487 was never true

488 raise ExtensionNotLoadedError(f"Could not import module from path '{path}'") 

489 module = module_from_spec(spec) 

490 sys.modules[module_name] = module 

491 spec.loader.exec_module(module) # ty:ignore[possibly-missing-attribute] 

492 return module 

493 

494 

495def _load_extension( 

496 extension: str | dict[str, Any] | Extension | type[Extension], 

497) -> Extension | list[Extension]: 

498 """Load a configured extension. 

499 

500 Parameters: 

501 extension: An extension, with potential configuration options. 

502 

503 Raises: 

504 ExtensionNotLoadedError: When the extension cannot be loaded, 

505 either because the module is not found, or because it does not expose 

506 the Extension attribute. ImportError will bubble up so users can see 

507 the traceback. 

508 

509 Returns: 

510 An extension instance. 

511 """ 

512 ext_object = None 

513 

514 # If it's already an extension instance, return it. 

515 if isinstance(extension, Extension): 

516 return extension 

517 

518 # If it's an extension class, instantiate it (without options) and return it. 

519 if isclass(extension) and issubclass(extension, Extension): 

520 return extension() 

521 

522 # If it's a dictionary, we expect the only key to be an import path 

523 # and the value to be a dictionary of options. 

524 if isinstance(extension, dict): 

525 import_path, options = next(iter(extension.items())) 

526 # Force path to be a string, as it could have been passed from `mkdocs.yml`, 

527 # using the custom YAML tag `!relative`, which gives an instance of MkDocs 

528 # path placeholder classes, which are not iterable. 

529 import_path = str(import_path) 

530 

531 # Otherwise we consider it's an import path, without options. 

532 else: 

533 import_path = str(extension) 

534 options = {} 

535 

536 # If the import path contains a colon, we split into path and class name. 

537 colons = import_path.count(":") 

538 # Special case for The Annoying Operating System. 

539 if colons > 1 or (colons and ":" not in Path(import_path).drive): 

540 import_path, extension_name = import_path.rsplit(":", 1) 

541 else: 

542 extension_name = None 

543 

544 # If the import path corresponds to a built-in extension, expand it. 

545 if import_path in builtin_extensions: 

546 import_path = f"griffe._internal.extensions.{import_path}" 

547 # If the import path is a path to an existing file, load it. 

548 elif os.path.exists(import_path): # noqa: PTH110 

549 try: 

550 ext_object = _load_extension_path(import_path) 

551 except ImportError as error: 

552 raise ExtensionNotLoadedError(f"Extension module '{import_path}' could not be found") from error 

553 

554 # If the extension wasn't loaded yet, we consider the import path 

555 # to be a Python dotted path like `package.module` or `package.module.Extension`. 

556 if not ext_object: 

557 try: 

558 ext_object = dynamic_import(import_path) 

559 except ModuleNotFoundError as error: 

560 raise ExtensionNotLoadedError(f"Extension module '{import_path}' could not be found") from error 

561 except ImportError as error: 

562 raise ExtensionNotLoadedError(f"Error while importing extension '{import_path}': {error}") from error 

563 

564 # If the loaded object is an extension class, instantiate it with options and return it. 

565 if isclass(ext_object) and issubclass(ext_object, Extension): 

566 return ext_object(**options) 

567 

568 # Otherwise the loaded object is a module, so we get the extension class by name, 

569 # instantiate it with options and return it. 

570 if extension_name: 

571 try: 

572 return getattr(ext_object, extension_name)(**options) 

573 except AttributeError as error: 

574 raise ExtensionNotLoadedError( 

575 f"Extension module '{import_path}' has no '{extension_name}' attribute", 

576 ) from error 

577 

578 # No class name was specified so we search all extension classes in the module, 

579 # instantiate each with the same options, and return them. 

580 extensions = [ 

581 obj for obj in vars(ext_object).values() if isclass(obj) and issubclass(obj, Extension) and obj is not Extension 

582 ] 

583 return [ext(**options) for ext in extensions] 

584 

585 

586def load_extensions(*exts: LoadableExtensionType) -> Extensions: 

587 """Load configured extensions. 

588 

589 Parameters: 

590 exts: Extensions with potential configuration options. 

591 

592 Returns: 

593 An extensions container. 

594 """ 

595 extensions = Extensions() 

596 

597 for extension in exts: 

598 ext = _load_extension(extension) 

599 if isinstance(ext, list): 

600 extensions.add(*ext) # ty:ignore[invalid-argument-type] 

601 else: 

602 extensions.add(ext) 

603 

604 # TODO: Deprecate and remove at some point? 

605 # Always add our built-in dataclasses extension. 

606 from griffe._internal.extensions.dataclasses import DataclassesExtension # noqa: PLC0415 

607 

608 for ext in extensions._extensions: 

609 if type(ext) is DataclassesExtension: 

610 break 

611 else: 

612 extensions.add(*_load_extension("dataclasses")) # ty:ignore[not-iterable] 

613 

614 return extensions