Extract Python API signatures and detect breaking changes for documentation generation.
—
Extensible plugin system for customizing and enhancing Griffe's analysis capabilities. Extensions allow developers to add custom processing during code analysis, handle special decorators, implement domain-specific analysis, and extend Griffe's understanding of Python constructs.
Functions for loading and managing extensions from various sources.
def load_extensions(
extensions: list[LoadableExtensionType] | None = None
) -> Extensions:
"""
Load configured extensions from various sources.
Supports loading built-in extensions, installed packages, and custom
extension classes. Extensions are executed during different phases
of the analysis process.
Args:
extensions: List of extension specifications including:
- String names for built-in extensions ("dataclasses")
- Import paths for installed extensions ("package.module:ExtensionClass")
- Extension class instances
- Extension class types
Returns:
Extensions: Container with loaded and configured extensions
Raises:
ExtensionNotLoadedError: If extension cannot be loaded
Examples:
Load built-in extensions:
>>> extensions = griffe.load_extensions(["dataclasses"])
Load custom extensions:
>>> extensions = griffe.load_extensions([
... "dataclasses", # built-in
... "mypackage.extensions:CustomAnalyzer", # installed
... MyExtension(), # instance
... ])
"""
# Type alias for extension specifications
LoadableExtensionType = (
str | # Extension name or import path
type[Extension] | # Extension class
Extension | # Extension instance
dict[str, Any] # Extension configuration
)
# Built-in extensions registry
builtin_extensions: dict[str, type[Extension]]
"""Dictionary of built-in extensions available in Griffe."""Foundation class for creating custom extensions with lifecycle hooks.
class Extension:
"""
Base class for Griffe extensions.
Allows custom processing during code analysis by providing hooks
at different stages of the loading and analysis process.
"""
def __init__(self, **options: Any) -> None:
"""
Initialize the extension.
Args:
**options: Extension-specific configuration options
"""
def on_package_loaded(self, *, pkg: Module) -> None:
"""
Hook called when a package is loaded.
Called after a top-level package (module) has been fully loaded
and all its contents analyzed. Use this for package-level processing.
Args:
pkg: The loaded package module
"""
def on_module_loaded(self, *, mod: Module) -> None:
"""
Hook called when a module is loaded.
Called after a module has been loaded and its contents analyzed.
Includes both top-level packages and submodules.
Args:
mod: The loaded module
"""
def on_class_loaded(self, *, cls: Class) -> None:
"""
Hook called when a class is loaded.
Called after a class definition has been analyzed and all its
methods, attributes, and nested classes have been processed.
Args:
cls: The loaded class
"""
def on_function_loaded(self, *, func: Function) -> None:
"""
Hook called when a function is loaded.
Called after a function or method has been analyzed, including
parameter extraction and signature processing.
Args:
func: The loaded function or method
"""
def on_attribute_loaded(self, *, attr: Attribute) -> None:
"""
Hook called when an attribute is loaded.
Called after an attribute (variable, class attribute, etc.)
has been analyzed and its type and value determined.
Args:
attr: The loaded attribute
"""
def on_alias_loaded(self, *, alias: Alias) -> None:
"""
Hook called when an alias is loaded.
Called after an import alias has been processed and its
target path determined.
Args:
alias: The loaded alias
"""Container class for managing multiple extensions and their execution.
class Extensions:
"""
Container class for managing multiple extensions.
Manages a collection of extensions and provides methods to execute
their hooks at appropriate times during analysis.
"""
def __init__(self, extensions: list[Extension] | None = None) -> None:
"""
Initialize extensions container.
Args:
extensions: List of extension instances
"""
def add(self, extension: Extension) -> None:
"""
Add an extension to the container.
Args:
extension: Extension instance to add
"""
def call(self, method: str, **kwargs: Any) -> None:
"""
Call a method on all extensions.
Args:
method: Method name to call (e.g., "on_class_loaded")
**kwargs: Arguments to pass to the method
"""
def __iter__(self) -> Iterator[Extension]:
"""Iterate over extensions."""
def __len__(self) -> int:
"""Number of extensions."""Built-in extension for enhanced analysis of Python dataclasses.
class DataclassesExtension(Extension):
"""
Built-in extension for handling Python dataclasses.
Provides enhanced analysis of dataclass decorators, field definitions,
and generated methods. Automatically detects dataclass usage and
extracts field information with types and defaults.
"""
def __init__(self, **options: Any) -> None:
"""
Initialize dataclasses extension.
Args:
**options: Configuration options for dataclass analysis
"""
def on_class_loaded(self, *, cls: Class) -> None:
"""
Process dataclass when class is loaded.
Analyzes @dataclass decorators and extracts field information,
adding synthetic attributes for dataclass fields.
"""import griffe
# Load with dataclasses extension
extensions = griffe.load_extensions(["dataclasses"])
loader = griffe.GriffeLoader(extensions=extensions)
# Load a package that uses dataclasses
package = loader.load("mypackage")
# The dataclasses extension will have processed any @dataclass decorators
for class_name, cls in package.classes.items():
if any("dataclass" in str(dec.value) for dec in cls.decorators):
print(f"Dataclass: {class_name}")
print(f" Fields: {list(cls.attributes.keys())}")import griffe
from griffe import Extension, Class, Function
class LoggingExtension(Extension):
"""Extension that logs all loaded objects."""
def __init__(self, log_level="INFO", **options):
super().__init__(**options)
self.log_level = log_level
self.stats = {"modules": 0, "classes": 0, "functions": 0}
def on_module_loaded(self, *, mod):
self.stats["modules"] += 1
print(f"[{self.log_level}] Loaded module: {mod.path}")
def on_class_loaded(self, *, cls):
self.stats["classes"] += 1
print(f"[{self.log_level}] Loaded class: {cls.path}")
# Log inheritance information
if cls.bases:
base_names = [str(base) for base in cls.bases]
print(f" Inherits from: {', '.join(base_names)}")
def on_function_loaded(self, *, func):
self.stats["functions"] += 1
print(f"[{self.log_level}] Loaded function: {func.path}")
# Log parameter information
param_count = len(func.parameters)
print(f" Parameters: {param_count}")
# Use the custom extension
logging_ext = LoggingExtension(log_level="DEBUG")
extensions = griffe.Extensions([logging_ext])
loader = griffe.GriffeLoader(extensions=extensions)
package = loader.load("requests")
print("Final stats:", logging_ext.stats)import griffe
from griffe import Extension, Function, Class
class FastAPIExtension(Extension):
"""Extension for analyzing FastAPI applications."""
def __init__(self, **options):
super().__init__(**options)
self.routes = []
self.dependencies = []
self.middleware = []
def on_function_loaded(self, *, func: Function):
"""Analyze FastAPI route decorators."""
for decorator in func.decorators:
decorator_str = str(decorator.value)
# Check for route decorators
if any(method in decorator_str for method in ["get", "post", "put", "delete", "patch"]):
route_info = {
"function": func.path,
"decorator": decorator_str,
"parameters": [p.name for p in func.parameters],
"returns": str(func.returns) if func.returns else None
}
self.routes.append(route_info)
print(f"Found route: {func.name} -> {decorator_str}")
# Check for dependency injection
elif "Depends" in decorator_str:
self.dependencies.append({
"function": func.path,
"dependency": decorator_str
})
def on_class_loaded(self, *, cls: Class):
"""Analyze FastAPI middleware classes."""
for decorator in cls.decorators:
if "middleware" in str(decorator.value).lower():
self.middleware.append({
"class": cls.path,
"decorator": str(decorator.value)
})
def get_api_summary(self):
"""Get summary of FastAPI application structure."""
return {
"routes": len(self.routes),
"dependencies": len(self.dependencies),
"middleware": len(self.middleware),
"route_details": self.routes
}
# Use FastAPI extension
fastapi_ext = FastAPIExtension()
extensions = griffe.Extensions([fastapi_ext])
loader = griffe.GriffeLoader(extensions=extensions)
app_module = loader.load("myapp")
# Get API analysis results
summary = fastapi_ext.get_api_summary()
print(f"FastAPI Analysis: {summary['routes']} routes, {summary['dependencies']} dependencies")import griffe
from griffe import Extension, Function, Attribute
import ast
class TypeAnnotationExtension(Extension):
"""Extension for enhanced type annotation analysis."""
def __init__(self, **options):
super().__init__(**options)
self.type_stats = {
"annotated_functions": 0,
"unannotated_functions": 0,
"annotated_attributes": 0,
"complex_types": 0
}
def on_function_loaded(self, *, func: Function):
"""Analyze function type annotations."""
if func.returns or any(p.annotation for p in func.parameters):
self.type_stats["annotated_functions"] += 1
# Check for complex return types
if func.returns:
return_str = str(func.returns)
if any(keyword in return_str for keyword in ["Union", "Optional", "Generic", "TypeVar"]):
self.type_stats["complex_types"] += 1
print(f"Complex return type in {func.name}: {return_str}")
else:
self.type_stats["unannotated_functions"] += 1
def on_attribute_loaded(self, *, attr: Attribute):
"""Analyze attribute type annotations."""
if attr.annotation:
self.type_stats["annotated_attributes"] += 1
# Check for complex attribute types
attr_str = str(attr.annotation)
if any(keyword in attr_str for keyword in ["Union", "Optional", "List", "Dict"]):
print(f"Complex attribute type: {attr.path}: {attr_str}")
def get_type_coverage(self):
"""Calculate type annotation coverage."""
total_functions = self.type_stats["annotated_functions"] + self.type_stats["unannotated_functions"]
if total_functions > 0:
coverage = (self.type_stats["annotated_functions"] / total_functions) * 100
return {
"function_coverage": round(coverage, 2),
"annotated_functions": self.type_stats["annotated_functions"],
"total_functions": total_functions,
"complex_types": self.type_stats["complex_types"],
"annotated_attributes": self.type_stats["annotated_attributes"]
}
return {"function_coverage": 0, "total_functions": 0}
# Use type annotation extension
type_ext = TypeAnnotationExtension()
extensions = griffe.Extensions([type_ext])
loader = griffe.GriffeLoader(extensions=extensions)
package = loader.load("mypackage")
coverage = type_ext.get_type_coverage()
print(f"Type annotation coverage: {coverage['function_coverage']}%")
print(f"Complex types found: {coverage['complex_types']}")import griffe
from griffe import Extension
class ConfigurableExtension(Extension):
"""Extension with comprehensive configuration options."""
def __init__(
self,
track_private=False,
ignore_patterns=None,
output_file=None,
**options
):
super().__init__(**options)
self.track_private = track_private
self.ignore_patterns = ignore_patterns or []
self.output_file = output_file
self.data = []
def should_process(self, obj_path: str) -> bool:
"""Check if object should be processed based on configuration."""
# Skip private objects if not tracking them
if not self.track_private and any(part.startswith("_") for part in obj_path.split(".")):
return False
# Skip ignored patterns
if any(pattern in obj_path for pattern in self.ignore_patterns):
return False
return True
def on_function_loaded(self, *, func):
if self.should_process(func.path):
self.data.append({
"type": "function",
"path": func.path,
"parameters": len(func.parameters),
"has_docstring": func.docstring is not None
})
def on_class_loaded(self, *, cls):
if self.should_process(cls.path):
self.data.append({
"type": "class",
"path": cls.path,
"methods": len(cls.methods),
"attributes": len(cls.attributes),
"bases": len(cls.bases)
})
def finalize(self):
"""Called after all loading is complete."""
if self.output_file:
import json
with open(self.output_file, "w") as f:
json.dump(self.data, f, indent=2)
print(f"Extension data written to {self.output_file}")
# Configure and use extension
ext = ConfigurableExtension(
track_private=False,
ignore_patterns=["test_", "_internal"],
output_file="analysis_results.json"
)
extensions = griffe.Extensions([ext])
loader = griffe.GriffeLoader(extensions=extensions)
package = loader.load("mypackage")
# Finalize extension processing
ext.finalize()import griffe
from griffe import Extension
class MultiPhaseExtension(Extension):
"""Extension that processes objects in multiple phases."""
def __init__(self, **options):
super().__init__(**options)
self.phase = "collection"
self.collected_objects = {"classes": [], "functions": []}
self.relationships = []
def on_class_loaded(self, *, cls):
if self.phase == "collection":
self.collected_objects["classes"].append(cls)
def on_function_loaded(self, *, func):
if self.phase == "collection":
self.collected_objects["functions"].append(func)
def on_package_loaded(self, *, pkg):
"""Switch to relationship analysis phase."""
if self.phase == "collection":
self.phase = "analysis"
self._analyze_relationships()
def _analyze_relationships(self):
"""Analyze relationships between collected objects."""
for cls in self.collected_objects["classes"]:
# Check inheritance relationships
for base in cls.bases:
base_name = str(base)
matching_classes = [
c for c in self.collected_objects["classes"]
if c.name == base_name.split(".")[-1]
]
if matching_classes:
self.relationships.append({
"type": "inheritance",
"child": cls.path,
"parent": matching_classes[0].path
})
print(f"Found {len(self.relationships)} relationships")
# Use multi-phase extension
multi_ext = MultiPhaseExtension()
extensions = griffe.Extensions([multi_ext])
loader = griffe.GriffeLoader(extensions=extensions)
package = loader.load("mypackage")
print("Relationships:", multi_ext.relationships)from typing import Any, Iterator, Union
# Extension type specifications
LoadableExtensionType = Union[
str, # Extension name or import path
type[Extension], # Extension class
Extension, # Extension instance
dict[str, Any] # Extension configuration
]
# Core extension classes
class Extension: ...
class Extensions: ...
# Built-in extensions registry
builtin_extensions: dict[str, type[Extension]]
# Built-in extension classes
class DataclassesExtension(Extension): ...
# Core object types from models
from griffe import Module, Class, Function, Attribute, AliasInstall with Tessl CLI
npx tessl i tessl/pypi-griffe