203 lines
8.4 KiB
Python
203 lines
8.4 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Renderer registry for automatic discovery and registration of renderers.
|
|
"""
|
|
|
|
import logging
|
|
import importlib
|
|
from typing import Dict, Type, List, Optional
|
|
from .documentRendererBaseTemplate import BaseRenderer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class RendererRegistry:
|
|
"""Registry for automatic renderer discovery and management."""
|
|
|
|
def __init__(self):
|
|
self._renderers: Dict[str, Type[BaseRenderer]] = {}
|
|
self._format_mappings: Dict[str, str] = {}
|
|
self._discovered = False
|
|
|
|
def discoverRenderers(self) -> None:
|
|
"""Automatically discover and register all renderers by scanning files."""
|
|
if self._discovered:
|
|
return
|
|
|
|
try:
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Get the directory containing this registry file
|
|
currentDir = Path(__file__).parent
|
|
renderersDir = currentDir
|
|
|
|
# Get the package name dynamically
|
|
packageName = __name__.rsplit('.', 1)[0]
|
|
|
|
# Scan all Python files in the renderers directory
|
|
for filePath in renderersDir.glob("*.py"):
|
|
if filePath.name in ['registry.py', 'documentRendererBaseTemplate.py', '__init__.py']:
|
|
continue
|
|
|
|
# Extract module name from filename
|
|
moduleName = filePath.stem
|
|
|
|
try:
|
|
# Import the module dynamically
|
|
fullModuleName = f"{packageName}.{moduleName}"
|
|
module = importlib.import_module(fullModuleName)
|
|
|
|
# Look for renderer classes in the module
|
|
for attrName in dir(module):
|
|
attr = getattr(module, attrName)
|
|
if (isinstance(attr, type) and
|
|
issubclass(attr, BaseRenderer) and
|
|
attr != BaseRenderer and
|
|
hasattr(attr, 'getSupportedFormats')):
|
|
|
|
# Register the renderer
|
|
self._registerRendererClass(attr)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Could not load renderer from {moduleName}: {str(e)}")
|
|
continue
|
|
|
|
self._discovered = True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during renderer discovery: {str(e)}")
|
|
self._discovered = True # Mark as discovered to avoid repeated attempts
|
|
|
|
def _registerRendererClass(self, rendererClass: Type[BaseRenderer]) -> None:
|
|
"""Register a renderer class with its supported formats."""
|
|
try:
|
|
# Get supported formats from the renderer class
|
|
supportedFormats = rendererClass.getSupportedFormats()
|
|
|
|
# Get priority (default to 0 if not specified)
|
|
priority = rendererClass.getPriority() if hasattr(rendererClass, 'getPriority') else 0
|
|
|
|
for formatName in supportedFormats:
|
|
formatKey = formatName.lower()
|
|
|
|
# Check if format already registered - use priority to decide
|
|
if formatKey in self._renderers:
|
|
existingRenderer = self._renderers[formatKey]
|
|
existingPriority = existingRenderer.getPriority() if hasattr(existingRenderer, 'getPriority') else 0
|
|
|
|
# Only replace if new renderer has higher priority
|
|
if priority > existingPriority:
|
|
logger.debug(f"Replacing {existingRenderer.__name__} with {rendererClass.__name__} for format '{formatName}' (priority {priority} > {existingPriority})")
|
|
self._renderers[formatKey] = rendererClass
|
|
else:
|
|
logger.debug(f"Keeping {existingRenderer.__name__} for format '{formatName}' (priority {existingPriority} >= {priority})")
|
|
else:
|
|
# Register primary format
|
|
self._renderers[formatKey] = rendererClass
|
|
|
|
# Register aliases if any
|
|
if hasattr(rendererClass, 'getFormatAliases'):
|
|
aliases = rendererClass.getFormatAliases()
|
|
for alias in aliases:
|
|
self._format_mappings[alias.lower()] = formatName.lower()
|
|
|
|
logger.debug(f"Registered {rendererClass.__name__} for formats: {supportedFormats} (priority: {priority})")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error registering renderer {rendererClass.__name__}: {str(e)}")
|
|
|
|
def getRenderer(self, outputFormat: str, services=None) -> Optional[BaseRenderer]:
|
|
"""Get a renderer instance for the specified format."""
|
|
if not self._discovered:
|
|
self.discoverRenderers()
|
|
|
|
# Normalize format name
|
|
formatName = outputFormat.lower().strip()
|
|
|
|
# Check for aliases first
|
|
if formatName in self._format_mappings:
|
|
formatName = self._format_mappings[formatName]
|
|
|
|
# Get renderer class
|
|
rendererClass = self._renderers.get(formatName)
|
|
|
|
if rendererClass:
|
|
try:
|
|
return rendererClass(services=services)
|
|
except Exception as e:
|
|
logger.error(f"Error creating renderer instance for {formatName}: {str(e)}")
|
|
return None
|
|
|
|
logger.warning(f"No renderer found for format: {outputFormat}")
|
|
return None
|
|
|
|
def getSupportedFormats(self) -> List[str]:
|
|
"""Get list of all supported formats."""
|
|
if not self._discovered:
|
|
self.discoverRenderers()
|
|
|
|
formats = list(self._renderers.keys())
|
|
formats.extend(self._format_mappings.keys())
|
|
return sorted(set(formats))
|
|
|
|
def getRendererInfo(self) -> Dict[str, Dict[str, str]]:
|
|
"""Get information about all registered renderers."""
|
|
if not self._discovered:
|
|
self.discoverRenderers()
|
|
|
|
info = {}
|
|
for formatName, rendererClass in self._renderers.items():
|
|
info[formatName] = {
|
|
'class_name': rendererClass.__name__,
|
|
'module': rendererClass.__module__,
|
|
'description': getattr(rendererClass, '__doc__', 'No description').strip().split('\n')[0] if rendererClass.__doc__ else 'No description'
|
|
}
|
|
|
|
return info
|
|
|
|
def getOutputStyle(self, outputFormat: str) -> Optional[str]:
|
|
"""
|
|
Get the output style classification for a given format.
|
|
Returns: 'code', 'document', 'image', or other (e.g., 'video' for future use)
|
|
"""
|
|
if not self._discovered:
|
|
self.discoverRenderers()
|
|
|
|
# Normalize format name
|
|
formatName = outputFormat.lower().strip()
|
|
|
|
# Check for aliases first
|
|
if formatName in self._format_mappings:
|
|
formatName = self._format_mappings[formatName]
|
|
|
|
# Get renderer class and call getOutputStyle (all renderers have same signature)
|
|
rendererClass = self._renderers.get(formatName)
|
|
try:
|
|
return rendererClass.getOutputStyle(formatName)
|
|
except (AttributeError, TypeError) as e:
|
|
logger.warning(f"No renderer found for format: {outputFormat}, cannot determine output style")
|
|
return None
|
|
except Exception as e:
|
|
logger.warning(f"Error getting output style for {outputFormat}: {str(e)}")
|
|
return None
|
|
|
|
# Global registry instance
|
|
_registry = RendererRegistry()
|
|
|
|
def getRenderer(outputFormat: str, services=None) -> Optional[BaseRenderer]:
|
|
"""Get a renderer instance for the specified format."""
|
|
return _registry.getRenderer(outputFormat, services)
|
|
|
|
def getSupportedFormats() -> List[str]:
|
|
"""Get list of all supported formats."""
|
|
return _registry.getSupportedFormats()
|
|
|
|
def getRendererInfo() -> Dict[str, Dict[str, str]]:
|
|
"""Get information about all registered renderers."""
|
|
return _registry.getRendererInfo()
|
|
|
|
def getOutputStyle(outputFormat: str) -> Optional[str]:
|
|
"""Get the output style classification for a given format."""
|
|
return _registry.getOutputStyle(outputFormat)
|