Tri-Netra-AI / src /config_loader.py
anannyavyas1's picture
Upload folder using huggingface_hub
1cf3825 verified
Raw
History Blame Contribute Delete
5.03 kB
"""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