"""Preset management with hierarchical inheritance."""importloggingfrompathlibimportPathfromtypingimportAny,Dict,List,Optionalfromlhp.modelsimportPresetfrom..errorsimportErrorFactory,codesfrom..parsers.yaml_parserimportYAMLParserfrom..utils.performance_timerimportperf_timerlogger=logging.getLogger(__name__)
[docs]classPresetManager:def__init__(self,presets_dir:Path):self.presets_dir=presets_dirself.presets:Dict[str,Preset]={}self.parser=YAMLParser()self._load_presets()def_load_presets(self):ifnotself.presets_dir.exists():logger.debug(f"Presets directory does not exist: {self.presets_dir}")returnpresets=self.parser.discover_presets(self.presets_dir)forpresetinpresets:logger.debug(f"Loaded preset '{preset.name}' from {self.presets_dir}")self.presets[preset.name]=presetlogger.info(f"Discovered {len(self.presets)} preset(s) from {self.presets_dir}")defresolve_preset_chain(self,preset_names:List[str])->Dict[str,Any]:withperf_timer("preset_resolve",category="preset_resolve"):logger.debug(f"Resolving preset chain: {preset_names}")resolved={}forpreset_nameinpreset_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")returnresolveddef_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). """ifvisitedisNone:visited=set()ifpathisNone:path=[]ifpreset_nameinvisited:cycle_path=" -> ".join([*path,preset_name])raiseErrorFactory.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},)ifpreset_namenotinself.presets:raiseErrorFactory.preset_not_found(preset_name=preset_name,available_presets=sorted(self.presets.keys()),)preset=self.presets[preset_name]result=preset.defaultsor{}ifpreset.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)returnresultdef_deep_merge(self,base:Dict[str,Any],override:Dict[str,Any])->Dict[str,Any]:result=base.copy()forkey,valueinoverride.items():if(keyinresultandisinstance(result[key],dict)andisinstance(value,dict)):logger.debug(f"Deep merging nested dict for key '{key}'")result[key]=self._deep_merge(result[key],value)elif(key=="operational_metadata"andisinstance(value,list)andisinstance(result.get(key),list)):# Special handling for operational_metadata lists - combine themresult[key]=list(dict.fromkeys(result[key]+value))# Order-preserving deduplogger.debug(f"Merged operational_metadata lists: {len(result[key])} entries after dedup")else:result[key]=valuereturnresultdeflist_presets(self)->List[str]:returnlist(self.presets.keys())