Source code for hydromt.config

"""config I/O functions."""

import abc
import codecs
from ast import literal_eval
from configparser import ConfigParser
from os.path import abspath, dirname, exists, join, splitext
from pathlib import Path
from typing import Dict, List, Optional, Union

import yaml
from tomli import load as load_toml
from tomli_w import dump as dump_toml

__all__ = [
    "configread",
    "configwrite",
]


def _process_config_out(d):
    ret = {}
    if isinstance(d, dict):
        for k, v in d.items():
            if v is None:
                ret[k] = "NONE"
            else:
                ret[k] = _process_config_out(v)
    else:
        ret = d

    return ret


def _process_config_in(d):
    ret = {}
    if isinstance(d, dict):
        for k, v in d.items():
            if v == "NONE":
                ret[k] = None
            else:
                ret[k] = _process_config_in(v)
    else:
        ret = d

    return ret


[docs] def configread( config_fn: Union[Path, str], defaults: Optional[Dict] = None, abs_path: bool = False, skip_abspath_sections: Optional[List] = None, **kwargs, ) -> Dict: """Read configuration/workflow file and parse to (nested) dictionary. Parameters ---------- config_fn : Union[Path, str] Path to configuration file defaults : dict, optional Nested dictionary with default options, by default dict() abs_path : bool, optional If True, parse string values to an absolute path if the a file or folder with that name (string value) relative to the config file exist, by default False skip_abspath_sections: list, optional These sections are not evaluated for absolute paths if abs_path=True, by default ['update_config'] **kwargs Additional keyword arguments that are passed to the read_ini` function. Returns ------- cfdict : dict Configuration dictionary. """ defaults = defaults or {} skip_abspath_sections = skip_abspath_sections or ["setup_config"] # read ext = splitext(config_fn)[-1].strip() if ext in [".yaml", ".yml"]: with open(config_fn, "rb") as f: cfdict = yaml.safe_load(f) cfdict = _process_config_in(cfdict) elif ext == ".toml": # user defined with open(config_fn, "rb") as f: cfdict = load_toml(f) cfdict = _process_config_in(cfdict) else: cfdict = read_ini_config(config_fn, **kwargs) # parse absolute paths if abs_path: root = Path(dirname(config_fn)) cfdict = parse_abspath(cfdict, root, skip_abspath_sections) # update defaults if defaults: _cfdict = defaults.copy() _cfdict.update(cfdict) cfdict = _cfdict return cfdict
[docs] def configwrite(config_fn: Union[str, Path], cfdict: dict, **kwargs) -> None: """Write configuration/workflow dictionary to file. Parameters ---------- config_fn : Union[Path, str] Path to configuration file cfdict : dict Configuration dictionary. If the configuration contains headers, the first level keys are the section headers, the second level option-value pairs. encoding : str, optional File encoding, by default "utf-8" cf : ConfigParser, optional Alternative configuration parser, by default None noheader : bool, optional Set true for a single-level configuration dictionary with no headers, by default False **kwargs Additional keyword arguments that are passed to the `write_ini_config` function. """ root = Path(dirname(config_fn)) _cfdict = parse_relpath(cfdict.copy(), root) ext = splitext(config_fn)[-1].strip() if ext in [".yaml", ".yml"]: _cfdict = _process_config_out(_cfdict) # should not be done for ini with open(config_fn, "w") as f: yaml.dump(_cfdict, f, sort_keys=False) elif ext == ".toml": # user defined _cfdict = _process_config_out(_cfdict) with open(config_fn, "wb") as f: dump_toml(_cfdict, f) else: write_ini_config(config_fn, _cfdict, **kwargs)
def read_ini_config( config_fn: Union[Path, str], encoding: str = "utf-8", cf: ConfigParser = None, skip_eval: bool = False, skip_eval_sections: Optional[list] = None, noheader: bool = False, ) -> dict: """Read configuration ini file and parse to (nested) dictionary. Parameters ---------- config_fn : Union[Path, str] Path to configuration file encoding : str, optional File encoding, by default "utf-8" cf : ConfigParser, optional Alternative configuration parser, by default None skip_eval : bool, optional If True, do not evaluate string values, by default False skip_eval_sections : list, optional These sections are not evaluated for string values if skip_eval=True, by default [] noheader : bool, optional Set true for a single-level configuration file with no headers, by default False Returns ------- cfdict : dict Configuration dictionary. """ skip_eval_sections = skip_eval_sections or [] if cf is None: cf = ConfigParser(allow_no_value=True, inline_comment_prefixes=[";", "#"]) elif isinstance(cf, abc.ABCMeta): # not yet instantiated cf = cf() cf.optionxform = str # preserve capital letter with codecs.open(config_fn, "r", encoding=encoding) as fp: cf.read_file(fp) cfdict = cf._sections # parse values cfdict = parse_values(cfdict, skip_eval, skip_eval_sections) # add dummy header if noheader and "dummy" in cfdict: cfdict = cfdict["dummy"] return cfdict def write_ini_config( config_fn: Union[Path, str], cfdict: dict, encoding: str = "utf-8", cf: ConfigParser = None, noheader: bool = False, ) -> None: """Write configuration dictionary to ini file. Parameters ---------- config_fn : Union[Path, str] Path to configuration file cfdict : dict Configuration dictionary. encoding : str, optional File encoding, by default "utf-8" cf : ConfigParser, optional Alternative configuration parser, by default None noheader : bool, optional Set true for a single-level configuration dictionary with no headers, by default False """ if cf is None: cf = ConfigParser(allow_no_value=True, inline_comment_prefixes=[";", "#"]) elif isinstance(cf, abc.ABCMeta): # not yet instantiated cf = cf() cf.optionxform = str # preserve capital letter if noheader: # add dummy header cfdict = {"dummy": cfdict} cf.read_dict(cfdict) with codecs.open(config_fn, "w", encoding=encoding) as fp: cf.write(fp) def parse_relpath(cfdict: dict, root: Path) -> dict: """Parse string/path value to relative path if possible.""" def _relpath(value, root): if isinstance(value, str) and str(Path(value)).startswith(str(root)): value = Path(value) if isinstance(value, Path): try: rel_path = value.relative_to(root) value = str(rel_path).replace("\\", "/") except ValueError: pass # `value` path is not relative to root return value # loop through n-level of dict for key, val in cfdict.items(): if isinstance(val, dict): cfdict[key] = parse_relpath(val, root) else: cfdict[key] = _relpath(val, root) return cfdict def parse_abspath( cfdict: dict, root: Path, skip_abspath_sections: Optional[List] = None ) -> dict: """Parse string value to absolute path from config file.""" skip_abspath_sections = skip_abspath_sections or ["setup_config"] def _abspath(value, root): if exists(join(root, value)): value = Path(abspath(join(root, value))) return value # loop through n-level of dict for key, val in cfdict.items(): if isinstance(val, dict): if key not in skip_abspath_sections: cfdict[key] = parse_abspath(val, root) elif isinstance(val, list) and all([isinstance(v, str) for v in val]): cfdict[key] = [_abspath(v, root) for v in val] elif isinstance(val, str): cfdict[key] = _abspath(val, root) return cfdict def parse_values( cfdict: dict, skip_eval: bool = False, skip_eval_sections: Optional[List] = None, ): """Parse string values to python default objects. Parameters ---------- cfdict : dict Configuration dictionary. skip_eval : bool, optional Set true to skip evaluation, by default False skip_eval_sections : List, optional List of sections to skip evaluation, by default [] Returns ------- cfdict : dict Configuration dictionary with evaluated values. """ skip_eval_sections = skip_eval_sections or [] # loop through two-level dict: section, key-value pairs for section in cfdict: # evaluate ini items to parse to python default objects: if skip_eval or section in skip_eval_sections: cfdict[section].update( {key: str(var) for key, var in cfdict[section].items()} ) # cast None type values to str continue # do not evaluate # numbers, tuples, lists, dicts, sets, booleans, and None for key, value in cfdict[section].items(): try: value = literal_eval(value) except Exception: pass if isinstance(value, str) and len(value) == 0: value = None cfdict[section].update({key: value}) return cfdict