""" Configuration Management Module Provides YAML-based configuration loading with validation and path resolution. All system parameters are centralized in config files, avoiding hard-coded values. Design Decisions: - YAML format for human readability and easy editing - Relative paths resolved from project root - Deep merge support for config overrides - Validation of required fields Time Complexity: O(n) where n = number of config entries Space Complexity: O(n) for config dictionary storage """ import os from pathlib import Path from typing import Any, Dict, Optional import yaml def get_project_root() -> Path: """ Find the project root directory. Strategy: Walk up from this file until we find a directory containing 'config' folder or 'requirements.txt' (project markers). Returns: Path to project root directory Raises: RuntimeError: If project root cannot be determined """ current = Path(__file__).resolve().parent # Walk up directory tree for _ in range(10): # Limit search depth if (current / "config").is_dir() or (current / "requirements.txt").is_file(): return current parent = current.parent if parent == current: # Reached filesystem root break current = parent # Fallback: assume we're in src/utils, go up two levels fallback = Path(__file__).resolve().parent.parent.parent if fallback.is_dir(): return fallback raise RuntimeError( "Could not determine project root. " "Ensure you're running from within the project directory." ) def load_config(config_path: Optional[str] = None) -> Dict[str, Any]: """ Load configuration from YAML file. Args: config_path: Path to config file. If None, loads default.yaml Returns: Configuration dictionary with all parameters Raises: FileNotFoundError: If config file doesn't exist yaml.YAMLError: If config file is malformed """ project_root = get_project_root() if config_path is None: config_path = project_root / "config" / "default.yaml" else: config_path = Path(config_path) if not config_path.is_absolute(): config_path = project_root / config_path if not config_path.exists(): raise FileNotFoundError(f"Configuration file not found: {config_path}") with open(config_path, "r", encoding="utf-8") as f: config = yaml.safe_load(f) # Resolve relative paths to absolute paths config = _resolve_paths(config, project_root) # Add project root to config for convenience config["_project_root"] = str(project_root) return config def _resolve_paths(config: Dict[str, Any], project_root: Path) -> Dict[str, Any]: """ Resolve relative paths in config to absolute paths. Identifies path-like config values and resolves them relative to project root. Path-like values are those ending in '_dir' or '_path'. Args: config: Configuration dictionary project_root: Project root path for resolution Returns: Config with resolved paths """ path_suffixes = ("_dir", "_path", "_file") def resolve_recursive(obj: Any, parent_key: str = "") -> Any: if isinstance(obj, dict): return { k: resolve_recursive(v, k) for k, v in obj.items() } elif isinstance(obj, list): return [resolve_recursive(item, parent_key) for item in obj] elif isinstance(obj, str): # Check if this looks like a path if any(parent_key.endswith(suffix) for suffix in path_suffixes): path = Path(obj) if not path.is_absolute(): return str(project_root / path) return obj else: return obj return resolve_recursive(config) def save_config(config: Dict[str, Any], save_path: str) -> None: """ Save configuration to YAML file. Useful for saving experiment configurations for reproducibility. Args: config: Configuration dictionary save_path: Path to save config file """ save_path = Path(save_path) save_path.parent.mkdir(parents=True, exist_ok=True) # Remove internal keys before saving config_to_save = { k: v for k, v in config.items() if not k.startswith("_") } with open(save_path, "w", encoding="utf-8") as f: yaml.dump(config_to_save, f, default_flow_style=False, sort_keys=False) def merge_configs(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]: """ Deep merge two configuration dictionaries. Override values take precedence. Useful for command-line overrides. Args: base: Base configuration override: Override values Returns: Merged configuration """ result = base.copy() for key, value in override.items(): if key in result and isinstance(result[key], dict) and isinstance(value, dict): result[key] = merge_configs(result[key], value) else: result[key] = value return result