Files
tegwick b0de32d083 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>
2025-10-03 11:23:32 +02:00

207 lines
5.9 KiB
Python

"""
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()