from collections import deque
from ..compat.colabc import Sequence
[docs]class ValidationError(ValueError, Sequence):
"""
Validation Error Base Class
:param deque context:
error context,
empty ``deque`` by default.
:param \\**kw:
concrete error attributes.
Since validators try to process as much as possible,
they can raise multiple errors
(wrapped by :class:`validx.exc.SchemaError`).
To unify handling of such errors,
each validation error provides ``Sequence`` interface.
It means,
you can iterate them,
get their length,
get nested errors by index,
and sort nested errors by context.
Error context is a full path,
that indicates where the error occurred.
It contains mapping keys,
sequence indexes,
and special markers
(see :class:`validx.exc.Extra` and :class:`validx.exc.Step`).
.. doctest:: validation_error
>>> from validx import exc, Dict, List, Int
>>> schema = Dict({"foo": List(Int(max=100))})
>>> try:
... schema({"foo": [1, 2, 200, 250], "bar": None})
... except exc.ValidationError as e:
... error = e
>>> error.sort()
>>> error
<SchemaError(errors=[
<bar: ForbiddenKeyError()>,
<foo.2: MaxValueError(expected=100, actual=200)>,
<foo.3: MaxValueError(expected=100, actual=250)>
])>
>>> len(error)
3
>>> error[1]
<foo.2: MaxValueError(expected=100, actual=200)>
>>> error[1].context
deque(['foo', 2])
>>> error[1].format_context()
'foo.2'
>>> error[1].format_error()
'MaxValueError(expected=100, actual=200)'
>>> error.sort(reverse=True)
>>> error
<SchemaError(errors=[
<foo.3: MaxValueError(expected=100, actual=250)>,
<foo.2: MaxValueError(expected=100, actual=200)>,
<bar: ForbiddenKeyError()>
])>
"""
__slots__ = ("context",)
def __init__(self, context=None, *args, **kw):
self.context = context or deque()
for slot, value in dict(zip(self.__slots__[1:], args), **kw).items():
setattr(self, slot, value)
super(ValidationError, self).__init__(
*(getattr(self, slot) for slot in self.__slots__)
)
[docs] def add_context(self, node):
"""
Add error context
:param node:
key or index of member,
where error is raised.
:returns:
the error itself,
so that the method is suitable for chaining.
Example:
.. doctest:: add_context
>>> from validx.exc import ValidationError
>>> e = ValidationError()
>>> e
<ValidationError()>
>>> e.context
deque([])
>>> e.add_context("foo")
<foo: ValidationError()>
>>> e.context
deque(['foo'])
"""
self.context.appendleft(node)
return self
def __getitem__(self, index):
if index != 0:
raise IndexError(index)
return self
def __len__(self):
return 1
def __iter__(self):
yield self
def sort(self, key=None, reverse=False):
pass
def __repr__(self):
if self.context:
return "<%s: %s>" % (self.format_context(), self.format_error())
else:
return "<%s>" % self.format_error()
def __str__(self):
return repr(self)
def __eq__(self, other):
if type(self) is not type(other):
return False
for slot in self.__slots__:
if getattr(self, slot) != getattr(other, slot):
return False
return True
def format_context(self):
def context():
for node in self.context:
if isinstance(node, str) and "." in node:
yield "[%s]" % node
else:
yield str(node)
return ".".join(context())
def format_error(self):
def params():
for slot in self.__slots__[1:]: # Exclude ``context``
yield "%s=%r" % (slot, getattr(self, slot))
return "%s(%s)" % (self.__class__.__name__, ", ".join(params()))
[docs]class ConditionError(ValidationError):
"""
Base Class for Condition Errors
It has a couple of attributes ``expected`` and ``actual``,
that gives info of what happens and why the error is raised.
See derived classes for details.
"""
__slots__ = ValidationError.__slots__ + ("expected", "actual")
[docs]class InvalidTypeError(ConditionError):
"""
Invalid Type Error
:param type expected:
expected type (types).
:type expected: type or tuple
:param type actual:
actual type of value.
"""
__slots__ = ConditionError.__slots__
[docs]class OptionsError(ConditionError):
"""
Options Error
:param expected:
list of valid values.
:type expected: list or tuple
:param actual:
actual value.
"""
__slots__ = ConditionError.__slots__
[docs]class MinValueError(ConditionError):
"""
Minimum Value Error
:param expected:
minimal allowed value.
:param actual:
actual value.
"""
__slots__ = ConditionError.__slots__
[docs]class MaxValueError(ConditionError):
"""
Maximum Value Error
:param expected:
maximal allowed value.
:param actual:
actual value.
"""
__slots__ = ConditionError.__slots__
[docs]class FloatValueError(ConditionError):
"""
Float Value Error
:param str expected:
* ``"number"`` on test for ``Not-a-Number``;
* ``"finite"`` on test for ``Infinity``.
:param float actual:
actual value.
"""
__slots__ = ConditionError.__slots__
[docs]class StrDecodeError(ConditionError):
"""
String Decode Error
:param str expected:
encoding name.
:param bytes actual:
actual byte-string value.
"""
__slots__ = ConditionError.__slots__
[docs]class MinLengthError(ConditionError):
"""
Minimum Length Error
:param int expected:
minimal allowed length.
:param int actual:
actual value length.
"""
__slots__ = ConditionError.__slots__
[docs]class MaxLengthError(ConditionError):
"""
Maximum Length Error
:param int expected:
maximal allowed length.
:param int actual:
actual value length.
"""
__slots__ = ConditionError.__slots__
[docs]class TupleLengthError(ConditionError):
"""
Tuple Length Error
:param int expected:
tuple length.
:param int actual:
actual value length.
"""
__slots__ = ConditionError.__slots__
[docs]class PatternMatchError(ConditionError):
"""
Pattern Match Error
:param str expected:
pattern, i.e. regular expression.
:param str actual:
actual value.
"""
__slots__ = ConditionError.__slots__
[docs]class DatetimeParseError(ConditionError):
"""
Date & Time Parse Error
:param str expected:
format.
:param str actual:
actual value.
"""
__slots__ = ConditionError.__slots__
[docs]class DatetimeTypeError(ConditionError):
"""
Date & Time Type Error
:param str expected:
expected type of datetime: "naive" or "tzaware".
:param datetime actual:
actual value.
"""
__slots__ = ConditionError.__slots__
[docs]class RecursionMaxDepthError(ConditionError):
"""
Recursion Maximum Depth Error
:param int expected:
maximal allowed depth.
:param int actual:
actual recursion depth.
"""
__slots__ = ConditionError.__slots__
[docs]class MappingKeyError(ValidationError):
"""
Base Class for Mapping Key Errors
:param key:
failed key,
that goes into error context.
.. testsetup:: mapping_key_error
from validx.exc import MappingKeyError
.. doctest:: mapping_key_error
>>> e = MappingKeyError("foo")
>>> e
<foo: MappingKeyError()>
"""
__slots__ = ValidationError.__slots__
def __init__(self, context=None, key=None):
if context is not None and not isinstance(context, deque):
context = deque([context])
super(MappingKeyError, self).__init__(context)
if key is not None:
self.add_context(key)
[docs]class ForbiddenKeyError(MappingKeyError):
"""Forbidden Mapping Key Error"""
__slots__ = MappingKeyError.__slots__
[docs]class MissingKeyError(MappingKeyError):
"""Missing Mapping Key Error"""
__slots__ = MappingKeyError.__slots__
[docs]class SchemaError(ValidationError):
"""
Schema Error
It is an error class,
that wraps multiple errors occurred during complex structure validation.
:param list errors:
list of all errors occurred during complex structure validation.
"""
__slots__ = ValidationError.__slots__ + ("errors",)
def __init__(self, context=None, errors=None):
if context is not None and not isinstance(context, deque):
errors = context
context = None
super(SchemaError, self).__init__(context, errors=errors)
def __getitem__(self, index):
return self.errors[index]
def __len__(self):
return len(self.errors)
def __iter__(self):
for error in self.errors:
yield error
def sort(self, key=None, reverse=False):
if key is None:
key = lambda error: tuple(repr(node) for node in error.context)
self.errors.sort(key=key, reverse=reverse)
def __repr__(self):
errors = ",\n".join(" %r" % e for e in self.errors)
return "<%s(errors=[\n%s\n])>" % (self.__class__.__name__, errors)
def add_context(self, node):
for e in self.errors:
e.add_context(node)
return self