238 lines
9.7 KiB
Python
238 lines
9.7 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Renderer registry for automatic discovery and registration of renderers.
|
|
|
|
Renderers are indexed by (format, outputStyle) so that document generation
|
|
and code generation each get the correct renderer for the same format.
|
|
"""
|
|
|
|
import logging
|
|
import importlib
|
|
from typing import Dict, Type, List, Optional, Tuple
|
|
from .documentRendererBaseTemplate import BaseRenderer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RendererRegistry:
|
|
"""Registry for automatic renderer discovery and management.
|
|
|
|
Maintains separate renderer mappings per outputStyle ('document', 'code', etc.)
|
|
so that document-generation and code-generation paths each resolve to the
|
|
correct renderer, even when both support the same format (e.g. 'csv').
|
|
"""
|
|
|
|
def __init__(self):
|
|
# Key: (formatName, outputStyle) -> rendererClass
|
|
self._renderers: Dict[Tuple[str, 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:
|
|
from pathlib import Path
|
|
|
|
currentDir = Path(__file__).parent
|
|
packageName = __name__.rsplit('.', 1)[0]
|
|
|
|
for filePath in currentDir.glob("*.py"):
|
|
if filePath.name in ['registry.py', 'documentRendererBaseTemplate.py', 'codeRendererBaseTemplate.py', '__init__.py']:
|
|
continue
|
|
|
|
moduleName = filePath.stem
|
|
|
|
try:
|
|
fullModuleName = f"{packageName}.{moduleName}"
|
|
module = importlib.import_module(fullModuleName)
|
|
|
|
for attrName in dir(module):
|
|
attr = getattr(module, attrName)
|
|
if (isinstance(attr, type) and
|
|
issubclass(attr, BaseRenderer) and
|
|
attr != BaseRenderer and
|
|
hasattr(attr, 'getSupportedFormats')):
|
|
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
|
|
|
|
def _registerRendererClass(self, rendererClass: Type[BaseRenderer]) -> None:
|
|
"""Register a renderer class keyed by (format, outputStyle)."""
|
|
try:
|
|
supportedFormats = rendererClass.getSupportedFormats()
|
|
outputStyle = rendererClass.getOutputStyle() if hasattr(rendererClass, 'getOutputStyle') else 'document'
|
|
priority = rendererClass.getPriority() if hasattr(rendererClass, 'getPriority') else 0
|
|
|
|
for formatName in supportedFormats:
|
|
formatKey = formatName.lower()
|
|
registryKey = (formatKey, outputStyle)
|
|
|
|
if registryKey in self._renderers:
|
|
existingRenderer = self._renderers[registryKey]
|
|
existingPriority = existingRenderer.getPriority() if hasattr(existingRenderer, 'getPriority') else 0
|
|
|
|
if priority > existingPriority:
|
|
logger.debug(f"Replacing {existingRenderer.__name__} with {rendererClass.__name__} for ({formatKey}, {outputStyle}) (priority {priority} > {existingPriority})")
|
|
self._renderers[registryKey] = rendererClass
|
|
else:
|
|
logger.debug(f"Keeping {existingRenderer.__name__} for ({formatKey}, {outputStyle}) (priority {existingPriority} >= {priority})")
|
|
else:
|
|
self._renderers[registryKey] = rendererClass
|
|
|
|
# Register aliases
|
|
if hasattr(rendererClass, 'getFormatAliases'):
|
|
aliases = rendererClass.getFormatAliases()
|
|
for alias in aliases:
|
|
self._format_mappings[alias.lower()] = formatKey
|
|
|
|
logger.debug(f"Registered {rendererClass.__name__} for formats={supportedFormats}, style={outputStyle}, priority={priority}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error registering renderer {rendererClass.__name__}: {str(e)}")
|
|
|
|
def getRenderer(self, outputFormat: str, services=None, outputStyle: str = None) -> Optional[BaseRenderer]:
|
|
"""Get a renderer instance for the specified format and style.
|
|
|
|
Args:
|
|
outputFormat: Format name (e.g. 'csv', 'json', 'pdf')
|
|
services: Services instance passed to renderer constructor
|
|
outputStyle: 'document' or 'code'. If None, returns the first match
|
|
with preference: document > code (most callers are document path).
|
|
"""
|
|
if not self._discovered:
|
|
self.discoverRenderers()
|
|
|
|
formatName = outputFormat.lower().strip()
|
|
if formatName in self._format_mappings:
|
|
formatName = self._format_mappings[formatName]
|
|
|
|
rendererClass = None
|
|
|
|
if outputStyle:
|
|
# Exact match by style
|
|
rendererClass = self._renderers.get((formatName, outputStyle))
|
|
else:
|
|
# No style specified — prefer 'document', then 'code', then any
|
|
for style in ['document', 'code']:
|
|
rendererClass = self._renderers.get((formatName, style))
|
|
if rendererClass:
|
|
break
|
|
# Fallback: check any registered style
|
|
if not rendererClass:
|
|
for key, cls in self._renderers.items():
|
|
if key[0] == formatName:
|
|
rendererClass = cls
|
|
break
|
|
|
|
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}, style={outputStyle}")
|
|
return None
|
|
|
|
def getSupportedFormats(self) -> List[str]:
|
|
"""Get list of all supported formats."""
|
|
if not self._discovered:
|
|
self.discoverRenderers()
|
|
|
|
formats = set()
|
|
for (fmt, _style) in self._renderers.keys():
|
|
formats.add(fmt)
|
|
formats.update(self._format_mappings.keys())
|
|
return sorted(formats)
|
|
|
|
def getRendererInfo(self) -> Dict[str, Dict[str, str]]:
|
|
"""Get information about all registered renderers."""
|
|
if not self._discovered:
|
|
self.discoverRenderers()
|
|
|
|
info = {}
|
|
for (formatName, style), rendererClass in self._renderers.items():
|
|
key = f"{formatName}:{style}"
|
|
info[key] = {
|
|
'class_name': rendererClass.__name__,
|
|
'module': rendererClass.__module__,
|
|
'outputStyle': style,
|
|
'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.
|
|
When both 'document' and 'code' renderers exist for a format,
|
|
returns the default ('document') since this is called during document generation.
|
|
"""
|
|
if not self._discovered:
|
|
self.discoverRenderers()
|
|
|
|
formatName = outputFormat.lower().strip()
|
|
if formatName in self._format_mappings:
|
|
formatName = self._format_mappings[formatName]
|
|
|
|
# Check document first, then code
|
|
for style in ['document', 'code']:
|
|
rendererClass = self._renderers.get((formatName, style))
|
|
if rendererClass:
|
|
try:
|
|
return rendererClass.getOutputStyle(formatName)
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback: any style
|
|
for key, rendererClass in self._renderers.items():
|
|
if key[0] == formatName:
|
|
try:
|
|
return rendererClass.getOutputStyle(formatName)
|
|
except Exception:
|
|
pass
|
|
|
|
logger.warning(f"No renderer found for format: {outputFormat}, cannot determine output style")
|
|
return None
|
|
|
|
|
|
# Global registry instance
|
|
_registry = RendererRegistry()
|
|
|
|
|
|
def getRenderer(outputFormat: str, services=None, outputStyle: str = None) -> Optional[BaseRenderer]:
|
|
"""Get a renderer instance for the specified format and style.
|
|
|
|
Args:
|
|
outputFormat: Format name (e.g. 'csv', 'json', 'pdf')
|
|
services: Services instance
|
|
outputStyle: 'document' or 'code'. If None, prefers document renderer.
|
|
"""
|
|
return _registry.getRenderer(outputFormat, services, outputStyle=outputStyle)
|
|
|
|
|
|
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)
|