| """
|
| 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
|
|
|
|
|
| for _ in range(10):
|
| if (current / "config").is_dir() or (current / "requirements.txt").is_file():
|
| return current
|
| parent = current.parent
|
| if parent == current:
|
| break
|
| current = parent
|
|
|
|
|
| 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)
|
|
|
|
|
| config = _resolve_paths(config, project_root)
|
|
|
|
|
| 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):
|
|
|
| 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)
|
|
|
|
|
| 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
|
|
|