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
« 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.
4from __future__ import annotations
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
13from griffe._internal.agents.nodes.ast import ast_children, ast_kind
14from griffe._internal.exceptions import ExtensionNotLoadedError
15from griffe._internal.importer import dynamic_import
17if TYPE_CHECKING:
18 import ast
19 from types import ModuleType
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
28class Extension:
29 """Base class for Griffe extensions."""
31 def visit(self, node: ast.AST) -> None:
32 """Visit a node.
34 Parameters:
35 node: The node to visit.
36 """
37 getattr(self, f"visit_{ast_kind(node)}", lambda _: None)(node)
39 def generic_visit(self, node: ast.AST) -> None:
40 """Visit children nodes.
42 Parameters:
43 node: The node to visit the children of.
44 """
45 for child in ast_children(node):
46 self.visit(child)
48 def inspect(self, node: ObjectNode) -> None:
49 """Inspect a node.
51 Parameters:
52 node: The node to inspect.
53 """
54 getattr(self, f"inspect_{node.kind}", lambda _: None)(node)
56 def generic_inspect(self, node: ObjectNode) -> None:
57 """Extend the base generic inspection with extensions.
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)
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.
69 Parameters:
70 node: The currently visited node.
71 """
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.
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.
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 """
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.
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.
102 Parameters:
103 obj: The object instance.
104 loader: The loader currently in use.
105 **kwargs: For forward-compatibility.
106 """
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.
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.
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 """
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.
126 Parameters:
127 node: The currently visited node.
128 agent: The analysis agent currently running.
129 **kwargs: For forward-compatibility.
130 """
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.
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.
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 """
154 def on_module(self, *, mod: Module, loader: GriffeLoader, **kwargs: Any) -> None:
155 """Run on modules once the object tree has been fully constructed.
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.
161 Parameters:
162 mod: The module instance.
163 loader: The loader currently in use.
164 **kwargs: For forward-compatibility.
165 """
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.
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.
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 """
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.
192 Parameters:
193 node: The currently visited node.
194 agent: The analysis agent currently running.
195 **kwargs: For forward-compatibility.
196 """
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.
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.
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 """
220 def on_class(self, *, cls: Class, loader: GriffeLoader, **kwargs: Any) -> None:
221 """Run on classes once the object tree has been fully constructed.
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.
227 Parameters:
228 cls: The class instance.
229 loader: The loader currently in use.
230 **kwargs: For forward-compatibility.
231 """
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.
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.
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 """
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.
258 Parameters:
259 node: The currently visited node.
260 agent: The analysis agent currently running.
261 **kwargs: For forward-compatibility.
262 """
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.
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.
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 """
286 def on_function(self, *, func: Function, loader: GriffeLoader, **kwargs: Any) -> None:
287 """Run on functions once the object tree has been fully constructed.
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.
293 Parameters:
294 func: The function instance.
295 loader: The loader currently in use.
296 **kwargs: For forward-compatibility.
297 """
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.
302 Parameters:
303 node: The currently visited node.
304 agent: The analysis agent currently running.
305 **kwargs: For forward-compatibility.
306 """
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.
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.
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 """
330 def on_attribute(self, *, attr: Attribute, loader: GriffeLoader, **kwargs: Any) -> None:
331 """Run on attributes once the object tree has been fully constructed.
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.
337 Parameters:
338 attr: The attribute instance.
339 loader: The loader currently in use.
340 **kwargs: For forward-compatibility.
341 """
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.
346 Parameters:
347 node: The currently visited node.
348 agent: The analysis agent currently running.
349 **kwargs: For forward-compatibility.
350 """
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.
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.
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 """
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.
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.
381 Parameters:
382 type_alias: The type alias instance.
383 loader: The loader currently in use.
384 **kwargs: For forward-compatibility.
385 """
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.
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.
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)
411 def on_alias(self, *, alias: Alias, loader: GriffeLoader, **kwargs: Any) -> None:
412 """Run on aliases once the object tree has been fully constructed.
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.
418 Parameters:
419 alias: The alias instance.
420 loader: The loader currently in use.
421 **kwargs: For forward-compatibility.
422 """
424 def on_package(self, *, pkg: Module, loader: GriffeLoader, **kwargs: Any) -> None:
425 """Run when a package has been completely loaded.
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.
431 Parameters:
432 pkg: The package (Module) instance.
433 loader: The loader currently in use.
434 **kwargs: For forward-compatibility.
435 """
438LoadableExtensionType = str | dict[str, Any] | Extension | type[Extension]
439"""All the types that can be passed to `load_extensions`."""
442class Extensions:
443 """This class helps iterating on extensions that should run at different times."""
445 def __init__(self, *extensions: Extension) -> None:
446 """Initialize the extensions container.
448 Parameters:
449 *extensions: The extensions to add.
450 """
451 self._extensions: list[Extension] = []
452 self.add(*extensions)
454 def add(self, *extensions: Extension) -> None:
455 """Add extensions to this container.
457 Parameters:
458 *extensions: The extensions to add.
459 """
460 for extension in extensions:
461 self._extensions.append(extension)
463 def _noop(self, **kwargs: Any) -> None:
464 """No-op method for extension hooks."""
466 def call(self, event: str, **kwargs: Any) -> None:
467 """Call the extension hook for the given event.
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)
477builtin_extensions: set[str] = {
478 "dataclasses",
479 "unpack_typeddict",
480}
481"""The names of built-in Griffe extensions."""
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
495def _load_extension(
496 extension: str | dict[str, Any] | Extension | type[Extension],
497) -> Extension | list[Extension]:
498 """Load a configured extension.
500 Parameters:
501 extension: An extension, with potential configuration options.
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.
509 Returns:
510 An extension instance.
511 """
512 ext_object = None
514 # If it's already an extension instance, return it.
515 if isinstance(extension, Extension):
516 return extension
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()
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)
531 # Otherwise we consider it's an import path, without options.
532 else:
533 import_path = str(extension)
534 options = {}
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
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
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
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)
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
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]
586def load_extensions(*exts: LoadableExtensionType) -> Extensions:
587 """Load configured extensions.
589 Parameters:
590 exts: Extensions with potential configuration options.
592 Returns:
593 An extensions container.
594 """
595 extensions = Extensions()
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)
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
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]
614 return extensions