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)