Switched to Black-focused config

This commit is contained in:
Jef Roosens 2021-05-22 21:28:01 +02:00
parent e044c07cd6
commit 11e5ee2869
Signed by: Jef Roosens
GPG key ID: 955C0660072F691F
10 changed files with 21 additions and 16 deletions

View file

@ -0,0 +1,11 @@
"""Main module for the app."""
from .exceptions import InvalidKeyError, InvalidValueError, MissingKeyError
from .skeleton import merge, merge_with_skeleton
__all__ = [
"InvalidKeyError",
"InvalidValueError",
"MissingKeyError",
"merge",
"merge_with_skeleton",
]

View file

@ -0,0 +1,54 @@
"""Common exceptions raised by the program."""
from typing import List, Union
class InvalidKeyError(Exception):
"""Thrown when a config file contains an invalid key."""
def __init__(self, keys: Union[str, List[str]]):
"""Create a new InvalidKeyError object with the given key.
Args:
keys: the invalid key(s)
"""
if type(keys) == str:
keys = [keys]
self.message = "Invalid key(s): {}".format(", ".join(keys))
super().__init__()
class MissingKeyError(Exception):
"""Thrown when a required key is missing from a config."""
def __init__(self, keys: Union[str, List[str]]):
"""Create a new MissingKeyError object with the given key.
Args:
keys: the invalid key(s)
"""
if type(keys) == str:
keys = [keys]
self.message = "Missing key(s): {}".format(", ".join(keys))
super().__init__()
class InvalidValueError(Exception):
"""Thrown when a key contains an invalid value."""
def __init__(self, key: str, expected: str, actual: str):
"""Create a new InvalidValueError given the arguments.
Args:
key: the key containing the invalid value
expected: name of the expected type
actual: name of the actual type
"""
self.message = (
f"Invalid value for key {key}: expected {expected}, " f"got {actual}"
)
super().__init__()

View file

@ -0,0 +1,92 @@
"""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