feat: implement comprehensive plugin architecture and extensions system (issue #19)
Complete plugin system implementation providing extensible architecture for MarkiTect: 🏗️ **Core Plugin Architecture**: - BasePlugin abstract class with lifecycle management (initialize/cleanup) - Specialized plugin types: ProcessorPlugin, FormatterPlugin, ValidatorPlugin, ExporterPlugin, CommandPlugin - PluginMetadata system with version, dependencies, and type information - Plugin initialization and configuration validation 🔍 **Plugin Discovery & Management**: - PluginManager with automatic discovery from built-in modules and directories - PluginRegistry for centralized plugin registration and lifecycle management - Support for plugin loading, unloading, and reloading with configuration - Plugin discovery from multiple sources (built-in, directories, packages) 🛠️ **CLI Integration**: - markitect plugin-list: List all available plugins with metadata - markitect plugin-load: Load plugins with optional configuration - markitect plugin-unload: Unload plugins and cleanup resources - markitect plugin-info: Show detailed plugin information - markitect plugin-discover: Discover and refresh plugin catalog 📦 **Built-in Plugins**: - JSON/YAML/Table formatters for output formatting - Markdown/Text processors for content processing - Auto-registered via @register_plugin decorator - Comprehensive configuration options 🔧 **Developer Experience**: - @register_plugin decorator for easy plugin registration - Plugin configuration validation and error handling - Comprehensive API documentation with examples - Plugin development guide and best practices 📋 **Example Plugins**: - Advanced text processor with case conversion and pattern replacement - XML/CSV formatters demonstrating custom output formats - Complete examples showing plugin development patterns 🧪 **Test Coverage**: - 59 comprehensive tests covering all plugin functionality - Tests for plugin lifecycle, registration, discovery, and CLI integration - Error handling and edge case coverage - Built-in plugin validation Technical Implementation: - Plugin types: processor, formatter, validator, exporter, generator, importer, transformer, extension, backend, command - Configuration-driven plugin management with YAML/JSON support - Graceful error handling and plugin isolation - Plugin dependency validation and compatibility checking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
207
markitect/plugins/registry.py
Normal file
207
markitect/plugins/registry.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Plugin registry for managing discovered and loaded plugins.
|
||||
|
||||
This module provides a central registry for all plugins in the system.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Type, Any
|
||||
from .base import BasePlugin, PluginType
|
||||
|
||||
|
||||
class PluginRegistry:
|
||||
"""Central registry for managing plugins."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize empty registry."""
|
||||
self._plugins: Dict[str, Type[BasePlugin]] = {}
|
||||
self._instances: Dict[str, BasePlugin] = {}
|
||||
self._plugins_by_type: Dict[PluginType, List[str]] = {}
|
||||
|
||||
def register(self, plugin_class: Type[BasePlugin], name: Optional[str] = None) -> str:
|
||||
"""
|
||||
Register a plugin class.
|
||||
|
||||
Args:
|
||||
plugin_class: Plugin class to register
|
||||
name: Optional plugin name (uses class name if not provided)
|
||||
|
||||
Returns:
|
||||
The name the plugin was registered under
|
||||
|
||||
Raises:
|
||||
ValueError: If plugin name already exists
|
||||
"""
|
||||
if name is None:
|
||||
name = plugin_class.__name__
|
||||
|
||||
if name in self._plugins:
|
||||
raise ValueError(f"Plugin '{name}' is already registered")
|
||||
|
||||
self._plugins[name] = plugin_class
|
||||
|
||||
# Create instance to get metadata
|
||||
try:
|
||||
instance = plugin_class()
|
||||
plugin_type = instance.metadata.plugin_type
|
||||
|
||||
if plugin_type not in self._plugins_by_type:
|
||||
self._plugins_by_type[plugin_type] = []
|
||||
|
||||
self._plugins_by_type[plugin_type].append(name)
|
||||
except Exception:
|
||||
# If we can't get metadata, register as generic extension
|
||||
if PluginType.EXTENSION not in self._plugins_by_type:
|
||||
self._plugins_by_type[PluginType.EXTENSION] = []
|
||||
self._plugins_by_type[PluginType.EXTENSION].append(name)
|
||||
|
||||
return name
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""
|
||||
Unregister a plugin.
|
||||
|
||||
Args:
|
||||
name: Plugin name to unregister
|
||||
|
||||
Returns:
|
||||
True if plugin was unregistered, False if not found
|
||||
"""
|
||||
if name not in self._plugins:
|
||||
return False
|
||||
|
||||
# Remove from instances if loaded
|
||||
if name in self._instances:
|
||||
instance = self._instances[name]
|
||||
instance.cleanup()
|
||||
del self._instances[name]
|
||||
|
||||
# Remove from type mapping
|
||||
for plugin_type, plugin_names in self._plugins_by_type.items():
|
||||
if name in plugin_names:
|
||||
plugin_names.remove(name)
|
||||
break
|
||||
|
||||
# Remove from main registry
|
||||
del self._plugins[name]
|
||||
return True
|
||||
|
||||
def get_plugin(self, name: str, config: Dict[str, Any] = None) -> Optional[BasePlugin]:
|
||||
"""
|
||||
Get plugin instance by name.
|
||||
|
||||
Args:
|
||||
name: Plugin name
|
||||
config: Optional configuration for plugin
|
||||
|
||||
Returns:
|
||||
Plugin instance or None if not found
|
||||
"""
|
||||
if name not in self._plugins:
|
||||
return None
|
||||
|
||||
# Return existing instance if already loaded and no new config
|
||||
if name in self._instances and config is None:
|
||||
return self._instances[name]
|
||||
|
||||
# Create new instance
|
||||
try:
|
||||
plugin_class = self._plugins[name]
|
||||
instance = plugin_class(config)
|
||||
|
||||
if instance.initialize():
|
||||
self._instances[name] = instance
|
||||
return instance
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_plugins_by_type(self, plugin_type: PluginType) -> List[str]:
|
||||
"""
|
||||
Get list of plugin names by type.
|
||||
|
||||
Args:
|
||||
plugin_type: Type of plugins to retrieve
|
||||
|
||||
Returns:
|
||||
List of plugin names of the specified type
|
||||
"""
|
||||
return self._plugins_by_type.get(plugin_type, []).copy()
|
||||
|
||||
def list_plugins(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
List all registered plugins with metadata.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping plugin names to metadata
|
||||
"""
|
||||
result = {}
|
||||
|
||||
for name, plugin_class in self._plugins.items():
|
||||
try:
|
||||
instance = plugin_class()
|
||||
metadata = instance.metadata
|
||||
result[name] = {
|
||||
'name': metadata.name,
|
||||
'version': metadata.version,
|
||||
'description': metadata.description,
|
||||
'author': metadata.author,
|
||||
'type': metadata.plugin_type.value,
|
||||
'dependencies': metadata.dependencies,
|
||||
'markitect_version': metadata.markitect_version,
|
||||
'loaded': name in self._instances
|
||||
}
|
||||
except Exception:
|
||||
result[name] = {
|
||||
'name': name,
|
||||
'error': 'Failed to load metadata'
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def is_loaded(self, name: str) -> bool:
|
||||
"""
|
||||
Check if plugin is loaded.
|
||||
|
||||
Args:
|
||||
name: Plugin name
|
||||
|
||||
Returns:
|
||||
True if plugin is loaded
|
||||
"""
|
||||
return name in self._instances
|
||||
|
||||
def reload_plugin(self, name: str, config: Dict[str, Any] = None) -> bool:
|
||||
"""
|
||||
Reload a plugin with new configuration.
|
||||
|
||||
Args:
|
||||
name: Plugin name
|
||||
config: New configuration
|
||||
|
||||
Returns:
|
||||
True if reload successful
|
||||
"""
|
||||
if name not in self._plugins:
|
||||
return False
|
||||
|
||||
# Cleanup existing instance
|
||||
if name in self._instances:
|
||||
self._instances[name].cleanup()
|
||||
del self._instances[name]
|
||||
|
||||
# Load with new config
|
||||
return self.get_plugin(name, config) is not None
|
||||
|
||||
def cleanup_all(self) -> None:
|
||||
"""Cleanup all loaded plugin instances."""
|
||||
for instance in self._instances.values():
|
||||
try:
|
||||
instance.cleanup()
|
||||
except Exception:
|
||||
pass
|
||||
self._instances.clear()
|
||||
|
||||
|
||||
# Global plugin registry instance
|
||||
plugin_registry = PluginRegistry()
|
||||
Reference in New Issue
Block a user