# 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)