backup-tool/app/skeleton.py

121 lines
3.2 KiB
Python
Raw Normal View History

2021-04-25 18:10:37 +02:00
"""Handles merging with the skeleton config."""
from typing import Dict
class InvalidKeyError(Exception):
2021-04-25 18:10:37 +02:00
"""Thrown when a config file contains an invalid key."""
def __init__(self, key: str):
2021-04-26 17:45:19 +02:00
"""Create a new InvalidKeyError object with the given key.
2021-04-25 18:10:37 +02:00
Args:
key: the invalid key
"""
self.message = "Invalid key: {}".format(key)
super().__init__(key)
class MissingKeyError(Exception):
2021-04-25 18:10:37 +02:00
"""Thrown when a required key is missing from a config."""
def __init__(self, key: str):
2021-04-26 17:45:19 +02:00
"""Create a new MissingKeyError object with the given key.
2021-04-25 18:10:37 +02:00
Args:
key: the invalid key
"""
self.message = "Missing key: {}".format(key)
super().__init__(key)
2021-01-15 21:08:56 +01:00
def merge(*dicts: [Dict]) -> Dict:
2021-04-26 17:45:19 +02:00
"""Merge multiple dicts into one.
2021-04-25 18:10:37 +02:00
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
"""
2021-01-15 21:08:56 +01:00
# 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:
2021-04-26 17:45:19 +02:00
"""Merge a dictionary with a skeleton containing default values.
2021-04-25 18:10:37 +02:00
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)
2021-04-25 18:27:57 +02:00
Returns:
a new dictionary representing the two merged dictionaries
2021-04-25 18:10:37 +02:00
Todo:
* Check if an infinite loop is possible
* Split info less complex functions
"""
# First, check for illegal keys
key = next(key not in skel for key in data)
if key:
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:
2021-04-25 18:10:37 +02:00
# TODO make this error message more verbose
raise TypeError("Invalid value type")
# Recurse into dicts
elif type(value) == dict:
2021-01-15 21:08:56 +01:00
data[key] = merge_with_skeleton(data[key], value)
return data