Skip to content

Instantly share code, notes, and snippets.

@kajuberdut
Last active January 14, 2022 17:00
Show Gist options
  • Save kajuberdut/d00631d4990c1b015532591c839516ee to your computer and use it in GitHub Desktop.
Save kajuberdut/d00631d4990c1b015532591c839516ee to your computer and use it in GitHub Desktop.
A quick first pass at a "configurable configuration validator"
import typing as t
from functools import partial
from inspect import getsource
class ConfigError(ValueError):
...
def get_func_name(func: t.Callable) -> str:
name = func.__name__
if name == "<lambda>":
name = getsource(func).split("=")[0].strip()
return name
def is_type(type: t.Type, key: str, config: dict) -> None:
if not isinstance((value := config[key]), type):
raise ConfigError(
f"{key} must be an instance of {type.__name__}. Was {type(value)}"
)
is_string = partial(is_type, str)
is_int = partial(is_type, int)
def exactly_n(n: int, keys: list, config: dict) -> None:
if (actual_n := len((filtered := [i for i in keys if i in config]))) != n:
raise ConfigError(
f"Config must contain exactly {n} items from {keys}. Found {actual_n}: {filtered}"
)
exactly_one = partial(exactly_n, n=1)
def disallowed_keys(keys: list, config: dict) -> bool:
if present := [k for k in keys if k in config]:
raise ConfigError(
f"The following dissalowed keys were found in config: {present}"
)
def required_keys(keys: list, config: dict) -> bool:
if not_present := [k for k in keys if k not in config]:
raise ConfigError(
f"The following required keys were not found in config: {not_present}"
)
def pass_filter(filter: t.Callable, key: str, config: dict) -> None:
if not filter(config[key]):
raise ConfigError(
f"The value of config item {key} does not pass {get_func_name(filter)}. Got value: '{config[key]}'"
)
def validate(validation_rules: dict, config: dict) -> None:
exceptions = list()
for key, rule_list in validation_rules.get("value_rules", {}).items():
for rule in list(rule_list):
try:
rule(key=key, config=config)
except ConfigError as e:
exceptions.append(e)
for cr in validation_rules.get("config_rules", []):
try:
cr(config=config)
except ConfigError as e:
exceptions.append(e)
print(exceptions)
e = exceptions.pop(0)
raise e
if __name__ == "__main__":
config = {
"setting1": 1,
"setting2": None,
"setting3": " ",
"color_hex": "#1f42a5",
"color_code": (165, 42, 42),
}
def not_red(value: str) -> bool:
return value != "red"
is_truthy = lambda x: x is True
validation_rules = {
"value_rules": {
"setting1": [is_int],
"setting3": [
is_string,
partial(pass_filter, not_red),
partial(pass_filter, is_truthy),
],
},
"config_rules": [
partial(disallowed_keys, keys=["thebadkey", "anotherbadkey"]),
partial(exactly_one, keys=["color_hex", "color_code"]),
],
}
validate(validation_rules, config)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment