Coverage for src/griffe/_internal/extensions/base.py: 83.55%

114 statements  

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

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, Union 

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 Parameters: 

84 node: The currently visited node. 

85 obj: The object instance. 

86 agent: The analysis agent currently running. 

87 **kwargs: For forward-compatibility. 

88 """ 

89 

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

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

92 

93 Parameters: 

94 node: The currently visited node. 

95 obj: The object instance. 

96 agent: The analysis agent currently running. 

97 **kwargs: For forward-compatibility. 

98 """ 

99 

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

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

102 

103 Parameters: 

104 node: The currently visited node. 

105 agent: The analysis agent currently running. 

106 **kwargs: For forward-compatibility. 

107 """ 

108 

109 def on_module_instance( 

110 self, 

111 *, 

112 node: ast.AST | ObjectNode, 

113 mod: Module, 

114 agent: Visitor | Inspector, 

115 **kwargs: Any, 

116 ) -> None: 

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

118 

119 Parameters: 

120 node: The currently visited node. 

121 mod: The module instance. 

122 agent: The analysis agent currently running. 

123 **kwargs: For forward-compatibility. 

124 """ 

125 

126 def on_module_members( 

127 self, 

128 *, 

129 node: ast.AST | ObjectNode, 

130 mod: Module, 

131 agent: Visitor | Inspector, 

132 **kwargs: Any, 

133 ) -> None: 

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

135 

136 Parameters: 

137 node: The currently visited node. 

138 mod: The module instance. 

139 agent: The analysis agent currently running. 

140 **kwargs: For forward-compatibility. 

141 """ 

142 

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

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

145 

146 Parameters: 

147 node: The currently visited node. 

148 agent: The analysis agent currently running. 

149 **kwargs: For forward-compatibility. 

150 """ 

151 

152 def on_class_instance( 

153 self, 

154 *, 

155 node: ast.AST | ObjectNode, 

156 cls: Class, 

157 agent: Visitor | Inspector, 

158 **kwargs: Any, 

159 ) -> None: 

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

161 

162 Parameters: 

163 node: The currently visited node. 

164 cls: The class instance. 

165 agent: The analysis agent currently running. 

166 **kwargs: For forward-compatibility. 

167 """ 

168 

169 def on_class_members( 

170 self, 

171 *, 

172 node: ast.AST | ObjectNode, 

173 cls: Class, 

174 agent: Visitor | Inspector, 

175 **kwargs: Any, 

176 ) -> None: 

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

178 

179 Parameters: 

180 node: The currently visited node. 

181 cls: The class instance. 

182 agent: The analysis agent currently running. 

183 **kwargs: For forward-compatibility. 

184 """ 

185 

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

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

188 

189 Parameters: 

190 node: The currently visited node. 

191 agent: The analysis agent currently running. 

192 **kwargs: For forward-compatibility. 

193 """ 

194 

195 def on_function_instance( 

196 self, 

197 *, 

198 node: ast.AST | ObjectNode, 

199 func: Function, 

200 agent: Visitor | Inspector, 

201 **kwargs: Any, 

202 ) -> None: 

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

204 

205 Parameters: 

206 node: The currently visited node. 

207 func: The function instance. 

208 agent: The analysis agent currently running. 

209 **kwargs: For forward-compatibility. 

210 """ 

211 

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

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

214 

215 Parameters: 

216 node: The currently visited node. 

217 agent: The analysis agent currently running. 

218 **kwargs: For forward-compatibility. 

219 """ 

220 

221 def on_attribute_instance( 

222 self, 

223 *, 

224 node: ast.AST | ObjectNode, 

225 attr: Attribute, 

226 agent: Visitor | Inspector, 

227 **kwargs: Any, 

228 ) -> None: 

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

230 

231 Parameters: 

232 node: The currently visited node. 

233 attr: The attribute instance. 

234 agent: The analysis agent currently running. 

235 **kwargs: For forward-compatibility. 

236 """ 

237 

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

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

240 

241 Parameters: 

242 node: The currently visited node. 

243 agent: The analysis agent currently running. 

244 **kwargs: For forward-compatibility. 

245 """ 

246 

247 def on_type_alias_instance( 

248 self, 

249 *, 

250 node: ast.AST | ObjectNode, 

251 type_alias: TypeAlias, 

252 agent: Visitor | Inspector, 

253 **kwargs: Any, 

254 ) -> None: 

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

256 

257 Parameters: 

258 node: The currently visited node. 

259 type_alias: The type alias instance. 

260 agent: The analysis agent currently running. 

261 **kwargs: For forward-compatibility. 

262 """ 

263 

264 def on_alias( 

265 self, 

266 *, 

267 node: ast.AST | ObjectNode, 

268 alias: Alias, 

269 agent: Visitor | Inspector, 

270 **kwargs: Any, 

271 ) -> None: 

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

273 

274 Parameters: 

275 node: The currently visited node. 

276 alias: The alias instance. 

277 agent: The analysis agent currently running. 

278 **kwargs: For forward-compatibility. 

279 """ 

280 

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

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

283 

284 Parameters: 

285 pkg: The package (Module) instance. 

286 loader: The loader currently in use. 

287 **kwargs: For forward-compatibility. 

288 """ 

289 

290 def on_wildcard_expansion( 

291 self, 

292 *, 

293 alias: Alias, 

294 loader: GriffeLoader, 

295 **kwargs: Any, 

296 ) -> None: 

297 """Run when wildcard imports are expanded into aliases. 

298 

299 Parameters: 

300 alias: The alias instance. 

301 loader: The loader currently in use. 

302 **kwargs: For forward-compatibility. 

303 """ 

304 

305 

306LoadableExtensionType = Union[str, dict[str, Any], Extension, type[Extension]] 

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

308 

309 

310class Extensions: 

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

312 

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

314 """Initialize the extensions container. 

315 

316 Parameters: 

317 *extensions: The extensions to add. 

318 """ 

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

320 self.add(*extensions) 

321 

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

323 """Add extensions to this container. 

324 

325 Parameters: 

326 *extensions: The extensions to add. 

327 """ 

328 for extension in extensions: 

329 self._extensions.append(extension) 

330 

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

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

333 

334 Parameters: 

335 event: The triggered event. 

336 **kwargs: Arguments passed to the hook. 

337 """ 

338 for extension in self._extensions: 

339 getattr(extension, event)(**kwargs) 

340 

341 

342builtin_extensions: set[str] = { 

343 "dataclasses", 

344} 

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

346 

347 

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

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

350 spec = spec_from_file_location(module_name, path) 

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

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

353 module = module_from_spec(spec) 

354 sys.modules[module_name] = module 

355 spec.loader.exec_module(module) # type: ignore[union-attr] 

356 return module 

357 

358 

359def _load_extension( 

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

361) -> Extension | list[Extension]: 

362 """Load a configured extension. 

363 

364 Parameters: 

365 extension: An extension, with potential configuration options. 

366 

367 Raises: 

368 ExtensionNotLoadedError: When the extension cannot be loaded, 

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

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

371 the traceback. 

372 

373 Returns: 

374 An extension instance. 

375 """ 

376 ext_object = None 

377 

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

379 if isinstance(extension, Extension): 

380 return extension 

381 

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

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

384 return extension() 

385 

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

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

388 if isinstance(extension, dict): 

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

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

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

392 # path placeholder classes, which are not iterable. 

393 import_path = str(import_path) 

394 

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

396 else: 

397 import_path = str(extension) 

398 options = {} 

399 

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

401 colons = import_path.count(":") 

402 # Special case for The Annoying Operating System. 

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

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

405 else: 

406 extension_name = None 

407 

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

409 if import_path in builtin_extensions: 

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

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

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

413 try: 

414 ext_object = _load_extension_path(import_path) 

415 except ImportError as error: 

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

417 

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

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

420 if not ext_object: 

421 try: 

422 ext_object = dynamic_import(import_path) 

423 except ModuleNotFoundError as error: 

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

425 except ImportError as error: 

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

427 

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

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

430 return ext_object(**options) 

431 

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

433 # instantiate it with options and return it. 

434 if extension_name: 

435 try: 

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

437 except AttributeError as error: 

438 raise ExtensionNotLoadedError( 

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

440 ) from error 

441 

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

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

444 extensions = [ 

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

446 ] 

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

448 

449 

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

451 """Load configured extensions. 

452 

453 Parameters: 

454 exts: Extensions with potential configuration options. 

455 

456 Returns: 

457 An extensions container. 

458 """ 

459 extensions = Extensions() 

460 

461 for extension in exts: 

462 ext = _load_extension(extension) 

463 if isinstance(ext, list): 

464 extensions.add(*ext) 

465 else: 

466 extensions.add(ext) 

467 

468 # TODO: Deprecate and remove at some point? 

469 # Always add our built-in dataclasses extension. 

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

471 

472 for ext in extensions._extensions: 

473 if type(ext) is DataclassesExtension: 473 ↛ 474line 473 didn't jump to line 474 because the condition on line 473 was never true

474 break 

475 else: 

476 extensions.add(*_load_extension("dataclasses")) # type: ignore[misc] 

477 

478 return extensions