Source code for haddock.gear.yaml2cfg

"""
Parse data from HADDOCK3 to YAML and YAML to HADDOCK3 and related.

Accross this file you will see references to "yaml" as variables and
function names. In these cases, we always mean the HADDOCK3 YAML
configuration files which have specific keys.
"""

import os
from collections.abc import Mapping
from pathlib import Path
from typing import Union

from haddock import _hidden_level, config_expert_levels
from haddock.core.exceptions import ConfigurationError
from haddock.core.typing import (
    ExpertLevel,
    FilePath,
    Optional,
    ParamDict,
    ParamMap,
)
from haddock.libs.libio import read_from_yaml


[docs] def yaml2cfg_text( ymlcfg: dict, module: str, explevel: str, details: bool = False, mandatory_param: bool = False, ) -> str: """ Convert HADDOCK3 YAML config to HADDOCK3 user config text. Adds commentaries with help strings. Parameters ---------- ymlcfg : dict The dictionary representing the HADDOCK3 config file. module : str The module to which the config belongs to. explevel : str The expert level to consider. Provides all parameters for that level and those of inferior hierarchy. If you give "all", all parameters will be considered. details : bool Whether to add the 'long' description of each parameter. mandatory_param : bool Whether this current parameters are mandatory ones. Special case where this must be set as they do not contain `default` value, therefore the downstream functions are not valid anymore. Returns ------- textual_config : str Textual representation of a YAML configuration file. """ new_config: list[str] = [] if module is not None: new_config.append(f"[{module}]") new_config.append( _yaml2cfg_text( ymlcfg, module, explevel, details=details, mandatory_param=mandatory_param, ) ) textual_config = os.linesep.join(new_config) + os.linesep return textual_config
def _yaml2cfg_text( ymlcfg: dict, module: str, explevel: str, details: bool = False, mandatory_param: bool = False, ) -> str: """ Convert HADDOCK3 YAML config to HADDOCK3 user config text. Does not consider expert levels. See :func:`yaml2cfg_text_with_explevels` instead. Parameters ---------- ymlcfg : dict The dictionary representing the HADDOCK3 YAML configuration. This configuration should NOT have the expertise levels. It expectes the first level of keys to be the parameter name. module : str The module to which the config belongs to. explevel : str The expert level to consider. Provides all parameters for that level and those of inferior hierarchy. If you give "all", all parameters will be considered. details : bool Whether to add the 'long' description of each parameter. mandatory_param : bool Whether this current parameters are mandatory ones. Special case where this must be set as they do not contain `default` value, therefore the downstream functions are not valid anymore. Returns ------- str_config : str String representation of the YAML configuration file. """ params: list[str] = [] exp_levels = { _el: i for i, _el in enumerate(config_expert_levels + ("all", _hidden_level)) } exp_level_idx = exp_levels[explevel] # define set of undesired parameter keys undesired = ["default", "explevel", "type"] if not details: # Add long description to undesired if not asked for detailed info undesired.append("long") for param_name, param in ymlcfg.items(): # Check if param is a dictionary if isinstance(param, Mapping): # Without `default` key parameter, assumes this `param` # is a subdictionaries of parameters if "default" not in param and not mandatory_param: params.append("") # give extra space if module is not None: curr_module = f"{module}.{param_name}" else: curr_module = param_name params.append(f"[{curr_module}]") _ = _yaml2cfg_text( param, # type: ignore module=curr_module, explevel=explevel, details=details, ) params.append(_) # treats normal parameters else: if exp_levels[param["explevel"]] > exp_level_idx: # ignore this parameter because is of an expert level # superior to the one request: continue comment: list[str] = [] for _comment, cvalue in param.items(): if _comment in undesired: continue if cvalue == "": continue comment.append(f"${_comment} {cvalue}") # In the case of mandatory global parameters, there is # no defined default parameters, so we create a `fake` one if mandatory_param: default_value = "Must be defined!" else: default_value = param["default"] # boolean values have to be lower for compatibility # with toml cfg if isinstance(default_value, bool): default_value = str(default_value).lower() param_line = "{} = {} # {}" else: param_line = "{} = {!r} # {}" params.append( param_line.format( param_name, default_value, " / ".join(comment), ) ) if param["type"] == "list": params.append(os.linesep) else: # ignore some other parameters that are defined for sections. continue # Generate one single string containing all parameters representation str_config = os.linesep.join(params) return str_config
[docs] def read_from_yaml_config( cfg_file: Union[Path, str], default_only: bool = True ) -> dict: """Read config from yaml by collapsing the expert levels. Parameters ---------- cfg_file : Path to a .yaml configuration file default_only : bool Set the return value of this function; if True (default value), only returns default values, else return the fully loaded configuration file Return ------ ycfg : dict The full default configuration file as a dict OR cfg : dict A dictionary containing only the default parameters values """ ycfg = read_from_yaml(cfg_file) if default_only: # there's no need to make a deep copy here, a shallow copy suffices. cfg = {} cfg.update(flat_yaml_cfg(ycfg)) return cfg return ycfg
[docs] def flat_yaml_cfg(cfg: ParamMap) -> ParamDict: """Flat a yaml config.""" new: ParamDict = {} for param, values in cfg.items(): try: new_value = values["default"] except KeyError: new_value = flat_yaml_cfg(values) except TypeError: # happens when values is a string for example, # addresses `explevel` in `mol*` topoaa. continue else: # coordinates with tests. # users should not edit yaml files. So this error triggers # only during development if "explevel" not in values: emsg = f"`explevel` not defined for: {param!r}" raise ConfigurationError(emsg) new[param] = new_value return new
[docs] def find_incompatible_parameters(yaml_file: Path) -> dict[str, ParamDict]: """ Reads a YAML configuration file and identifies nodes containing the 'incompatible' key. This function takes the path to a YAML file, reads its contents, and searches for nodes that include an 'incompatible' key. It returns a dictionary where each key is a node name and each value is another dictionary representing the 'incompatible' parameters for that node. Parameters ---------- yaml_file : Path The path to the YAML file to be read. Return ------ incompatible_parameters : dict[str, dict[str, str]] A dictionary with node names as keys and their corresponding 'incompatible' parameters as values. """ config = read_from_yaml(yaml_file=yaml_file) # Go over each node and see which contains the `incompatible` key incompatible_parameters = {} for node, values in config.items(): if "incompatible" in values: incompatible_parameters[node] = values["incompatible"] return incompatible_parameters