from __future__ import annotations from pathlib import Path from typing import Any import yaml def load_yaml(path: str | Path) -> dict[str, Any]: path = Path(path) with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def deep_update(base: dict, update: dict) -> dict: """ Recursively merge update into base. """ for key, value in update.items(): if ( key in base and isinstance(base[key], dict) and isinstance(value, dict) ): deep_update(base[key], value) else: base[key] = value return base def load_config(config_path: str | Path) -> dict[str, Any]: """ Load a config file and recursively merge files listed under `defaults`. Example: defaults: - ../paths.yaml - ../data/coco_256.yaml """ config_path = Path(config_path) cfg = load_yaml(config_path) merged: dict[str, Any] = {} defaults = cfg.pop("defaults", []) for default_path in defaults: default_file = (config_path.parent / default_path).resolve() default_cfg = load_config(default_file) merged = deep_update(merged, default_cfg) merged = deep_update(merged, cfg) return merged def get_by_key(cfg: dict[str, Any], key: str) -> Any: """ Get nested config value using dot notation. Example: get_by_key(cfg, "data.coco_root") """ value: Any = cfg for part in key.split("."): value = value[part] return value def resolve_path_key(cfg: dict[str, Any], key: str) -> Path: """ Resolve a path stored in config using dot notation. """ return Path(get_by_key(cfg, key)).expanduser().resolve() def save_yaml(data: dict, path: str | Path) -> None: path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w", encoding="utf-8") as f: yaml.safe_dump(data, f, sort_keys=False)