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)