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
 |