Source code for configconfig.validator

#!/usr/bin/env python3
#
#  validator.py
"""
Validate values obtained from the ``YAML`` file and coerce into the appropriate return types.
"""
#
#  Copyright © 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#
#  validate based on https://github.com/yaccob/ytools
#  Copyright (c) Jakob Stemberger <yaccob@gmx.net>
#  Apache 2.0 Licensed
#

# stdlib
import pathlib
from typing import Any, Dict, Iterable, List, Optional, Union

# 3rd party
import jsonschema  # type: ignore[import]
from domdf_python_tools.typing import PathLike
from domdf_python_tools.utils import strtobool
from ruamel.yaml import YAML
from typing_extensions import NoReturn
from typing_inspect import get_origin, is_literal_type  # type: ignore[import]

# this package
from configconfig.metaclass import ConfigVarMeta
from configconfig.utils import RawConfigVarsType, check_union, get_literal_values, optional_getter

__all__ = ["Validator", "validate_files"]


[docs]class Validator: """ Methods are named ``visit_<type>``. .. autosummary-widths:: 4/10 """ def __init__(self, config_var: ConfigVarMeta): self.config_var = config_var _dtypes = { str: "str", int: "int", float: "float", bool: "bool", }
[docs] def validate(self, raw_config_vars: Optional[RawConfigVarsType] = None) -> Any: """ Validate the configuration value. :param raw_config_vars: :returns: The validated value. """ if raw_config_vars is None: raw_config_vars = {} if self.config_var.rtype is None: self.config_var.rtype = self.config_var.dtype if self.config_var.dtype in self._dtypes: return getattr(self, f"visit_{self._dtypes[self.config_var.dtype]}")(raw_config_vars) elif get_origin(self.config_var.dtype) in {list, List}: return self.visit_list(raw_config_vars) elif get_origin(self.config_var.dtype) in {dict, Dict}: return self.visit_dict(raw_config_vars) elif get_origin(self.config_var.dtype) is Union: return self.visit_union(raw_config_vars) elif is_literal_type(self.config_var.dtype): return self.visit_literal(raw_config_vars) else: self.unknown_type()
def _visit_str_number(self, raw_config_vars: RawConfigVarsType) -> Union[str, int, float]: obj = optional_getter(raw_config_vars, self.config_var, self.config_var.required) if not isinstance(obj, self.config_var.dtype): raise ValueError(f"'{self.config_var.__name__}' must be a {self.config_var.dtype}") from None return obj
[docs] def visit_str(self, raw_config_vars: RawConfigVarsType) -> str: """ Used to validate and convert :class:`str` values. :param raw_config_vars: """ return self.config_var.rtype(self._visit_str_number(raw_config_vars))
[docs] def visit_int(self, raw_config_vars: RawConfigVarsType) -> int: """ Used to validate and convert :class:`int` values. :param raw_config_vars: """ return self.config_var.rtype(self._visit_str_number(raw_config_vars))
[docs] def visit_float(self, raw_config_vars: RawConfigVarsType) -> float: """ Used to validate and convert :class:`float` values. :param raw_config_vars: """ return self.config_var.rtype(self._visit_str_number(raw_config_vars))
[docs] def visit_bool(self, raw_config_vars: RawConfigVarsType) -> bool: """ Used to validate and convert :class:`bool` values. :param raw_config_vars: """ obj = optional_getter(raw_config_vars, self.config_var, self.config_var.required) if not isinstance(obj, (int, bool, str)): raise ValueError( f"'{self.config_var.__name__}' must be one of {(int, bool, str)}, " f"not {type(obj)}" ) from None return self.config_var.rtype(strtobool(obj))
[docs] def visit_list(self, raw_config_vars: RawConfigVarsType) -> List: """ Used to validate and convert :class:`list` values. :param raw_config_vars: """ # Lists of strings, numbers, Unions and Literals buf = [] data = optional_getter(raw_config_vars, self.config_var, self.config_var.required) if isinstance(data, str) or not isinstance(data, Iterable): raise ValueError( f"'{self.config_var.__name__}' must be a List of {self.config_var.dtype.__args__[0]}" ) from None if get_origin(self.config_var.dtype.__args__[0]) is Union: for obj in data: if not check_union(obj, self.config_var.dtype.__args__[0]): raise ValueError( f"'{self.config_var.__name__}' must be a " f"List of {self.config_var.dtype.__args__[0]}" ) from None elif is_literal_type(self.config_var.dtype.__args__[0]): for obj in data: # if isinstance(obj, str): # obj = obj.lower() if obj not in get_literal_values(self.config_var.dtype.__args__[0]): raise ValueError( f"Elements of '{self.config_var.__name__}' must be " f"one of {get_literal_values(self.config_var.dtype.__args__[0])}" ) from None else: for obj in data: if not check_union(obj, self.config_var.dtype): raise ValueError( f"'{self.config_var.__name__}' must be a List of {self.config_var.dtype.__args__[0]}" ) from None try: for obj in data: if self.config_var.rtype.__args__[0] in {int, str, float, bool}: buf.append(self.config_var.rtype.__args__[0](obj)) else: buf.append(obj) return buf except ValueError: raise ValueError( f"Values in '{self.config_var.__name__}' must be {self.config_var.rtype.__args__[0]}" ) from None
[docs] def visit_dict(self, raw_config_vars: RawConfigVarsType) -> Dict: """ Used to validate and convert :class:`dict` values. :param raw_config_vars: """ # Dict[str, str] if self.config_var.dtype == Dict[str, str]: obj = optional_getter(raw_config_vars, self.config_var, self.config_var.required) if not isinstance(obj, dict): raise ValueError(f"'{self.config_var.__name__}' must be a dictionary") from None return {str(k): str(v) for k, v in obj.items()} # Dict[str, Any] elif self.config_var.dtype == Dict[str, Any]: obj = optional_getter(raw_config_vars, self.config_var, self.config_var.required) if not isinstance(obj, dict): raise ValueError(f"'{self.config_var.__name__}' must be a dictionary") from None return obj # Dict[str, List[str] elif self.config_var.dtype == Dict[str, List[str]]: obj = optional_getter(raw_config_vars, self.config_var, self.config_var.required) if not isinstance(obj, dict): raise ValueError(f"'{self.config_var.__name__}' must be a dictionary") from None return {str(k): [str(i) for i in v] for k, v in obj.items()} else: self.unknown_type()
[docs] def visit_union(self, raw_config_vars: RawConfigVarsType) -> Any: """ Used to validate and convert :class:`typing.Union` values. :param raw_config_vars: """ obj = optional_getter(raw_config_vars, self.config_var, self.config_var.required) if not check_union(obj, self.config_var.dtype): raise ValueError( f"'{self.config_var.__name__}' must be one of {self.config_var.dtype.__args__}, " f"not {type(obj)}" ) from None try: return self.config_var.rtype(obj) except ValueError: raise ValueError( f"'{self.config_var.__name__}' must be {self.config_var.rtype.__args__}, " f"not {type(obj)}" ) from None
[docs] def visit_literal(self, raw_config_vars: RawConfigVarsType) -> Any: """ Used to validate and convert :class:`typing.Literal` values. :param raw_config_vars: """ obj = optional_getter(raw_config_vars, self.config_var, self.config_var.required) # if isinstance(obj, str): # obj = obj.lower() if obj not in get_literal_values(self.config_var.dtype): raise ValueError( f"'{self.config_var.__name__}' must be one of {get_literal_values(self.config_var.dtype)}" ) from None return obj
[docs] def unknown_type(self) -> NoReturn: """ Called when the desired type has no visitor. """ print(self.config_var) print(self.config_var.dtype) print(get_origin(self.config_var.dtype)) raise NotImplementedError
[docs]def validate_files( schemafile: PathLike, *datafiles: PathLike, encoding: str = "utf-8", ) -> None: r""" Validate the given datafiles against the given schema. :param schemafile: The ``json`` or ``yaml`` formatted schema to validate with. :param \*datafiles: The ``json`` or ``yaml`` files to validate. :param encoding: Encoding to open the files with. .. versionadded:: 0.4.0 """ schemafile = pathlib.Path(schemafile) yaml = YAML(typ="safe", pure=True) schema = yaml.load(schemafile.read_text(encoding=encoding)) for filename in datafiles: for document in yaml.load_all(pathlib.Path(filename).read_text(encoding=encoding)): try: jsonschema.validate(document, schema, format_checker=jsonschema.FormatChecker()) except jsonschema.exceptions.ValidationError as e: e.filename = str(filename) raise e