Source code for lhp.presets.preset_manager

"""Preset management with hierarchical inheritance."""

import logging
from pathlib import Path
from typing import Any, Dict, List, Optional

from lhp.models import Preset

from ..errors import ErrorFactory, codes
from ..parsers.yaml_parser import YAMLParser
from ..utils.performance_timer import perf_timer

logger = logging.getLogger(__name__)


[docs] class PresetManager: def __init__(self, presets_dir: Path): self.presets_dir = presets_dir self.presets: Dict[str, Preset] = {} self.parser = YAMLParser() self._load_presets() def _load_presets(self): if not self.presets_dir.exists(): logger.debug(f"Presets directory does not exist: {self.presets_dir}") return presets = self.parser.discover_presets(self.presets_dir) for preset in presets: logger.debug(f"Loaded preset '{preset.name}' from {self.presets_dir}") self.presets[preset.name] = preset logger.info(f"Discovered {len(self.presets)} preset(s) from {self.presets_dir}") def resolve_preset_chain(self, preset_names: List[str]) -> Dict[str, Any]: with perf_timer("preset_resolve", category="preset_resolve"): logger.debug(f"Resolving preset chain: {preset_names}") resolved = {} for preset_name in preset_names: preset_config = self._resolve_preset_inheritance(preset_name) resolved = self._deep_merge(resolved, preset_config) logger.debug(f"Preset chain resolved with {len(resolved)} top-level keys") return resolved def _resolve_preset_inheritance( self, preset_name: str, visited: Optional[set] = None, path: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Args: visited: Set of already-visited preset names (cycle detection) path: Ordered list of visited preset names (deterministic cycle path) Raises: LHPConfigError: If the preset is not found (LHP-ACT-001). LHPError: If circular inheritance is detected (LHP-DEP-022). """ if visited is None: visited = set() if path is None: path = [] if preset_name in visited: cycle_path = " -> ".join([*path, preset_name]) raise ErrorFactory.dependency_error( codes.DEP_022, title="Circular preset inheritance detected", details=( f"Preset '{preset_name}' creates a circular inheritance chain: {cycle_path}" ), suggestions=[ "Remove the circular 'extends' reference in one of the presets", "Review the preset inheritance chain for unintended cycles", ], context={"Preset": preset_name, "Chain": cycle_path}, ) if preset_name not in self.presets: raise ErrorFactory.preset_not_found( preset_name=preset_name, available_presets=sorted(self.presets.keys()), ) preset = self.presets[preset_name] result = preset.defaults or {} if preset.extends: logger.debug( f"Preset '{preset_name}' extends '{preset.extends}', resolving parent" ) parent_config = self._resolve_preset_inheritance( preset.extends, visited | {preset_name}, [*path, preset_name] ) result = self._deep_merge(parent_config, result) return result def _deep_merge( self, base: Dict[str, Any], override: Dict[str, Any] ) -> Dict[str, Any]: result = base.copy() for key, value in override.items(): if ( key in result and isinstance(result[key], dict) and isinstance(value, dict) ): logger.debug(f"Deep merging nested dict for key '{key}'") result[key] = self._deep_merge(result[key], value) elif ( key == "operational_metadata" and isinstance(value, list) and isinstance(result.get(key), list) ): # Special handling for operational_metadata lists - combine them result[key] = list( dict.fromkeys(result[key] + value) ) # Order-preserving dedup logger.debug( f"Merged operational_metadata lists: {len(result[key])} entries after dedup" ) else: result[key] = value return result def list_presets(self) -> List[str]: return list(self.presets.keys())