|
|
|
|
@ -4,6 +4,8 @@ being loaded and used.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
|
import re
|
|
|
|
|
import itertools
|
|
|
|
|
|
|
|
|
|
from preserve import preservable
|
|
|
|
|
|
|
|
|
|
@ -16,9 +18,12 @@ class InvalidConfigError(Exception):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _ValueSpecification(ABC):
|
|
|
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
|
|
|
self.default = default
|
|
|
|
|
self.optional = optional
|
|
|
|
|
"""
|
|
|
|
|
The abstract base class for all ConfigSpecifications. `default` values are used to ensure
|
|
|
|
|
that a config structure exists even if optional fields are not supplied.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, helptext=""):
|
|
|
|
|
self.helptext = helptext
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
@ -32,8 +37,8 @@ class _ValueSpecification(ABC):
|
|
|
|
|
|
|
|
|
|
@preservable
|
|
|
|
|
class BoolSpec(_ValueSpecification):
|
|
|
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
|
|
|
super().__init__(default, optional, helptext)
|
|
|
|
|
def __init__(self, helptext=""):
|
|
|
|
|
super().__init__(helptext)
|
|
|
|
|
|
|
|
|
|
def validate(self, value):
|
|
|
|
|
if not isinstance(value, bool):
|
|
|
|
|
@ -42,9 +47,8 @@ class BoolSpec(_ValueSpecification):
|
|
|
|
|
|
|
|
|
|
@preservable
|
|
|
|
|
class IntSpec(_ValueSpecification):
|
|
|
|
|
def __init__(self, default=None, minval=None, maxval=None,
|
|
|
|
|
optional=False, helptext=""):
|
|
|
|
|
super().__init__(default, optional, helptext)
|
|
|
|
|
def __init__(self, minval=None, maxval=None, helptext=""):
|
|
|
|
|
super().__init__(helptext)
|
|
|
|
|
self.minval = minval
|
|
|
|
|
self.maxval = maxval
|
|
|
|
|
|
|
|
|
|
@ -61,74 +65,121 @@ class IntSpec(_ValueSpecification):
|
|
|
|
|
|
|
|
|
|
@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 __init__(self, regex=None, helptext=""):
|
|
|
|
|
"""
|
|
|
|
|
Specify a string to be provided. If `regex` is not None, validation requires that the
|
|
|
|
|
config value directly matches the supplied regex string (calls `re.fullmatch`)
|
|
|
|
|
"""
|
|
|
|
|
super().__init__(helptext)
|
|
|
|
|
self.regex = regex
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
if self.regex and (re.fullmatch(self.regex, value) is None):
|
|
|
|
|
raise InvalidConfigError(F"Config string '{value}' must match regex"
|
|
|
|
|
F" pattern '{self.regex}'")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@preservable
|
|
|
|
|
class DictSpec(_ValueSpecification):
|
|
|
|
|
def __init__(self, default=None, optional=False, helptext=""):
|
|
|
|
|
super().__init__(default, optional, helptext)
|
|
|
|
|
def __init__(self, helptext=""):
|
|
|
|
|
super().__init__(helptext)
|
|
|
|
|
self.spec_dict = {}
|
|
|
|
|
self.optional_spec_dict = {}
|
|
|
|
|
self.default_values = {}
|
|
|
|
|
|
|
|
|
|
def add_spec(self, name, value_spec, optional=False, default=None):
|
|
|
|
|
"""
|
|
|
|
|
Adds a specification to this dict with the given `name`. To validate, corresponding fields
|
|
|
|
|
must be present with matching keys. Only supports string names/keys.
|
|
|
|
|
|
|
|
|
|
Specifications marked as `optional` will be filled with their default value if not present
|
|
|
|
|
during validation (note, this _will_ modify the data validated to fill in defaults), and
|
|
|
|
|
will also pass validation if the value is None. Providing `default` as anything other than
|
|
|
|
|
None will implicitly set `optional` to True.
|
|
|
|
|
|
|
|
|
|
def add_spec(self, name, newspec):
|
|
|
|
|
if not isinstance(newspec, _ValueSpecification):
|
|
|
|
|
The default value must be None, or a value that will pass validation with the spec.
|
|
|
|
|
|
|
|
|
|
To allow concise access to the value_spec passed in, returns `value_spec`. This beahviour
|
|
|
|
|
has an exception for ListSpecs, for which it will return the value_spec passed to the
|
|
|
|
|
ListSpec itself (also to allow concise access to further define the spec).
|
|
|
|
|
|
|
|
|
|
Note that nested optional dicts _are_ possible by passing `default` as an empty dict `{}`,
|
|
|
|
|
and passing `value_spec` a DictSpec where every field is itself optional. If this entire
|
|
|
|
|
field is missing during validation it will then attempt to validate an empty dict,
|
|
|
|
|
populating it with all of the nested optional fields.
|
|
|
|
|
"""
|
|
|
|
|
if not isinstance(value_spec, _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
|
|
|
|
|
if (name in self.spec_dict) or (name in self.optional_spec_dict):
|
|
|
|
|
raise TypeError(F"Name '{name}' already exists in this DictSpec")
|
|
|
|
|
|
|
|
|
|
if default is not None:
|
|
|
|
|
optional = True
|
|
|
|
|
|
|
|
|
|
if not optional:
|
|
|
|
|
self.spec_dict[name] = value_spec
|
|
|
|
|
else:
|
|
|
|
|
self.optional_spec_dict[name] = value_spec
|
|
|
|
|
self.default_values[name] = default
|
|
|
|
|
|
|
|
|
|
def add_specs(self, spec_list):
|
|
|
|
|
if isinstance(value_spec, ListSpec):
|
|
|
|
|
return value_spec.spec
|
|
|
|
|
|
|
|
|
|
return value_spec
|
|
|
|
|
|
|
|
|
|
def add_specs(self, spec_dict, optional=False):
|
|
|
|
|
"""
|
|
|
|
|
Convenience method to pass a series of specifications to 'add_spec()'
|
|
|
|
|
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`
|
|
|
|
|
spec_dict: A dict of specs, where each key is the name of the spec stored as the
|
|
|
|
|
corresponding value. If `optional` is true, the value may be a tuple with the
|
|
|
|
|
first item being the spec and the second being the default value.
|
|
|
|
|
optional: Same behaviour as `add_spec()`. Applies to all specs passed in, and will use
|
|
|
|
|
a default value of None.
|
|
|
|
|
"""
|
|
|
|
|
for spec in spec_list:
|
|
|
|
|
self.add_spec(*spec)
|
|
|
|
|
for name, spec in spec_dict.items():
|
|
|
|
|
if optional and isinstance(spec, tuple):
|
|
|
|
|
self.add_spec(name, spec[0], optional=True, default=spec[1])
|
|
|
|
|
else:
|
|
|
|
|
self.add_spec(name, spec, optional)
|
|
|
|
|
|
|
|
|
|
def validate(self, value_dict):
|
|
|
|
|
"""
|
|
|
|
|
Checks the supplied value to confirm that it complies with this specification.
|
|
|
|
|
Checks the supplied values 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.
|
|
|
|
|
This _can_ modify the supplied value dict, inserting defaults for any child
|
|
|
|
|
config specifications that are marked as optional. Optional values (after defaults are
|
|
|
|
|
inserted) must either be None or pass validation as usual.
|
|
|
|
|
"""
|
|
|
|
|
spec_set = set(self.spec_dict.keys())
|
|
|
|
|
value_set = set(value_dict.keys())
|
|
|
|
|
for missing_key in (self.spec_dict.keys() - value_dict.keys()):
|
|
|
|
|
raise InvalidConfigError(F"Dict must contain key '{missing_key}'")
|
|
|
|
|
|
|
|
|
|
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_dict.keys() - self.spec_dict.keys()) -
|
|
|
|
|
self.optional_spec_dict.keys()):
|
|
|
|
|
raise InvalidConfigError(F"Dict contains unknown key '{extra_key}'")
|
|
|
|
|
|
|
|
|
|
for default_key in (self.optional_spec_dict.keys() - value_dict.keys()):
|
|
|
|
|
value_dict[default_key] = self.default_values[default_key]
|
|
|
|
|
|
|
|
|
|
for extra_key in value_set - spec_set:
|
|
|
|
|
raise InvalidConfigError("Dict contains unknown key: " +
|
|
|
|
|
extra_key)
|
|
|
|
|
for key, spec in self.spec_dict.items():
|
|
|
|
|
try:
|
|
|
|
|
spec.validate(value_dict[key])
|
|
|
|
|
except InvalidConfigError as e:
|
|
|
|
|
e.args = ("Key: " + key,) + e.args
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
for key, value in value_dict.items():
|
|
|
|
|
for key, spec in self.optional_spec_dict.items():
|
|
|
|
|
if value_dict[key] is None:
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
self.spec_dict[key].validate(value)
|
|
|
|
|
spec.validate(value_dict[key])
|
|
|
|
|
except InvalidConfigError as e:
|
|
|
|
|
e.args = ("Key: " + key,) + e.args
|
|
|
|
|
raise
|
|
|
|
|
@ -147,53 +198,58 @@ class DictSpec(_ValueSpecification):
|
|
|
|
|
with this config specification.
|
|
|
|
|
"""
|
|
|
|
|
template = {}
|
|
|
|
|
for key, confspec in self.spec_dict.items():
|
|
|
|
|
if confspec.optional and (not include_optional):
|
|
|
|
|
continue
|
|
|
|
|
if include_optional:
|
|
|
|
|
spec_items = itertools.chain(self.spec_dict.items(), self.optional_spec_dict.items())
|
|
|
|
|
else:
|
|
|
|
|
spec_items = self.spec_dict.items()
|
|
|
|
|
|
|
|
|
|
for key, confspec in spec_items:
|
|
|
|
|
if hasattr(confspec, "get_template"):
|
|
|
|
|
template[key] = confspec.get_template(include_optional)
|
|
|
|
|
else:
|
|
|
|
|
template[key] = confspec.default
|
|
|
|
|
template[key] = self.default_values.get(key, None)
|
|
|
|
|
return template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _ListSpecMixin():
|
|
|
|
|
@preservable
|
|
|
|
|
class ListSpec(_ValueSpecification):
|
|
|
|
|
"""
|
|
|
|
|
Specify a list of values
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, value_spec, min_values=None, max_values=None, helptext=""):
|
|
|
|
|
"""
|
|
|
|
|
Make a new ListSpec. Requires a child spec to be applied to all list values.
|
|
|
|
|
Min and max limits can be supplied to ensure a certain range of list sizes.
|
|
|
|
|
Returns the value_spec passed in, to allow easy additions to it (if it's a DictSpec)
|
|
|
|
|
"""
|
|
|
|
|
super().__init__(helptext)
|
|
|
|
|
self.spec = value_spec
|
|
|
|
|
self.min_values = min_values
|
|
|
|
|
self.max_values = max_values
|
|
|
|
|
|
|
|
|
|
def validate(self, value_list):
|
|
|
|
|
if not isinstance(value_list, list):
|
|
|
|
|
raise InvalidConfigError("Config item must be a list")
|
|
|
|
|
if self.min_values and (len(value_list) < self.min_values):
|
|
|
|
|
raise InvalidConfigError(F"Config list requires {self.min_values} values, and only"
|
|
|
|
|
F" has {len(value_list)}")
|
|
|
|
|
if self.max_values and (len(value_list) > self.max_values):
|
|
|
|
|
raise InvalidConfigError(F"Config list can have no more than {self.max_values} values,"
|
|
|
|
|
F" and has {len(value_list)}")
|
|
|
|
|
|
|
|
|
|
for index, value in enumerate(value_list):
|
|
|
|
|
try:
|
|
|
|
|
super().validate(value)
|
|
|
|
|
self.spec.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)]
|
|
|
|
|
if hasattr(self.spec, "get_template"):
|
|
|
|
|
return [self.spec.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
|
|
|
|
|
return [None]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@preservable
|
|
|
|
|
|