File size: 5,519 Bytes
c2a61b6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | """
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
|