from typing import List, Optional, Dict, Tuple, Union import json import yaml from pathlib import Path def clean_dict(dictionary: Dict) -> Optional[Dict]: aux = { k: clean_dict(v) for k, v in dictionary.items() if type(v) == dict } | { k: v for k, v in dictionary.items() if type(v) != dict } aux2 = { k: v for k, v in aux.items() if v is not None } return aux2 if aux2 != {} else None 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 class Config: def merge_with(self, other, 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 config: content: Dict = json.load(config) config = cls() config.set_from_json(content) return config @classmethod def from_yaml(cls, path: Path): with open(path, 'r') as config: path: Dict = yaml.safe_load(config) return cls.from_json(path) def set_from_json(self, content: 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) else: pass # TODO class FormatterIF: """ A formatter is bound to a specific input file with some building configuration. """ def __init__( self, input_file: Optional[Path] = None, config: Optional[Config] = None, ): self._input_file: Optional[Path] = input_file self._config: Optional[Config] = config def format(self, build_dir: Path, overwrite: bool = False) -> Optional[List[Tuple[str, Dict]]]: """ :param build_dir: Directory where output files are written to :param overwrite: overwrite existing files :return: When configuration files are needed for a future build of the output files, a list of the file names and their needed configurations. Else None. """ raise NotImplementedError @property def output_files(self) -> List[str]: """ :return: List of files that will be built when the formatter is invoked """ raise NotImplementedError @property def input_file(self) -> Path: if self._input_file is None: raise NotImplementedError return self._input_file @input_file.setter def input_file(self, input_file: Path) -> None: self._input_file = input_file @property def config(self) -> Config: if self._config is None: raise NotImplementedError return self._config @config.setter def config(self, config: Config): self._config = config