Source code for statham.schema.validation.base

from typing import Any, ClassVar, Optional, Tuple, Type

from statham.schema.constants import NotPassed
from statham.schema.exceptions import ValidationError


_TRUE = object()
_FALSE = object()


def replace_bool(value: Any) -> Any:
    return _TRUE if value is True else _FALSE if value is False else value


def _is_instance(value, type_args):
    """Variant of isinstance to handle booleans correctly.

    In CPython, isinstance(True, int) evaluates True.
    """
    if isinstance(value, bool):
        if bool in type_args:
            return True
        return False
    return isinstance(value, type_args)


[docs]class Validator: """Base validator type. Logic for given validation keywords is implemented in subclasses by overriding class variables and implementing the ``_validate()`` method. """ types: ClassVar[Optional[Tuple[Type, ...]]] = None """Types on which this validator applies. If ``None``, apply to all values. """ message: ClassVar[str] = "" """The error message to display on validation failure. Can be tamplated on :attr:`Validator.keywords`. """ keywords: Tuple[str, ...] = tuple() """Keywords which configure this validator. These are used to configure the validator based on the :class:`statham.schema.elements.Element`. """ def __init__(self, *args): """Accepts the parameters specified by the `keywords` class variable.""" if len(self.keywords) != len(args): raise TypeError( f"{type(self).__name__}.__init__ takes exactly " f"{len(self.keywords)} ({len(args)} given)" ) self.params = dict(zip(self.keywords, args))
[docs] @classmethod def from_element(cls, element) -> Optional["Validator"]: """Construct validator from an Element instance. Check for attributes matching keywords for this validator. If none are present, then return None. """ params = tuple( getattr(element, keyword, NotPassed()) for keyword in cls.keywords ) if NotPassed() in params: return None return cls(*params)
# pylint: disable=no-self-use def _validate(self, _value: Any): """Validate a value. Validation logic should be added by base classes, and raise a bare `ValidationError` on a failure. The error message will be automatically generated from the `message` class variable for consistency. """ return
[docs] def error_message(self): """Generate the error message on failed validation.""" return self.message.format(**self.params)
[docs] def __call__(self, value: Any, property_: Any): """Apply the validator to a value. Checks that `value` has correct type for this validator, runs validation logic and constructs the error message on failure. :param value: The value to validate. :param property_: The enclosing property if present - used for error reporting. :raises: :class:`~statham.schema.exceptions.ValidationError` if :paramref:`~Validator.__call__.value` fails validation. """ if self.types and not _is_instance(value, self.types): return try: self._validate(value) except ValidationError: raise ValidationError.from_validator( property_, value, self.error_message() )
[docs]class InstanceOf(Validator): """Validate the type of a value.""" message = "Must be of type {type_names}." def __init__(self, *args): super().__init__() self.params["types"] = args names = (type_.__name__ for type_ in self.params["types"]) self.params["type_names"] = f"({','.join(names)})" def _validate(self, value: Any): if value == NotPassed() or not self.params["types"]: return if not _is_instance(value, self.params["types"]): raise ValidationError
[docs]class NoMatch(Validator): """Don't accept any passed value. Used exclusively by :class:`~statham.schema.elements.base.Nothing`. """ message = "Schema does not accept any values." def _validate(self, value: Any): if value is NotPassed(): return raise ValidationError
[docs]class Const(Validator): """Validate that passed values match a constant value.""" keywords = ("const",) message = "Must match constant value: {const}" def _validate(self, value: Any): aliased = replace_bool(value) const = replace_bool(self.params["const"]) if aliased != const: raise ValidationError
[docs]class Enum(Validator): """Validate that passed values are members of an enumeration.""" keywords = ("enum",) message = "Must be one of these values: {enum}" def _validate(self, value: Any): aliased = replace_bool(value) enum = list(map(replace_bool, self.params["enum"])) if aliased not in enum: raise ValidationError