File size: 1,961 Bytes
c87881a | 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 | """Configuration loading with explicit path resolution and validation."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import yaml
@dataclass(frozen=True)
class LoadedConfig:
path: Path
values: dict[str, Any]
@property
def base_dir(self) -> Path:
return self.path.parent.parent
def resolve_path(self, section: str, key: str) -> Path:
value = self.values[section][key]
path = Path(value)
if not path.is_absolute():
path = (self.base_dir / path).resolve()
return path
def _merge_values(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
merged = dict(base)
for key, value in override.items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = _merge_values(merged[key], value)
else:
merged[key] = value
return merged
def _read_config(config_path: Path, seen: set[Path]) -> dict[str, Any]:
if config_path in seen:
raise ValueError(f"Configuration inheritance cycle at {config_path}")
with config_path.open("r", encoding="utf-8") as handle:
values = yaml.safe_load(handle) or {}
parent = values.pop("extends", None)
if parent is None:
return values
parent_path = Path(parent)
if not parent_path.is_absolute():
parent_path = (config_path.parent / parent_path).resolve()
return _merge_values(_read_config(parent_path, seen | {config_path}), values)
def load_config(path: str | Path) -> LoadedConfig:
config_path = Path(path).resolve()
values = _read_config(config_path, set())
required = {"project", "data", "model", "training", "evaluation"}
missing = required.difference(values)
if missing:
raise ValueError(f"Configuration is missing sections: {sorted(missing)}")
return LoadedConfig(path=config_path, values=values)
|