93 lines
2.6 KiB
Python
93 lines
2.6 KiB
Python
|
"""Handles merging with the skeleton config."""
|
||
|
from typing import Dict
|
||
|
|
||
|
from .exceptions import InvalidKeyError, MissingKeyError
|
||
|
|
||
|
|
||
|
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
|
||
|
invalid_keys = list(filter(lambda k: k not in skel, data))
|
||
|
|
||
|
if invalid_keys:
|
||
|
raise InvalidKeyError(invalid_keys)
|
||
|
|
||
|
# 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
|