265 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			265 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from collections import defaultdict, deque
 | 
						|
import itertools
 | 
						|
import pprint
 | 
						|
import textwrap
 | 
						|
 | 
						|
from jsonschema import _utils
 | 
						|
from jsonschema.compat import PY3, iteritems
 | 
						|
 | 
						|
 | 
						|
WEAK_MATCHES = frozenset(["anyOf", "oneOf"])
 | 
						|
STRONG_MATCHES = frozenset()
 | 
						|
 | 
						|
_unset = _utils.Unset()
 | 
						|
 | 
						|
 | 
						|
class _Error(Exception):
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        message,
 | 
						|
        validator=_unset,
 | 
						|
        path=(),
 | 
						|
        cause=None,
 | 
						|
        context=(),
 | 
						|
        validator_value=_unset,
 | 
						|
        instance=_unset,
 | 
						|
        schema=_unset,
 | 
						|
        schema_path=(),
 | 
						|
        parent=None,
 | 
						|
    ):
 | 
						|
        self.message = message
 | 
						|
        self.path = self.relative_path = deque(path)
 | 
						|
        self.schema_path = self.relative_schema_path = deque(schema_path)
 | 
						|
        self.context = list(context)
 | 
						|
        self.cause = self.__cause__ = cause
 | 
						|
        self.validator = validator
 | 
						|
        self.validator_value = validator_value
 | 
						|
        self.instance = instance
 | 
						|
        self.schema = schema
 | 
						|
        self.parent = parent
 | 
						|
 | 
						|
        for error in context:
 | 
						|
            error.parent = self
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        return "<%s: %r>" % (self.__class__.__name__, self.message)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return unicode(self).encode("utf-8")
 | 
						|
 | 
						|
    def __unicode__(self):
 | 
						|
        essential_for_verbose = (
 | 
						|
            self.validator, self.validator_value, self.instance, self.schema,
 | 
						|
        )
 | 
						|
        if any(m is _unset for m in essential_for_verbose):
 | 
						|
            return self.message
 | 
						|
 | 
						|
        pschema = pprint.pformat(self.schema, width=72)
 | 
						|
        pinstance = pprint.pformat(self.instance, width=72)
 | 
						|
        return self.message + textwrap.dedent("""
 | 
						|
 | 
						|
            Failed validating %r in schema%s:
 | 
						|
            %s
 | 
						|
 | 
						|
            On instance%s:
 | 
						|
            %s
 | 
						|
            """.rstrip()
 | 
						|
        ) % (
 | 
						|
            self.validator,
 | 
						|
            _utils.format_as_index(list(self.relative_schema_path)[:-1]),
 | 
						|
            _utils.indent(pschema),
 | 
						|
            _utils.format_as_index(self.relative_path),
 | 
						|
            _utils.indent(pinstance),
 | 
						|
        )
 | 
						|
 | 
						|
    if PY3:
 | 
						|
        __str__ = __unicode__
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def create_from(cls, other):
 | 
						|
        return cls(**other._contents())
 | 
						|
 | 
						|
    @property
 | 
						|
    def absolute_path(self):
 | 
						|
        parent = self.parent
 | 
						|
        if parent is None:
 | 
						|
            return self.relative_path
 | 
						|
 | 
						|
        path = deque(self.relative_path)
 | 
						|
        path.extendleft(reversed(parent.absolute_path))
 | 
						|
        return path
 | 
						|
 | 
						|
    @property
 | 
						|
    def absolute_schema_path(self):
 | 
						|
        parent = self.parent
 | 
						|
        if parent is None:
 | 
						|
            return self.relative_schema_path
 | 
						|
 | 
						|
        path = deque(self.relative_schema_path)
 | 
						|
        path.extendleft(reversed(parent.absolute_schema_path))
 | 
						|
        return path
 | 
						|
 | 
						|
    def _set(self, **kwargs):
 | 
						|
        for k, v in iteritems(kwargs):
 | 
						|
            if getattr(self, k) is _unset:
 | 
						|
                setattr(self, k, v)
 | 
						|
 | 
						|
    def _contents(self):
 | 
						|
        attrs = (
 | 
						|
            "message", "cause", "context", "validator", "validator_value",
 | 
						|
            "path", "schema_path", "instance", "schema", "parent",
 | 
						|
        )
 | 
						|
        return dict((attr, getattr(self, attr)) for attr in attrs)
 | 
						|
 | 
						|
 | 
						|
class ValidationError(_Error):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class SchemaError(_Error):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class RefResolutionError(Exception):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class UnknownType(Exception):
 | 
						|
    def __init__(self, type, instance, schema):
 | 
						|
        self.type = type
 | 
						|
        self.instance = instance
 | 
						|
        self.schema = schema
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return unicode(self).encode("utf-8")
 | 
						|
 | 
						|
    def __unicode__(self):
 | 
						|
        pschema = pprint.pformat(self.schema, width=72)
 | 
						|
        pinstance = pprint.pformat(self.instance, width=72)
 | 
						|
        return textwrap.dedent("""
 | 
						|
            Unknown type %r for validator with schema:
 | 
						|
            %s
 | 
						|
 | 
						|
            While checking instance:
 | 
						|
            %s
 | 
						|
            """.rstrip()
 | 
						|
        ) % (self.type, _utils.indent(pschema), _utils.indent(pinstance))
 | 
						|
 | 
						|
    if PY3:
 | 
						|
        __str__ = __unicode__
 | 
						|
 | 
						|
 | 
						|
 | 
						|
class FormatError(Exception):
 | 
						|
    def __init__(self, message, cause=None):
 | 
						|
        super(FormatError, self).__init__(message, cause)
 | 
						|
        self.message = message
 | 
						|
        self.cause = self.__cause__ = cause
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return self.message.encode("utf-8")
 | 
						|
 | 
						|
    def __unicode__(self):
 | 
						|
        return self.message
 | 
						|
 | 
						|
    if PY3:
 | 
						|
        __str__ = __unicode__
 | 
						|
 | 
						|
 | 
						|
class ErrorTree(object):
 | 
						|
    """
 | 
						|
    ErrorTrees make it easier to check which validations failed.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    _instance = _unset
 | 
						|
 | 
						|
    def __init__(self, errors=()):
 | 
						|
        self.errors = {}
 | 
						|
        self._contents = defaultdict(self.__class__)
 | 
						|
 | 
						|
        for error in errors:
 | 
						|
            container = self
 | 
						|
            for element in error.path:
 | 
						|
                container = container[element]
 | 
						|
            container.errors[error.validator] = error
 | 
						|
 | 
						|
            self._instance = error.instance
 | 
						|
 | 
						|
    def __contains__(self, index):
 | 
						|
        """
 | 
						|
        Check whether ``instance[index]`` has any errors.
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
        return index in self._contents
 | 
						|
 | 
						|
    def __getitem__(self, index):
 | 
						|
        """
 | 
						|
        Retrieve the child tree one level down at the given ``index``.
 | 
						|
 | 
						|
        If the index is not in the instance that this tree corresponds to and
 | 
						|
        is not known by this tree, whatever error would be raised by
 | 
						|
        ``instance.__getitem__`` will be propagated (usually this is some
 | 
						|
        subclass of :class:`LookupError`.
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
        if self._instance is not _unset and index not in self:
 | 
						|
            self._instance[index]
 | 
						|
        return self._contents[index]
 | 
						|
 | 
						|
    def __setitem__(self, index, value):
 | 
						|
        self._contents[index] = value
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        """
 | 
						|
        Iterate (non-recursively) over the indices in the instance with errors.
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
        return iter(self._contents)
 | 
						|
 | 
						|
    def __len__(self):
 | 
						|
        """
 | 
						|
        Same as :attr:`total_errors`.
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
        return self.total_errors
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        return "<%s (%s total errors)>" % (self.__class__.__name__, len(self))
 | 
						|
 | 
						|
    @property
 | 
						|
    def total_errors(self):
 | 
						|
        """
 | 
						|
        The total number of errors in the entire tree, including children.
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
        child_errors = sum(len(tree) for _, tree in iteritems(self._contents))
 | 
						|
        return len(self.errors) + child_errors
 | 
						|
 | 
						|
 | 
						|
def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
 | 
						|
    def relevance(error):
 | 
						|
        validator = error.validator
 | 
						|
        return -len(error.path), validator not in weak, validator in strong
 | 
						|
    return relevance
 | 
						|
 | 
						|
 | 
						|
relevance = by_relevance()
 | 
						|
 | 
						|
 | 
						|
def best_match(errors, key=relevance):
 | 
						|
    errors = iter(errors)
 | 
						|
    best = next(errors, None)
 | 
						|
    if best is None:
 | 
						|
        return
 | 
						|
    best = max(itertools.chain([best], errors), key=key)
 | 
						|
 | 
						|
    while best.context:
 | 
						|
        best = min(best.context, key=key)
 | 
						|
    return best
 |