Source code for yamlpars.parameters

import pathlib
from operator import getitem, setitem
from typing import TYPE_CHECKING, Any, Dict, List, MutableMapping, Union

from addict import Addict
from ruamel.yaml import YAML


[docs]class Parameters: """The Parameters object loads the yaml files from the folder specified in the constructor, and allows easy dot-notation access to the parameters, as well as an useful serialization methods. Attributes: yaml_folder (str | pathlib.Path): the folder containing the yaml files. auto_load (bool): if True, the yaml files are loaded automatically when the object is created. The default is True. Example: >>> pars = Parameters("path/to/yaml/folder") >>> pars.group_a.a_int 1 >>> pars.group_a.a_float 1.0 """ def __init__( self, yaml_folder: Union[str, pathlib.Path], auto_load: bool = True ) -> None: super().__init__() self._keys: List[str] = [] self._yaml_folder = pathlib.Path(yaml_folder) self._successful_load = False if auto_load: self.load()
[docs] def load(self) -> None: """Load parameters into the object by parsing the yaml files. Raises: FileNotFoundError: the folder containing the yaml files does not exist. """ yaml = YAML() if self._yaml_folder.exists(): for suffix in ["yaml", "yml"]: for file in self._yaml_folder.glob(f"*.{suffix}"): file_name = file.with_suffix("").name with open(file, "r", encoding="UTF-8") as file_handle: loaded_dict = yaml.load(file_handle) if loaded_dict is not None: self.__setattr__(file_name, Addict(loaded_dict)) self._keys.append(file_name) else: raise FileNotFoundError(f"The folder {self._yaml_folder} does not exist.") del yaml self._successful_load = True
@property def __dict__(self): """Return the parameters as a dictionary.""" if self._successful_load: return {key: self.__getattribute__(key) for key in self._keys} else: return {} def __repr__(self) -> str: """Return a string representation of the object.""" if self._successful_load: return str( f'Parameters object, loading from "{self._yaml_folder}":\n' + self.__dict__.__repr__() ) else: return f"Parameters object, did not load yaml folder yet." @staticmethod def _flatten_dict_gen( dict_to_flatten: MutableMapping[str, Any], parent_key: str, separator: str ): """https://www.freecodecamp.org/news/how-to-flatten-a-dictionary-in-python-in-4-different-ways/""" for k, v in dict_to_flatten.items(): new_key = parent_key + separator + k if parent_key else k if isinstance(v, MutableMapping): yield from Parameters._flatten_dict( v, new_key, separator=separator ).items() else: yield new_key, v @staticmethod def _flatten_dict( dict_to_flatten: MutableMapping, parent_key: str = "", separator: str = "." ) -> Dict[str, Any]: """https://www.freecodecamp.org/news/how-to-flatten-a-dictionary-in-python-in-4-different-ways/""" return dict( Parameters._flatten_dict_gen(dict_to_flatten, parent_key, separator) )
[docs] def serialize(self) -> Dict[str, Any]: """Return the parameters as a dictionary of depth one. Example: >>> pars.serialize() { 'group_a.a_int': 1, 'group_a.a_float': 1.0, 'group_a.a_bool': True, 'group_a.a_dictionary.first_key': 'first', 'group_a.a_dictionary.second_key': 'second', ... """ return Parameters._flatten_dict(self.__dict__)
[docs] def replace_entry(self, key: str, value: Any, type_check: bool = True) -> None: """Replace the value of a parameter. Args: key (str): the key of the parameter to replace. value (typing.Any): the new value of the parameter. type_check (bool): if True, the type of the value is checked against the type of the parameter, and an error is raised if the types do not match. The default is True. Example: >>> pars.replace_entry("group_a.a_dictionary.first_key", 'some_new_value') Raises: KeyError: the key does not exist in the parameters. TypeError: the type of the value does not match the type of the parameter. """ split_key = key.split(".") # First step is attr, next is items. head = split_key.pop(0) object_ref = getattr(self, head) if not isinstance(object_ref, dict): raise KeyError(f"Could not find {key} in the Parameters object.") while len(split_key) > 1: head = split_key.pop(0) object_ref = getitem(object_ref, head) if not isinstance(object_ref, dict): raise KeyError(f"Could not find {key} in the Parameters object.") if type_check: # Check that the type matches the type of the parameter. last_value = getitem(object_ref, split_key[0]) if not issubclass(type(last_value), type(value)): raise TypeError( f"The type of {key} in the Parameters object. is not the same as the type of the replace value. " + f"It should be {type(last_value)}, but it is {type(value)}." ) setitem(object_ref, split_key[0], value)
# This is to make mypy happy. if TYPE_CHECKING: def __getattribute__(self, __name: str) -> Any: """Return the value of the parameter.""" return super().__getattribute__(__name)