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:
272
markitect/plugins/base.py
Normal file
272
markitect/plugins/base.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
Base classes and interfaces for MarkiTect plugins.
|
||||
|
||||
This module defines the core plugin architecture that all plugins must implement.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Dict, Any, Optional, List, Union
|
||||
from pathlib import Path
|
||||
import inspect
|
||||
|
||||
|
||||
class PluginType(Enum):
|
||||
"""Types of plugins supported by MarkiTect."""
|
||||
PROCESSOR = "processor" # Content processors (markdown, etc.)
|
||||
FORMATTER = "formatter" # Output formatters (JSON, YAML, etc.)
|
||||
VALIDATOR = "validator" # Content validators
|
||||
EXPORTER = "exporter" # Export handlers (PDF, HTML, etc.)
|
||||
GENERATOR = "generator" # Content generators (templates, stubs, etc.)
|
||||
IMPORTER = "importer" # Import handlers (various formats)
|
||||
TRANSFORMER = "transformer" # Content transformers
|
||||
EXTENSION = "extension" # General extensions
|
||||
BACKEND = "backend" # Storage/API backends
|
||||
COMMAND = "command" # CLI command extensions
|
||||
|
||||
|
||||
class PluginMetadata:
|
||||
"""Metadata about a plugin."""
|
||||
|
||||
def __init__(self,
|
||||
name: str,
|
||||
version: str,
|
||||
description: str,
|
||||
author: str = "",
|
||||
plugin_type: PluginType = PluginType.EXTENSION,
|
||||
dependencies: List[str] = None,
|
||||
markitect_version: str = ">=0.1.0"):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.description = description
|
||||
self.author = author
|
||||
self.plugin_type = plugin_type
|
||||
self.dependencies = dependencies or []
|
||||
self.markitect_version = markitect_version
|
||||
|
||||
|
||||
class BasePlugin(ABC):
|
||||
"""Abstract base class for all MarkiTect plugins."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any] = None):
|
||||
"""
|
||||
Initialize plugin with configuration.
|
||||
|
||||
Args:
|
||||
config: Plugin-specific configuration dictionary
|
||||
"""
|
||||
self.config = config or {}
|
||||
self._metadata = None
|
||||
self._initialized = False
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def metadata(self) -> PluginMetadata:
|
||||
"""Return plugin metadata."""
|
||||
pass
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
Initialize the plugin. Called after plugin is loaded.
|
||||
|
||||
Returns:
|
||||
True if initialization successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
self._initialize()
|
||||
self._initialized = True
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _initialize(self) -> None:
|
||||
"""
|
||||
Override this method to implement plugin-specific initialization.
|
||||
Default implementation does nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Cleanup plugin resources. Called when plugin is unloaded.
|
||||
Override this method to implement cleanup logic.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_initialized(self) -> bool:
|
||||
"""Check if plugin is initialized."""
|
||||
return self._initialized
|
||||
|
||||
def validate_config(self) -> List[str]:
|
||||
"""
|
||||
Validate plugin configuration.
|
||||
|
||||
Returns:
|
||||
List of validation error messages (empty if valid)
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
class ProcessorPlugin(BasePlugin):
|
||||
"""Base class for content processor plugins."""
|
||||
|
||||
@abstractmethod
|
||||
def process(self, content: str, **kwargs) -> str:
|
||||
"""
|
||||
Process content and return processed result.
|
||||
|
||||
Args:
|
||||
content: Input content to process
|
||||
**kwargs: Additional processing parameters
|
||||
|
||||
Returns:
|
||||
Processed content
|
||||
"""
|
||||
pass
|
||||
|
||||
def can_process(self, content: str, **kwargs) -> bool:
|
||||
"""
|
||||
Check if this processor can handle the given content.
|
||||
|
||||
Args:
|
||||
content: Content to check
|
||||
**kwargs: Additional context
|
||||
|
||||
Returns:
|
||||
True if processor can handle content
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class FormatterPlugin(BasePlugin):
|
||||
"""Base class for output formatter plugins."""
|
||||
|
||||
@abstractmethod
|
||||
def format(self, data: Any, **kwargs) -> str:
|
||||
"""
|
||||
Format data to string representation.
|
||||
|
||||
Args:
|
||||
data: Data to format
|
||||
**kwargs: Formatting options
|
||||
|
||||
Returns:
|
||||
Formatted string
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_file_extension(self) -> str:
|
||||
"""
|
||||
Get the file extension for this format.
|
||||
|
||||
Returns:
|
||||
File extension (e.g., '.json', '.yaml')
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ValidatorPlugin(BasePlugin):
|
||||
"""Base class for content validator plugins."""
|
||||
|
||||
@abstractmethod
|
||||
def validate(self, content: str, **kwargs) -> List[str]:
|
||||
"""
|
||||
Validate content and return list of errors.
|
||||
|
||||
Args:
|
||||
content: Content to validate
|
||||
**kwargs: Validation options
|
||||
|
||||
Returns:
|
||||
List of validation error messages (empty if valid)
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ExporterPlugin(BasePlugin):
|
||||
"""Base class for export handler plugins."""
|
||||
|
||||
@abstractmethod
|
||||
def export(self, data: Any, output_path: Path, **kwargs) -> bool:
|
||||
"""
|
||||
Export data to file.
|
||||
|
||||
Args:
|
||||
data: Data to export
|
||||
output_path: Output file path
|
||||
**kwargs: Export options
|
||||
|
||||
Returns:
|
||||
True if export successful
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_supported_formats(self) -> List[str]:
|
||||
"""
|
||||
Get list of supported export formats.
|
||||
|
||||
Returns:
|
||||
List of format names (e.g., ['pdf', 'html'])
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CommandPlugin(BasePlugin):
|
||||
"""Base class for CLI command extension plugins."""
|
||||
|
||||
@abstractmethod
|
||||
def get_commands(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get Click commands provided by this plugin.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping command names to Click command objects
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_command_group_name(self) -> Optional[str]:
|
||||
"""
|
||||
Get the command group name if commands should be grouped.
|
||||
|
||||
Returns:
|
||||
Group name or None for top-level commands
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
# Plugin discovery and loading utilities
|
||||
|
||||
def get_plugin_class_from_module(module, plugin_type: PluginType = None) -> List[type]:
|
||||
"""
|
||||
Discover plugin classes in a module.
|
||||
|
||||
Args:
|
||||
module: Python module to search
|
||||
plugin_type: Optional plugin type filter
|
||||
|
||||
Returns:
|
||||
List of plugin classes found
|
||||
"""
|
||||
plugin_classes = []
|
||||
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if (issubclass(obj, BasePlugin) and
|
||||
obj != BasePlugin and
|
||||
not inspect.isabstract(obj)):
|
||||
|
||||
# Check plugin type if specified
|
||||
if plugin_type:
|
||||
try:
|
||||
instance = obj()
|
||||
if instance.metadata.plugin_type == plugin_type:
|
||||
plugin_classes.append(obj)
|
||||
except Exception:
|
||||
# Skip if we can't instantiate or get metadata
|
||||
continue
|
||||
else:
|
||||
plugin_classes.append(obj)
|
||||
|
||||
return plugin_classes
|
||||
Reference in New Issue
Block a user