@ -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
@ -59,65 +63,145 @@ class IntSpec(_ValueSpecification):
str ( self . maxval ) )
@preservable
class FloatSpec ( _ValueSpecification ) :
def __init__ ( self , minval = None , maxval = None , helptext = " " ) :
super ( ) . __init__ ( helptext )
self . minval = minval
self . maxval = maxval
def validate ( self , value ) :
if not isinstance ( value , float ) :
raise InvalidConfigError ( " Config value must be a float " )
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 __init__ ( self , regex = None , allowempty = False , 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 ` ) .
If ` allowempty ` is False , validation requires that the string not be an empty or blank .
"""
super ( ) . __init__ ( helptext )
self . regex = regex
self . allowempty = allowempty
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 not self . allowempty and ( value == " " ) :
raise InvalidConfigError ( " Config value cannot be a blank string " )
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 , newspec ) :
if not isinstance ( newspec , _ValueSpecification ) :
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 .
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
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() ' .
Args :
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 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 value s 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 extra_key in value_set - spec_set :
raise InvalidConfigError ( " 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 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
@ -136,53 +220,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