Switched to Black-focused config
This commit is contained in:
parent
e044c07cd6
commit
11e5ee2869
10 changed files with 21 additions and 16 deletions
11
config_skeleton/__init__.py
Normal file
11
config_skeleton/__init__.py
Normal 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",
|
||||
]
|
||||
54
config_skeleton/exceptions.py
Normal file
54
config_skeleton/exceptions.py
Normal 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__()
|
||||
92
config_skeleton/skeleton.py
Normal file
92
config_skeleton/skeleton.py
Normal 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
|
||||
Reference in a new issue