from __future__ import annotations import json from pathlib import Path from typing import Dict, Optional, Union, List import yaml def clean_list(list_: List) -> Optional[List]: """ Recursively removes all entries from list that are None, empty dictionaries or empty lists. Dicts ore lists that are None after cleaning will also be removed Typically applied before dumping data to JSON where None values are default and will be restored on read-in 'automatically' :return: :param list_: Any list :return: """ ret = [] for elem in list_: if type(elem) == list: ret.append(clean_list(elem)) elif type(elem) == dict: ret.append(clean_dict(elem)) elif elem is not None: ret.append(elem) return ret if ret != [] else None def clean_dict(dictionary: Dict) -> Optional[Dict]: """ Recursively removes all entries from dictionary that are None, empty dictionaries or empty lists. Keys whose value is a dict / list that only contains non-valued keys will then also be removed. Typically applied before dumping data to JSON where None values are default and will be restored on read-in 'automatically' :param dictionary: Any dictionary :return: """ aux: Dict = { k: clean_dict(v) for k, v in dictionary.items() if type(v) == dict } | { k: clean_list(v) for k, v in dictionary.items() if type(v) == list } | { k: v for k, v in dictionary.items() if type(v) != dict and type(v) != list } aux2: Dict = { k: v for k, v in aux.items() if v is not None } return aux2 if aux2 != {} and aux2 != [] else None class Config: def merge_with(self, other: Config, strict: bool = False): """ Merges the other config into this one :param other: :param strict: whether conflicting options are allowed or not :return: self """ for var in vars(self): if not getattr(self, var): setattr(self, var, getattr(other, var)) else: if strict and getattr(other, var) is not None and getattr(self, var) != getattr(other, var): raise NotImplementedError return self @classmethod def from_json(cls, content: Union[Path, Dict]): if isinstance(content, Path): with open(content, 'r') as file: json_content: Dict = json.load(file) else: json_content = content config = cls() config.set_from_json(json_content) return config @classmethod def from_yaml(cls, content: Union[Path, str]): if isinstance(content, Path): with open(content, 'r') as file: json_content: Dict = yaml.safe_load(file) else: json_content = yaml.safe_load(content) return cls.from_json(json_content) def set_from_json(self, content: Optional[Dict]): raise NotImplementedError def to_json(self) -> Dict: raise NotImplementedError def dump_as_yaml(self, filename: Path, clean_none_entries: bool = True): with filename.open('w') as file: if clean_none_entries: simple_dict = clean_dict(self.to_json()) else: simple_dict = self.to_json() if simple_dict is not None: yaml.dump(simple_dict, file) else: pass # TODO def dump_as_json(self, filename: Path, clean_none_entries: bool = True): with open(filename, 'w') as config: if clean_none_entries: simple_dict = clean_dict(self.to_json()) else: simple_dict = self.to_json() if simple_dict is not None: json.dump(simple_dict, config, indent=4) else: pass # TODO @classmethod def _fill_keys(cls, dictionary: Optional[Dict]): if dictionary is None: return cls().to_json() else: return recursive_merge_dictionaries( cls().to_json(), dictionary ) def recursive_merge_dictionaries(dict1: Dict, dict2: Dict) -> Dict: aux1 = { k: v for k, v in dict1.items() if type(v) == dict } aux2 = { k: v for k, v in dict2.items() if type(v) == dict } merged = { k: recursive_merge_dictionaries(v, aux2[k]) for k, v in aux1.items() if k in aux2.keys() } return dict1 | dict2 | merged