"""Basic utilities for structuring experiment configurations based on Pydantic schemas.""" import os import re from abc import abstractmethod from io import TextIOBase from typing import Annotated, Type, Dict, TextIO, Callable, cast import yaml from pydantic import BaseModel, IPvAnyAddress, AfterValidator, TypeAdapter from typing_extensions import Generic, overload from benchmarks.core.experiments.experiments import TExperiment, Experiment def drop_config_suffix(name: str) -> str: return name[:-6] if name.endswith('Config') else name def to_snake_case(name: str) -> str: return re.sub(r'(? TExperiment: pass class ConfigParser: """ :class:`ConfigParser` is a utility class to parse configuration files into :class:`ExperimentBuilder`s. Currently, each :class:`ExperimentBuilder` can appear at most once in the config file. """ def __init__(self): self.root_tags = {} def register(self, root: Type[ExperimentBuilder[TExperiment]]): name = root.__name__ alias = cast(Callable[[str], str], root.model_config.get('alias_generator', lambda x: x))(name) self.root_tags[alias] = root @overload def parse(self, data: dict) -> Dict[str, ExperimentBuilder[TExperiment]]: ... @overload def parse(self, data: TextIO) -> Dict[str, ExperimentBuilder[TExperiment]]: ... def parse(self, data: dict | TextIO) -> Dict[str, ExperimentBuilder[TExperiment]]: if isinstance(data, TextIOBase): entries = yaml.safe_load(os.path.expandvars(data.read())) else: entries = data return { tag: self.root_tags[tag].model_validate(config) for tag, config in entries.items() }