"""Handles merging with the skeleton config.""" from typing import Dict class InvalidKeyError(Exception): """Thrown when a config file contains an invalid key.""" def __init__(self, key: str): """Create a new InvalidKeyError object with the given key. Args: key: the invalid key """ self.message = "Invalid key: {}".format(key) super().__init__(key) class MissingKeyError(Exception): """Thrown when a required key is missing from a config.""" def __init__(self, key: str): """Create a new MissingKeyError object with the given key. Args: key: the invalid key """ self.message = "Missing key: {}".format(key) super().__init__(key) def merge(*dicts: [Dict]) -> Dict: """Merge multiple dicts into one. It reads the dicts from left to right, always preferring the "right" dictionary's values. Therefore, the dictionaries should be sorted from least important to most important (e.g. a default values skeleton should be to the left of a dict of selected values). Args: dicts: the dictionaries to merge Returns: a new dictionary representing the merged dictionaries Todo: * Make sure an infinite loop is not possible """ # Base cases if len(dicts) == 0: return {} if len(dicts) == 1: return dicts[0] # We merge the first two dicts d1, d2 = dicts[0], dicts[1] output = d1.copy() for key, value in d2.items(): if type(value) == dict: # Merge the two sub-dictionaries output[key] = ( merge(output[key], value) if type(output.get(key)) == dict else value ) else: output[key] = value return merge(output, *dicts[2:]) def merge_with_skeleton(data: Dict, skel: Dict) -> Dict: """Merge a dictionary with a skeleton containing default values. The skeleton not only defines what the default values are, but also enforces a certain shape. This allows us to define a config file using a dictionary and parse it. Args: data: dictionary containing the selected config values skel: dictionary containing the skeleton (aka the def) Returns: a new dictionary representing the two merged dictionaries Todo: * Check if an infinite loop is possible * Split info less complex functions """ # First, check for illegal keys for key in data: if key not in skel: raise InvalidKeyError(key) # Then, check the default values for key, value in skel.items(): if key not in data: # Raise error if there's not default value if value is None: raise MissingKeyError(key) # Replace with default value data[key] = value # Error if value is not same type as default value elif type(data[key]) != type(value) and value is not None: # TODO make this error message more verbose raise TypeError("Invalid value type") # Recurse into dicts elif type(value) == dict: data[key] = merge_with_skeleton(data[key], value) return data