You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
202 lines
6.7 KiB
202 lines
6.7 KiB
"""
|
|
A Python library for specifying config requirements. Enables configuration to be validated before
|
|
being loaded and used.
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
from preserve import preservable
|
|
|
|
|
|
class InvalidConfigError(Exception):
|
|
pass
|
|
|
|
# The Table and Array terms from the TOML convention essentially
|
|
# map directly to Dictionaries (Tables), and Lists (Arrays)
|
|
|
|
|
|
class _ValueSpecification(ABC):
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
self.default = default
|
|
self.optional = optional
|
|
self.helptext = helptext
|
|
|
|
@abstractmethod
|
|
def validate(self, value):
|
|
"""
|
|
Checks the supplied value to confirm that it complies with this specification.
|
|
Raises InvalidConfigError on failure.
|
|
"""
|
|
pass
|
|
|
|
|
|
@preservable
|
|
class BoolSpec(_ValueSpecification):
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
super().__init__(default, optional, helptext)
|
|
|
|
def validate(self, value):
|
|
if not isinstance(value, bool):
|
|
raise InvalidConfigError("Config value must be a boolean")
|
|
|
|
|
|
@preservable
|
|
class IntSpec(_ValueSpecification):
|
|
def __init__(self, default=None, minval=None, maxval=None,
|
|
optional=False, helptext=""):
|
|
super().__init__(default, optional, helptext)
|
|
self.minval = minval
|
|
self.maxval = maxval
|
|
|
|
def validate(self, value):
|
|
if not isinstance(value, int):
|
|
raise InvalidConfigError("Config value must be an integer")
|
|
if self.minval is not None and value < self.minval:
|
|
raise InvalidConfigError("Config value must be >= " +
|
|
str(self.minval))
|
|
if self.maxval is not None and value > self.maxval:
|
|
raise InvalidConfigError("Config value must be <= " +
|
|
str(self.maxval))
|
|
|
|
|
|
@preservable
|
|
class StringSpec(_ValueSpecification):
|
|
def __init__(self, default="", minlength=None, maxlength=None,
|
|
optional=False, helptext=""):
|
|
super().__init__(default, optional, helptext)
|
|
self.minlength = minlength
|
|
self.maxlength = maxlength
|
|
|
|
def validate(self, value):
|
|
if not isinstance(value, str):
|
|
raise InvalidConfigError(F"Config value must be a string and is {value}")
|
|
if self.minlength is not None and len(value) < self.minlength:
|
|
raise InvalidConfigError("Config string length must be >= " +
|
|
str(self.minlength))
|
|
if self.maxlength is not None and len(value) > self.maxlength:
|
|
raise InvalidConfigError("Config string length must be <= " +
|
|
str(self.maxlength))
|
|
|
|
|
|
@preservable
|
|
class DictSpec(_ValueSpecification):
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
super().__init__(default, optional, helptext)
|
|
self.spec_dict = {}
|
|
|
|
def add_spec(self, name, newspec):
|
|
if not isinstance(newspec, _ValueSpecification):
|
|
raise TypeError("Config specification must be an instance of a "
|
|
"_ValueSpecification subclass")
|
|
if not isinstance(name, str):
|
|
raise TypeError("Config specification name must be a string")
|
|
self.spec_dict[name] = newspec
|
|
return newspec
|
|
|
|
def add_specs(self, spec_list):
|
|
"""
|
|
Convenience method to pass a series of specifications to 'add_spec()'
|
|
|
|
Args:
|
|
spec_list: A list of tuples, where each tuple contains a `name` and `newspec` value,
|
|
to be passed in the same form as a call to `add_spec`
|
|
"""
|
|
for spec in spec_list:
|
|
self.add_spec(*spec)
|
|
|
|
def validate(self, value_dict):
|
|
"""
|
|
Checks the supplied value to confirm that it complies with this specification.
|
|
Raises InvalidConfigError on failure.
|
|
|
|
This *can* modify the supplied value dict, inserting defaults for any child
|
|
config specifications that are marked as optional.
|
|
"""
|
|
spec_set = set(self.spec_dict.keys())
|
|
value_set = set(value_dict.keys())
|
|
|
|
for missing_key in spec_set - value_set:
|
|
if not self.spec_dict[missing_key].optional:
|
|
raise InvalidConfigError("Dict must contain key: " +
|
|
missing_key)
|
|
else:
|
|
value_dict[missing_key] = self.spec_dict[missing_key].default
|
|
|
|
for extra_key in value_set - spec_set:
|
|
raise InvalidConfigError("Dict contains unknown key: " +
|
|
extra_key)
|
|
|
|
for key, value in value_dict.items():
|
|
try:
|
|
self.spec_dict[key].validate(value)
|
|
except InvalidConfigError as e:
|
|
e.args = ("Key: " + key,) + e.args
|
|
raise
|
|
|
|
def get_template(self, include_optional=False):
|
|
"""
|
|
Return a config dict with the minimum structure required for this specification.
|
|
Default values will be included, though not all required fields will necessarily have
|
|
defaults that successfully validate.
|
|
|
|
Args:
|
|
include_optional: If set true, will include *all* config fields, not just the
|
|
required ones
|
|
Returns:
|
|
Dict containing the structure that should be passed back in (with values) to comply
|
|
with this config specification.
|
|
"""
|
|
template = {}
|
|
for key, confspec in self.spec_dict.items():
|
|
if confspec.optional and (not include_optional):
|
|
continue
|
|
|
|
if hasattr(confspec, "get_template"):
|
|
template[key] = confspec.get_template(include_optional)
|
|
else:
|
|
template[key] = confspec.default
|
|
return template
|
|
|
|
|
|
class _ListSpecMixin():
|
|
def validate(self, value_list):
|
|
if not isinstance(value_list, list):
|
|
raise InvalidConfigError("Config item must be a list")
|
|
for index, value in enumerate(value_list):
|
|
try:
|
|
super().validate(value)
|
|
except InvalidConfigError as e:
|
|
e.args = ("List index: " + str(index),) + e.args
|
|
raise
|
|
|
|
def get_template(self, include_optional=False):
|
|
if hasattr(super(), "get_template"):
|
|
return [super().get_template(include_optional)]
|
|
else:
|
|
return [self.default]
|
|
|
|
|
|
@preservable
|
|
class BoolListSpec(_ListSpecMixin, BoolSpec):
|
|
pass
|
|
|
|
|
|
@preservable
|
|
class IntListSpec(_ListSpecMixin, IntSpec):
|
|
pass
|
|
|
|
|
|
@preservable
|
|
class StringListSpec(_ListSpecMixin, StringSpec):
|
|
pass
|
|
|
|
|
|
@preservable
|
|
class DictListSpec(_ListSpecMixin, DictSpec):
|
|
pass
|
|
|
|
|
|
@preservable
|
|
class ConfigSpecification(DictSpec):
|
|
pass
|