Source code for viewtext.engine

"""
Layout engine for building text grid layouts from configuration and context.

This module provides the LayoutEngine class that builds formatted text
layouts by combining field registries, formatters, and layout configurations.
"""

from typing import TYPE_CHECKING, Any, Optional

from .formatters import get_formatter_registry
from .registry import BaseFieldRegistry

if TYPE_CHECKING:
    from .loader import LayoutLoader


[docs] class LayoutEngine: """ Engine for building text grid layouts from configuration and context data. The LayoutEngine combines a field registry and formatter registry to build formatted text layouts according to TOML layout configurations. Parameters ---------- field_registry : BaseFieldRegistry, optional Registry of field getter functions. If None, fields are retrieved directly from the context dictionary. Attributes ---------- field_registry : BaseFieldRegistry or None The field registry for resolving field values formatter_registry : FormatterRegistry The formatter registry for formatting values Examples -------- >>> from viewtext import LayoutEngine, BaseFieldRegistry >>> registry = BaseFieldRegistry() >>> registry.register("temp", lambda ctx: ctx["temperature"]) >>> engine = LayoutEngine(field_registry=registry) >>> layout = { ... "lines": [ ... {"field": "temp", "index": 0, "formatter": "number", ... "formatter_params": {"decimals": 1}} ... ] ... } >>> result = engine.build_line_str(layout, {"temperature": 23.456}) >>> result ['23.5'] """
[docs] def __init__( self, field_registry: Optional[BaseFieldRegistry] = None, layout_loader: Optional["LayoutLoader"] = None, ): """ Initialize the layout engine. Parameters ---------- field_registry : BaseFieldRegistry, optional Registry of field getter functions layout_loader : LayoutLoader, optional Layout loader for resolving formatter presets """ self.field_registry = field_registry self.formatter_registry = get_formatter_registry() self.layout_loader = layout_loader
[docs] def build_line_str( self, layout_config: dict[str, Any], context: dict[str, Any] ) -> list[str]: """ Build formatted text lines from layout configuration and context. Parameters ---------- layout_config : dict[str, Any] Layout configuration dictionary containing "lines" list context : dict[str, Any] Context dictionary containing data values Returns ------- list[str] List of formatted text lines Examples -------- >>> engine = LayoutEngine() >>> layout = { ... "lines": [ ... {"field": "name", "index": 0}, ... {"field": "age", "index": 1} ... ] ... } >>> result = engine.build_line_str(layout, {"name": "John", "age": 30}) >>> result ['John', '30'] """ lines = layout_config.get("lines", []) max_index = max((line.get("index", 0) for line in lines), default=0) line_str = [""] * (max_index + 1) for line_config in lines: index = line_config.get("index") field_name = line_config.get("field") formatter_name = line_config.get("formatter") formatter_params = line_config.get("formatter_params", {}) if index is None or field_name is None: continue value = self._get_field_value(field_name, context) if formatter_name: value = self._format_value( value, formatter_name, formatter_params, context ) if index < len(line_str): line_str[index] = str(value) if value is not None else "" return line_str
[docs] def build_dict_str( self, layout_config: dict[str, Any], context: dict[str, Any] ) -> dict[str, str]: """ Build formatted dictionary from layout configuration and context. Parameters ---------- layout_config : dict[str, Any] Layout configuration dictionary containing "items" list context : dict[str, Any] Context dictionary containing data values Returns ------- dict[str, str] Dictionary mapping keys to formatted values Examples -------- >>> engine = LayoutEngine() >>> layout = { ... "items": [ ... {"field": "temp", "key": "temperature", ... "formatter": "number", "formatter_params": {"suffix": "°"}}, ... {"field": "price", "key": "cost", "formatter": "price"} ... ] ... } >>> result = engine.build_dict_str(layout, {"temp": 31, "price": 32}) >>> result {'temperature': '31°', 'cost': '$32.00'} """ items = layout_config.get("items", []) result = {} for item_config in items: key = item_config.get("key") field_name = item_config.get("field") formatter_name = item_config.get("formatter") formatter_params = item_config.get("formatter_params", {}) if key is None or field_name is None: continue value = self._get_field_value(field_name, context) if formatter_name: value = self._format_value( value, formatter_name, formatter_params, context ) result[key] = str(value) if value is not None else "" return result
def _get_field_value(self, field_name: str, context: dict[str, Any]) -> Any: """ Get field value from registry or context. Parameters ---------- field_name : str Name of the field to retrieve context : dict[str, Any] Context dictionary Returns ------- Any The field value, or None if not found """ if self.field_registry and self.field_registry.has_field(field_name): getter = self.field_registry.get(field_name) return getter(context) elif field_name in context: return context[field_name] else: return None def _format_value( self, value: Any, formatter_name: str, formatter_params: dict[str, Any], context: Optional[dict[str, Any]] = None, ) -> Any: """ Format a value using the specified formatter. Parameters ---------- value : Any The value to format formatter_name : str Name of the formatter to use (can be a preset reference) formatter_params : dict[str, Any] Parameters to pass to the formatter context : dict[str, Any], optional Context dictionary for template formatter Returns ------- Any The formatted value """ if not formatter_params: formatter_params = {} if self.layout_loader and not formatter_params: preset = self.layout_loader.get_formatter_preset(formatter_name) if preset: formatter_type = preset.get("type", formatter_name) formatter_params = preset.copy() formatter_params.pop("type", None) else: formatter_type = formatter_name else: formatter_type = formatter_params.get("type", formatter_name) try: formatter = self.formatter_registry.get(formatter_type) except ValueError: formatter = self.formatter_registry.get("text") if formatter_type == "template" and context is not None: formatter_params = { **formatter_params, "_context": context, "_engine": self, "_loader": self.layout_loader, } return formatter(value, **formatter_params)
_global_layout_engine: Optional[LayoutEngine] = None
[docs] def get_layout_engine( field_registry: Optional[BaseFieldRegistry] = None, ) -> LayoutEngine: """ Get or create the global layout engine instance. Parameters ---------- field_registry : BaseFieldRegistry, optional Registry to use when creating a new engine instance Returns ------- LayoutEngine The global layout engine instance Raises ------ ValueError If no global engine exists and no field_registry is provided Examples -------- >>> from viewtext import get_layout_engine, BaseFieldRegistry >>> registry = BaseFieldRegistry() >>> engine = get_layout_engine(field_registry=registry) """ global _global_layout_engine if _global_layout_engine is None and field_registry is not None: _global_layout_engine = LayoutEngine(field_registry) if _global_layout_engine is None: raise ValueError("LayoutEngine not initialized. Provide a field_registry.") return _global_layout_engine