Spaces:
Runtime error
Runtime error
| """Central loader for config.yaml. | |
| Previously nothing in the repo actually read config.yaml — every script used | |
| argparse defaults instead, so the YAML file was decorative. This module loads | |
| config.yaml once and exposes: | |
| load_config(path=None) -> dict | |
| section(name, default=None) -> dict | None | |
| merge_args(args, section_name, mapping=None) -> Namespace | |
| `merge_args` lets training scripts use config values as defaults and override | |
| them from the CLI. Example: | |
| from src.config_loader import merge_args | |
| args = parser.parse_args() | |
| args = merge_args(args, 'training') | |
| Only the sections whose consumers exist in the repo are considered "live": | |
| data, model, training, kfold, ablation, evaluation, experiment, paths, | |
| hardware, random, robustness, uncertainty, multiclass. | |
| The 3D-MRI / federated / SSL sections still exist in config.yaml as reference | |
| parameter sets used by the corresponding classes in src/advanced_models.py. | |
| They are no longer silently ignored — when the relevant module is invoked, it | |
| can fetch its section from here. | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import Any, Mapping, Optional | |
| import yaml | |
| _DEFAULT_CONFIG_PATH = Path(__file__).resolve().parents[1] / 'config.yaml' | |
| _CACHE: dict[str, Any] = {} | |
| def load_config(path: Optional[str | Path] = None) -> dict: | |
| """Load and cache config.yaml. Pass a path to override the default location.""" | |
| path = Path(path) if path is not None else _DEFAULT_CONFIG_PATH | |
| cache_key = str(path.resolve()) | |
| if cache_key in _CACHE: | |
| return _CACHE[cache_key] | |
| if not path.exists(): | |
| raise FileNotFoundError(f'Config file not found: {path}') | |
| with path.open('r', encoding='utf-8') as fh: | |
| cfg = yaml.safe_load(fh) or {} | |
| _CACHE[cache_key] = cfg | |
| return cfg | |
| def section(name: str, default: Optional[Mapping[str, Any]] = None, *, path: Optional[str | Path] = None) -> dict: | |
| """Return a top-level section of the config, or the default if missing.""" | |
| cfg = load_config(path) | |
| value = cfg.get(name) | |
| if value is None: | |
| return dict(default) if default is not None else {} | |
| if not isinstance(value, Mapping): | |
| raise TypeError(f'Config section {name!r} must be a mapping, got {type(value).__name__}.') | |
| return dict(value) | |
| def merge_args(args, section_name: str, mapping: Optional[Mapping[str, str]] = None, *, path: Optional[str | Path] = None): | |
| """Fill in argparse defaults from the named config section. | |
| `mapping` maps `argparse_attr_name -> config_key_name`. If None, attributes | |
| are matched against YAML keys of the same name. | |
| A CLI value (anything that differs from the argparse default) wins over the | |
| YAML value. The argparse default itself is treated as "unset" so YAML | |
| overrides it. To detect "default vs explicit", we compare against the | |
| parser's recorded defaults; that requires the caller to pass the parser via | |
| args._parser, OR to call this BEFORE parse_args by supplying the parser | |
| object directly (see set_yaml_defaults). | |
| """ | |
| cfg = section(section_name, path=path) | |
| if not cfg: | |
| return args | |
| keys = mapping or {k: k for k in vars(args).keys() if k in cfg} | |
| for attr, yaml_key in keys.items(): | |
| if yaml_key not in cfg: | |
| continue | |
| # Only override if the user did not pass the flag (i.e. attr equals the | |
| # parser's stored default). When we can't tell, prefer the CLI value. | |
| current = getattr(args, attr, None) | |
| parser_defaults = getattr(args, '_parser_defaults', {}) | |
| default = parser_defaults.get(attr) | |
| if current == default: | |
| setattr(args, attr, cfg[yaml_key]) | |
| return args | |
| def set_yaml_defaults(parser, section_name: str, mapping: Optional[Mapping[str, str]] = None, *, path: Optional[str | Path] = None): | |
| """Apply YAML values as argparse defaults BEFORE parse_args is called. | |
| Preferred over merge_args when you control parser construction. Example: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('--epochs', type=int, default=10) | |
| set_yaml_defaults(parser, 'training') # may set parser default to 100 | |
| args = parser.parse_args() # CLI still overrides | |
| Returns the parser for chaining. | |
| """ | |
| cfg = section(section_name, path=path) | |
| if not cfg: | |
| return parser | |
| if mapping is None: | |
| # Match by action.dest -> yaml key of the same name. | |
| for action in parser._actions: # noqa: SLF001 | |
| if action.dest in cfg: | |
| action.default = cfg[action.dest] | |
| return parser | |
| for dest, yaml_key in mapping.items(): | |
| if yaml_key in cfg: | |
| for action in parser._actions: # noqa: SLF001 | |
| if action.dest == dest: | |
| action.default = cfg[yaml_key] | |
| break | |
| return parser | |