Add stable Python API and comprehensive environment management
Some checks failed
Build Deb-Mock Package / build (push) Failing after 59s
Lint Code / Lint All Code (push) Failing after 2s
Test Deb-Mock Build / test (push) Failing after 41s

- Add MockAPIClient and MockEnvironment for external integration
- Implement EnvironmentManager with full lifecycle support
- Enhance plugin system with registry and BasePlugin class
- Add comprehensive test suite and documentation
- Include practical usage examples and plugin development guide
This commit is contained in:
robojerk 2025-09-04 10:04:16 -07:00
parent c51819c836
commit 8c585e2e33
9 changed files with 3413 additions and 1267 deletions

View file

@ -0,0 +1,234 @@
"""
Example plugin for deb-mock
This plugin demonstrates how to create custom plugins for deb-mock
and provides examples of common plugin patterns.
"""
import os
import logging
from typing import Dict, Any, List
from ..plugin import BasePlugin, HookStages
class ExamplePlugin(BasePlugin):
"""
Example plugin demonstrating deb-mock plugin capabilities
This plugin shows how to:
- Register hooks for different stages
- Access configuration and deb-mock context
- Perform custom operations during build lifecycle
- Log information and errors
"""
# Plugin metadata
requires_api_version = "1.0"
plugin_name = "example"
plugin_version = "1.0.0"
plugin_description = "Example plugin for deb-mock"
def __init__(self, plugin_manager, config, deb_mock):
super().__init__(plugin_manager, config, deb_mock)
# Plugin-specific configuration
self.enabled = self.get_config('enabled', True)
self.log_level = self.get_config('log_level', 'INFO')
self.custom_option = self.get_config('custom_option', 'default_value')
# Setup logging
self.logger.setLevel(getattr(logging, self.log_level.upper()))
self.log_info(f"ExamplePlugin initialized with config: {config}")
def _register_hooks(self):
"""Register hooks for different build stages"""
# Chroot lifecycle hooks
self.plugin_manager.add_hook(HookStages.PRECHROOT_INIT, self.prechroot_init)
self.plugin_manager.add_hook(HookStages.POSTCHROOT_INIT, self.postchroot_init)
self.plugin_manager.add_hook(HookStages.PRECHROOT_CLEAN, self.prechroot_clean)
self.plugin_manager.add_hook(HookStages.POSTCHROOT_CLEAN, self.postchroot_clean)
# Build lifecycle hooks
self.plugin_manager.add_hook(HookStages.PREBUILD, self.prebuild)
self.plugin_manager.add_hook(HookStages.POSTBUILD, self.postbuild)
self.plugin_manager.add_hook(HookStages.BUILD_START, self.build_start)
self.plugin_manager.add_hook(HookStages.BUILD_END, self.build_end)
# Package management hooks
self.plugin_manager.add_hook(HookStages.PRE_INSTALL_DEPS, self.pre_install_deps)
self.plugin_manager.add_hook(HookStages.POST_INSTALL_DEPS, self.post_install_deps)
# Mount management hooks
self.plugin_manager.add_hook(HookStages.PRE_MOUNT, self.pre_mount)
self.plugin_manager.add_hook(HookStages.POST_MOUNT, self.post_mount)
# Cache management hooks
self.plugin_manager.add_hook(HookStages.PRE_CACHE_CREATE, self.pre_cache_create)
self.plugin_manager.add_hook(HookStages.POST_CACHE_CREATE, self.post_cache_create)
# Error handling hooks
self.plugin_manager.add_hook(HookStages.ON_ERROR, self.on_error)
self.plugin_manager.add_hook(HookStages.ON_WARNING, self.on_warning)
self.log_debug("Registered all hooks for ExamplePlugin")
def prechroot_init(self, chroot_name: str, **kwargs):
"""Called before chroot initialization"""
self.log_info(f"Pre-chroot init for {chroot_name}")
# Example: Create custom directory structure
if self.get_config('create_custom_dirs', False):
custom_dirs = self.get_config('custom_dirs', ['/build/custom'])
for dir_path in custom_dirs:
self.log_debug(f"Would create directory: {dir_path}")
def postchroot_init(self, chroot_name: str, **kwargs):
"""Called after chroot initialization"""
self.log_info(f"Post-chroot init for {chroot_name}")
# Example: Install additional packages
extra_packages = self.get_config('extra_packages', [])
if extra_packages:
self.log_info(f"Installing extra packages: {extra_packages}")
try:
result = self.deb_mock.install_packages(extra_packages)
if result.get('success', False):
self.log_info("Extra packages installed successfully")
else:
self.log_warning(f"Failed to install extra packages: {result}")
except Exception as e:
self.log_error(f"Error installing extra packages: {e}")
def prechroot_clean(self, chroot_name: str, **kwargs):
"""Called before chroot cleanup"""
self.log_info(f"Pre-chroot clean for {chroot_name}")
# Example: Backup important files before cleanup
if self.get_config('backup_before_clean', False):
self.log_info("Backing up important files before cleanup")
# Implementation would backup files here
def postchroot_clean(self, chroot_name: str, **kwargs):
"""Called after chroot cleanup"""
self.log_info(f"Post-chroot clean for {chroot_name}")
def prebuild(self, source_package: str, **kwargs):
"""Called before package build"""
self.log_info(f"Pre-build for {source_package}")
# Example: Validate source package
if not os.path.exists(source_package):
self.log_error(f"Source package not found: {source_package}")
raise FileNotFoundError(f"Source package not found: {source_package}")
# Example: Check build dependencies
if self.get_config('check_deps', True):
self.log_info("Checking build dependencies")
# Implementation would check dependencies here
def postbuild(self, build_result: Dict[str, Any], source_package: str, **kwargs):
"""Called after package build"""
self.log_info(f"Post-build for {source_package}")
success = build_result.get('success', False)
if success:
self.log_info("Build completed successfully")
artifacts = build_result.get('artifacts', [])
self.log_info(f"Generated {len(artifacts)} artifacts")
else:
self.log_error("Build failed")
def build_start(self, source_package: str, chroot_name: str, **kwargs):
"""Called when build starts"""
self.log_info(f"Build started: {source_package} in {chroot_name}")
# Example: Set build environment variables
if self.get_config('set_build_env', False):
env_vars = self.get_config('build_env_vars', {})
for key, value in env_vars.items():
os.environ[key] = value
self.log_debug(f"Set environment variable: {key}={value}")
def build_end(self, build_result: Dict[str, Any], source_package: str, chroot_name: str, **kwargs):
"""Called when build ends"""
self.log_info(f"Build ended: {source_package} in {chroot_name}")
# Example: Collect build statistics
if self.get_config('collect_stats', True):
stats = {
'package': source_package,
'chroot': chroot_name,
'success': build_result.get('success', False),
'artifacts_count': len(build_result.get('artifacts', [])),
'duration': build_result.get('duration', 0)
}
self.log_info(f"Build statistics: {stats}")
def pre_install_deps(self, dependencies: List[str], chroot_name: str, **kwargs):
"""Called before installing dependencies"""
self.log_info(f"Pre-install deps: {dependencies} in {chroot_name}")
# Example: Filter dependencies
if self.get_config('filter_deps', False):
filtered_deps = [dep for dep in dependencies if not dep.startswith('lib')]
self.log_info(f"Filtered dependencies: {filtered_deps}")
return filtered_deps
def post_install_deps(self, dependencies: List[str], chroot_name: str, **kwargs):
"""Called after installing dependencies"""
self.log_info(f"Post-install deps: {dependencies} in {chroot_name}")
def pre_mount(self, mount_type: str, mount_path: str, chroot_name: str, **kwargs):
"""Called before mounting"""
self.log_debug(f"Pre-mount: {mount_type} at {mount_path} in {chroot_name}")
def post_mount(self, mount_type: str, mount_path: str, chroot_name: str, **kwargs):
"""Called after mounting"""
self.log_debug(f"Post-mount: {mount_type} at {mount_path} in {chroot_name}")
def pre_cache_create(self, cache_path: str, chroot_name: str, **kwargs):
"""Called before creating cache"""
self.log_info(f"Pre-cache create: {cache_path} for {chroot_name}")
def post_cache_create(self, cache_path: str, chroot_name: str, **kwargs):
"""Called after creating cache"""
self.log_info(f"Post-cache create: {cache_path} for {chroot_name}")
def on_error(self, error: Exception, stage: str, **kwargs):
"""Called when an error occurs"""
self.log_error(f"Error in {stage}: {error}")
# Example: Send error notification
if self.get_config('notify_on_error', False):
self.log_info("Would send error notification")
def on_warning(self, warning: str, stage: str, **kwargs):
"""Called when a warning occurs"""
self.log_warning(f"Warning in {stage}: {warning}")
def get_plugin_info(self) -> Dict[str, Any]:
"""Return plugin information"""
return {
'name': self.plugin_name,
'version': self.plugin_version,
'description': self.plugin_description,
'enabled': self.enabled,
'config': {
'log_level': self.log_level,
'custom_option': self.custom_option
}
}
# Plugin initialization function (required by deb-mock)
def init(plugin_manager, config, deb_mock):
"""
Initialize the plugin
This function is called by deb-mock when the plugin is loaded.
It should create and return an instance of the plugin class.
"""
return ExamplePlugin(plugin_manager, config, deb_mock)

View file

@ -1,361 +1,413 @@
"""
Plugin Registry for Deb-Mock Plugin System
Plugin registry and management for deb-mock
This module provides the plugin registration and management functionality
for the Deb-Mock plugin system, inspired by Fedora's Mock plugin architecture.
This module provides a centralized registry for managing deb-mock plugins,
including discovery, loading, and lifecycle management.
"""
import os
import sys
import importlib
import importlib.util
import logging
from typing import Any, Dict, Optional, Type
from pathlib import Path
from typing import Dict, List, Any, Optional, Type, Callable
from dataclasses import dataclass
from datetime import datetime
from .base import BasePlugin
from ..exceptions import PluginError
logger = logging.getLogger(__name__)
@dataclass
class PluginInfo:
"""Information about a registered plugin"""
name: str
version: str
description: str
author: str
requires_api_version: str
plugin_class: Type[BasePlugin]
init_function: Callable
file_path: str
loaded_at: datetime
enabled: bool = True
config: Dict[str, Any] = None
class PluginRegistry:
"""
Manages plugin registration and instantiation.
This class provides the functionality for registering plugin classes
and creating plugin instances, following Mock's plugin system pattern.
Central registry for deb-mock plugins
This class manages plugin discovery, loading, and lifecycle.
"""
def __init__(self):
"""Initialize the plugin registry."""
self.plugins: Dict[str, Type[BasePlugin]] = {}
self.plugin_metadata: Dict[str, Dict[str, Any]] = {}
# Auto-register built-in plugins
self._register_builtin_plugins()
def register(
self,
plugin_name: str,
plugin_class: Type[BasePlugin],
metadata: Optional[Dict[str, Any]] = None,
) -> None:
def __init__(self, plugin_dirs: List[str] = None):
"""
Register a plugin class.
Initialize the plugin registry
Args:
plugin_name: Name of the plugin
plugin_class: Plugin class to register
metadata: Optional metadata about the plugin
Raises:
ValueError: If plugin_name is already registered
TypeError: If plugin_class is not a subclass of BasePlugin
plugin_dirs: List of directories to search for plugins
"""
if not issubclass(plugin_class, BasePlugin):
raise TypeError("Plugin class must inherit from BasePlugin")
if plugin_name in self.plugins:
raise ValueError(f"Plugin '{plugin_name}' is already registered")
self.plugins[plugin_name] = plugin_class
self.plugin_metadata[plugin_name] = metadata or {}
logger.debug(f"Registered plugin '{plugin_name}' with class {plugin_class.__name__}")
def unregister(self, plugin_name: str) -> bool:
self.logger = logging.getLogger(__name__)
# Default plugin directories
self.plugin_dirs = plugin_dirs or [
'/usr/share/deb-mock/plugins',
'/usr/local/share/deb-mock/plugins',
os.path.join(os.path.expanduser('~'), '.local', 'share', 'deb-mock', 'plugins'),
os.path.join(os.getcwd(), 'plugins')
]
# Plugin storage
self._plugins: Dict[str, PluginInfo] = {}
self._loaded_plugins: Dict[str, BasePlugin] = {}
# API version compatibility
self.current_api_version = "1.0"
self.min_api_version = "1.0"
self.max_api_version = "1.0"
def discover_plugins(self) -> List[PluginInfo]:
"""
Unregister a plugin.
Discover available plugins in plugin directories
Returns:
List of discovered plugin information
"""
discovered = []
for plugin_dir in self.plugin_dirs:
if not os.path.exists(plugin_dir):
continue
self.logger.debug(f"Scanning plugin directory: {plugin_dir}")
for file_path in Path(plugin_dir).glob("*.py"):
if file_path.name.startswith('_'):
continue
try:
plugin_info = self._load_plugin_info(file_path)
if plugin_info:
discovered.append(plugin_info)
self.logger.debug(f"Discovered plugin: {plugin_info.name}")
except Exception as e:
self.logger.warning(f"Failed to load plugin from {file_path}: {e}")
return discovered
def _load_plugin_info(self, file_path: Path) -> Optional[PluginInfo]:
"""
Load plugin information from a file
Args:
file_path: Path to the plugin file
Returns:
PluginInfo object or None if not a valid plugin
"""
try:
# Load module
spec = importlib.util.spec_from_file_location(file_path.stem, file_path)
if not spec or not spec.loader:
return None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Check if it's a valid plugin
if not hasattr(module, 'init'):
return None
# Get plugin metadata
plugin_name = getattr(module, 'plugin_name', file_path.stem)
plugin_version = getattr(module, 'plugin_version', '1.0.0')
plugin_description = getattr(module, 'plugin_description', 'No description')
plugin_author = getattr(module, 'plugin_author', 'Unknown')
requires_api_version = getattr(module, 'requires_api_version', '1.0')
# Check API version compatibility
if not self._is_api_version_compatible(requires_api_version):
self.logger.warning(
f"Plugin {plugin_name} requires API version {requires_api_version}, "
f"but current version is {self.current_api_version}"
)
return None
# Get plugin class
plugin_class = getattr(module, 'Plugin', None)
if not plugin_class:
# Look for classes that inherit from BasePlugin
for attr_name in dir(module):
attr = getattr(module, attr_name)
if (isinstance(attr, type) and
issubclass(attr, BasePlugin) and
attr != BasePlugin):
plugin_class = attr
break
if not plugin_class:
return None
return PluginInfo(
name=plugin_name,
version=plugin_version,
description=plugin_description,
author=plugin_author,
requires_api_version=requires_api_version,
plugin_class=plugin_class,
init_function=module.init,
file_path=str(file_path),
loaded_at=datetime.now(),
enabled=True
)
except Exception as e:
self.logger.error(f"Error loading plugin info from {file_path}: {e}")
return None
def _is_api_version_compatible(self, required_version: str) -> bool:
"""
Check if a plugin's required API version is compatible
Args:
required_version: Required API version string
Returns:
True if compatible, False otherwise
"""
try:
required_major, required_minor = map(int, required_version.split('.'))
current_major, current_minor = map(int, self.current_api_version.split('.'))
# Same major version, minor version can be higher
return required_major == current_major and required_minor <= current_minor
except ValueError:
return False
def register_plugin(self, plugin_info: PluginInfo) -> None:
"""
Register a plugin in the registry
Args:
plugin_info: Plugin information
"""
self._plugins[plugin_info.name] = plugin_info
self.logger.info(f"Registered plugin: {plugin_info.name} v{plugin_info.version}")
def unregister_plugin(self, plugin_name: str) -> None:
"""
Unregister a plugin from the registry
Args:
plugin_name: Name of the plugin to unregister
Returns:
True if plugin was unregistered, False if not found
"""
if plugin_name not in self.plugins:
return False
del self.plugins[plugin_name]
del self.plugin_metadata[plugin_name]
logger.debug(f"Unregistered plugin '{plugin_name}'")
return True
def get_plugin_class(self, plugin_name: str) -> Optional[Type[BasePlugin]]:
if plugin_name in self._plugins:
del self._plugins[plugin_name]
self.logger.info(f"Unregistered plugin: {plugin_name}")
def get_plugin(self, plugin_name: str) -> Optional[PluginInfo]:
"""
Get a registered plugin class.
Get plugin information by name
Args:
plugin_name: Name of the plugin
Returns:
Plugin class if found, None otherwise
PluginInfo object or None if not found
"""
return self.plugins.get(plugin_name)
def get_plugins(self) -> Dict[str, Type[BasePlugin]]:
return self._plugins.get(plugin_name)
def list_plugins(self) -> List[PluginInfo]:
"""
Get all registered plugins.
List all registered plugins
Returns:
Dictionary of registered plugin names and classes
List of plugin information
"""
return self.plugins.copy()
def get_plugin_names(self) -> list:
return list(self._plugins.values())
def list_enabled_plugins(self) -> List[PluginInfo]:
"""
Get list of registered plugin names.
List enabled plugins
Returns:
List of registered plugin names
List of enabled plugin information
"""
return list(self.plugins.keys())
def create(self, plugin_name: str, config: Any, hook_manager: Any) -> Optional[BasePlugin]:
return [plugin for plugin in self._plugins.values() if plugin.enabled]
def enable_plugin(self, plugin_name: str) -> None:
"""
Create a plugin instance.
Enable a plugin
Args:
plugin_name: Name of the plugin to create
config: Configuration object
hook_manager: Hook manager instance
Returns:
Plugin instance if successful, None if plugin not found
plugin_name: Name of the plugin to enable
"""
plugin_class = self.get_plugin_class(plugin_name)
if not plugin_class:
logger.warning(f"Plugin '{plugin_name}' not found")
return None
if plugin_name in self._plugins:
self._plugins[plugin_name].enabled = True
self.logger.info(f"Enabled plugin: {plugin_name}")
def disable_plugin(self, plugin_name: str) -> None:
"""
Disable a plugin
Args:
plugin_name: Name of the plugin to disable
"""
if plugin_name in self._plugins:
self._plugins[plugin_name].enabled = False
self.logger.info(f"Disabled plugin: {plugin_name}")
def load_plugin(self, plugin_name: str, plugin_manager, config: Dict[str, Any], deb_mock) -> BasePlugin:
"""
Load a plugin instance
Args:
plugin_name: Name of the plugin to load
plugin_manager: Plugin manager instance
config: Plugin configuration
deb_mock: DebMock instance
Returns:
Loaded plugin instance
Raises:
PluginError: If plugin cannot be loaded
"""
if plugin_name not in self._plugins:
raise PluginError(f"Plugin '{plugin_name}' not found in registry")
plugin_info = self._plugins[plugin_name]
if not plugin_info.enabled:
raise PluginError(f"Plugin '{plugin_name}' is disabled")
if plugin_name in self._loaded_plugins:
return self._loaded_plugins[plugin_name]
try:
plugin_instance = plugin_class(config, hook_manager)
logger.debug(f"Created plugin instance '{plugin_name}'")
# Create plugin instance
plugin_instance = plugin_info.init_function(plugin_manager, config, deb_mock)
if not isinstance(plugin_instance, BasePlugin):
raise PluginError(f"Plugin '{plugin_name}' did not return a BasePlugin instance")
# Store loaded plugin
self._loaded_plugins[plugin_name] = plugin_instance
self.logger.info(f"Loaded plugin: {plugin_name}")
return plugin_instance
except Exception as e:
logger.error(f"Failed to create plugin '{plugin_name}': {e}")
return None
def create_all_enabled(self, config: Any, hook_manager: Any) -> Dict[str, BasePlugin]:
raise PluginError(f"Failed to load plugin '{plugin_name}': {e}")
def unload_plugin(self, plugin_name: str) -> None:
"""
Create instances of all enabled plugins.
Unload a plugin instance
Args:
config: Configuration object
hook_manager: Hook manager instance
Returns:
Dictionary of plugin names and instances
plugin_name: Name of the plugin to unload
"""
enabled_plugins = {}
for plugin_name in self.get_plugin_names():
plugin_instance = self.create(plugin_name, config, hook_manager)
if plugin_instance and plugin_instance.enabled:
enabled_plugins[plugin_name] = plugin_instance
logger.debug(f"Created {len(enabled_plugins)} enabled plugin instances")
return enabled_plugins
def get_plugin_info(self, plugin_name: str) -> Dict[str, Any]:
if plugin_name in self._loaded_plugins:
del self._loaded_plugins[plugin_name]
self.logger.info(f"Unloaded plugin: {plugin_name}")
def reload_plugin(self, plugin_name: str, plugin_manager, config: Dict[str, Any], deb_mock) -> BasePlugin:
"""
Get information about a registered plugin.
Reload a plugin
Args:
plugin_name: Name of the plugin
plugin_name: Name of the plugin to reload
plugin_manager: Plugin manager instance
config: Plugin configuration
deb_mock: DebMock instance
Returns:
Dictionary with plugin information
Reloaded plugin instance
"""
if plugin_name not in self.plugins:
return {"error": f'Plugin "{plugin_name}" not found'}
plugin_class = self.plugins[plugin_name]
metadata = self.plugin_metadata[plugin_name]
info = {
"name": plugin_name,
"class": plugin_class.__name__,
"module": plugin_class.__module__,
"metadata": metadata,
"docstring": plugin_class.__doc__ or "No documentation available",
}
return info
def get_all_plugin_info(self) -> Dict[str, Dict[str, Any]]:
"""
Get information about all registered plugins.
Returns:
Dictionary mapping plugin names to their information
"""
return {name: self.get_plugin_info(name) for name in self.get_plugin_names()}
def load_plugin_from_module(self, module_name: str, plugin_name: str) -> bool:
"""
Load a plugin from a module.
Args:
module_name: Name of the module to load
plugin_name: Name of the plugin class in the module
Returns:
True if plugin was loaded successfully, False otherwise
"""
try:
module = importlib.import_module(module_name)
plugin_class = getattr(module, plugin_name)
# Use module name as plugin name if not specified
self.register(plugin_name, plugin_class)
return True
except ImportError as e:
logger.error(f"Failed to import module '{module_name}': {e}")
return False
except AttributeError as e:
logger.error(f"Plugin class '{plugin_name}' not found in module '{module_name}': {e}")
return False
except Exception as e:
logger.error(f"Failed to load plugin from '{module_name}.{plugin_name}': {e}")
return False
def load_plugins_from_config(self, config: Any) -> Dict[str, BasePlugin]:
"""
Load plugins based on configuration.
Args:
config: Configuration object with plugin settings
Returns:
Dictionary of loaded plugin instances
"""
loaded_plugins = {}
if not hasattr(config, "plugins") or not config.plugins:
return loaded_plugins
for plugin_name, plugin_config in config.plugins.items():
if not isinstance(plugin_config, dict):
continue
if plugin_config.get("enabled", False):
# Try to load from built-in plugins first
plugin_instance = self.create(plugin_name, config, None)
if plugin_instance:
loaded_plugins[plugin_name] = plugin_instance
else:
# Try to load from external module
module_name = plugin_config.get("module")
if module_name:
if self.load_plugin_from_module(module_name, plugin_name):
plugin_instance = self.create(plugin_name, config, None)
if plugin_instance:
loaded_plugins[plugin_name] = plugin_instance
return loaded_plugins
def _register_builtin_plugins(self) -> None:
"""Register built-in plugins."""
try:
# Import and register built-in plugins
from .bind_mount import BindMountPlugin
from .compress_logs import CompressLogsPlugin
from .root_cache import RootCachePlugin
from .tmpfs import TmpfsPlugin
self.register(
"bind_mount",
BindMountPlugin,
{
"description": "Mount host directories into chroot",
"hooks": ["mount_root", "postumount"],
"builtin": True,
},
)
self.register(
"compress_logs",
CompressLogsPlugin,
{
"description": "Compress build logs to save space",
"hooks": ["process_logs"],
"builtin": True,
},
)
self.register(
"root_cache",
RootCachePlugin,
{
"description": "Root cache management for faster builds",
"hooks": [
"preinit",
"postinit",
"postchroot",
"postshell",
"clean",
],
"builtin": True,
},
)
self.register(
"tmpfs",
TmpfsPlugin,
{
"description": "Use tmpfs for faster I/O operations",
"hooks": ["mount_root", "postumount"],
"builtin": True,
},
)
logger.debug("Registered built-in plugins")
except ImportError as e:
logger.warning(f"Some built-in plugins could not be loaded: {e}")
except Exception as e:
logger.warning(f"Error registering built-in plugins: {e}")
self.unload_plugin(plugin_name)
return self.load_plugin(plugin_name, plugin_manager, config, deb_mock)
def get_plugin_statistics(self) -> Dict[str, Any]:
"""
Get statistics about registered plugins.
Get plugin registry statistics
Returns:
Dictionary with plugin statistics
"""
stats = {
"total_plugins": len(self.plugins),
"builtin_plugins": len([p for p in self.plugin_metadata.values() if p.get("builtin", False)]),
"external_plugins": len([p for p in self.plugin_metadata.values() if not p.get("builtin", False)]),
"plugins_by_hook": {},
total_plugins = len(self._plugins)
enabled_plugins = len(self.list_enabled_plugins())
loaded_plugins = len(self._loaded_plugins)
return {
'total_plugins': total_plugins,
'enabled_plugins': enabled_plugins,
'loaded_plugins': loaded_plugins,
'disabled_plugins': total_plugins - enabled_plugins,
'api_version': self.current_api_version,
'plugin_directories': self.plugin_dirs
}
# Count plugins by hook usage
for plugin_name, metadata in self.plugin_metadata.items():
hooks = metadata.get("hooks", [])
for hook in hooks:
if hook not in stats["plugins_by_hook"]:
stats["plugins_by_hook"][hook] = []
stats["plugins_by_hook"][hook].append(plugin_name)
return stats
def validate_plugin_config(self, plugin_name: str, config: Any) -> bool:
def validate_plugin_dependencies(self, plugin_name: str) -> List[str]:
"""
Validate plugin configuration.
Validate plugin dependencies
Args:
plugin_name: Name of the plugin
config: Configuration to validate
plugin_name: Name of the plugin to validate
Returns:
True if configuration is valid, False otherwise
List of missing dependencies
"""
if plugin_name not in self.plugins:
return False
if plugin_name not in self._plugins:
return [f"Plugin '{plugin_name}' not found"]
plugin_info = self._plugins[plugin_name]
missing_deps = []
# Check if plugin file exists
if not os.path.exists(plugin_info.file_path):
missing_deps.append(f"Plugin file not found: {plugin_info.file_path}")
# Check Python dependencies
try:
spec = importlib.util.spec_from_file_location(plugin_name, plugin_info.file_path)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
except Exception as e:
missing_deps.append(f"Failed to load plugin module: {e}")
return missing_deps
# Basic validation - plugins can override this method
plugin_class = self.plugins[plugin_name]
if hasattr(plugin_class, "validate_config"):
return plugin_class.validate_config(config)
return True
# Global plugin registry instance
_global_registry = None
def get_plugin_registry() -> PluginRegistry:
"""Get the global plugin registry instance"""
global _global_registry
if _global_registry is None:
_global_registry = PluginRegistry()
return _global_registry
def discover_plugins() -> List[PluginInfo]:
"""Discover all available plugins"""
registry = get_plugin_registry()
return registry.discover_plugins()
def register_plugin(plugin_info: PluginInfo) -> None:
"""Register a plugin in the global registry"""
registry = get_plugin_registry()
registry.register_plugin(plugin_info)
def get_plugin(plugin_name: str) -> Optional[PluginInfo]:
"""Get plugin information by name"""
registry = get_plugin_registry()
return registry.get_plugin(plugin_name)