Source code for schematics.exceptions

# -*- coding: utf-8 -*-

from __future__ import unicode_literals, absolute_import

import json

from .common import *
from .compat import string_type, str_compat
from .datastructures import FrozenDict, FrozenList
from .translator import LazyText

try:
    from collections.abc import Mapping, Sequence  # PY3
except ImportError:
    from collections import Mapping, Sequence  # PY2


__all__ = [
    'BaseError', 'ErrorMessage', 'FieldError', 'ConversionError',
    'ValidationError', 'StopValidationError', 'CompoundError', 'DataError',
    'MockCreationError', 'UndefinedValueError', 'UnknownFieldError']


@str_compat
class BaseError(Exception):

    def __init__(self, errors):
        """
        The base class for all Schematics errors.

        message should be a human-readable message,
        while errors is a machine-readable list, or dictionary.

        if None is passed as the message, and error is populated,
        the primitive representation will be serialized.

        the Python logging module expects exceptions to be hashable
        and therefore immutable. As a result, it is not possible to
        mutate BaseError's error list or dict after initialization.
        """
        errors = self._freeze(errors)
        super(BaseError, self).__init__(errors)

    @property
    def errors(self):
        return self.args[0]

    def to_primitive(self):
        """
        converts the errors dict to a primitive representation of dicts,
        list and strings.
        """
        if not hasattr(self, "_primitive"):
            self._primitive = self._to_primitive(self.errors)
        return self._primitive

    @staticmethod
    def _freeze(obj):
        """ freeze common data structures to something immutable. """
        if isinstance(obj, dict):
            return FrozenDict(obj)
        elif isinstance(obj, list):
            return FrozenList(obj)
        else:
            return obj

    @classmethod
    def _to_primitive(cls, obj):
        """ recursive to_primitive for basic data types. """
        if isinstance(obj, string_type):
            return obj
        if isinstance(obj, Sequence):
            return [cls._to_primitive(e) for e in obj]
        elif isinstance(obj, Mapping):
            return dict(
                (k, cls._to_primitive(v)) for k, v in obj.items()
            )
        else:
            return str(obj)

    def __str__(self):
        return json.dumps(self.to_primitive())

    def __repr__(self):
        return "%s(%s)" % (self.__class__.__name__, repr(self.errors))

    def __hash__(self):
        return hash(self.errors)

    def __eq__(self, other):
        if type(self) is type(other):
            return self.errors == other.errors
        else:
            return self.errors == other
        return False

    def __ne__(self, other):
        return not (self == other)


@str_compat
class ErrorMessage(object):

    def __init__(self, summary, info=None):
        self.type = None
        self.summary = summary
        self.info = info

    def __repr__(self):
        return "%s(%s, %s)" % (
            self.__class__.__name__,
            repr(self.summary),
            repr(self.info)
        )

    def __str__(self):
        if self.info:
            return '%s: %s' % (self.summary, self._info_as_str())
        else:
            return '%s' % self.summary

    def _info_as_str(self):
        if isinstance(self.info, int):
            return str(self.info)
        elif isinstance(self.info, string_type):
            return '"%s"' % self.info
        else:
            return str(self.info)

    def __eq__(self, other):
        if isinstance(other, ErrorMessage):
            return (
                self.summary == other.summary and
                self.type == other.type and
                self.info == other.info
            )
        elif isinstance(other, string_type):
            return self.summary == other
        else:
            return False

    def __ne__(self, other):
        return not (self == other)

    def __hash__(self):
        return hash((self.summary, self.type, self.info))


class FieldError(BaseError, Sequence):

    type = None

    def __init__(self, *args, **kwargs):

        if type(self) is FieldError:
            raise NotImplementedError("Please raise either ConversionError or ValidationError.")
        if len(args) == 0:
            raise TypeError("Please provide at least one error or error message.")
        if kwargs:
            items = [ErrorMessage(*args, **kwargs)]
        elif len(args) == 1:
            arg = args[0]
            if isinstance(arg, list):
                items = list(arg)
            else:
                items = [arg]
        else:
            items = args
        errors = []
        for item in items:
            if isinstance(item, (string_type, LazyText)):
                errors.append(ErrorMessage(str(item)))
            elif isinstance(item, tuple):
                errors.append(ErrorMessage(*item))
            elif isinstance(item, ErrorMessage):
                errors.append(item)
            elif isinstance(item, self.__class__):
                errors.extend(item.errors)
            else:
                raise TypeError("'{0}()' object is neither a {1} nor an error message."\
                                .format(type(item).__name__, type(self).__name__))
        for error in errors:
            error.type = self.type or type(self)

        super(FieldError, self).__init__(errors)

    def __contains__(self, value):
        return value in self.errors

    def __getitem__(self, index):
        return self.errors[index]

    def __iter__(self):
        return iter(self.errors)

    def __len__(self):
        return len(self.errors)


class ConversionError(FieldError, TypeError):
    """ Exception raised when data cannot be converted to the correct python type """
    pass


class ValidationError(FieldError, ValueError):
    """Exception raised when invalid data is encountered."""
    pass


class StopValidationError(ValidationError):
    """Exception raised when no more validation need occur."""
    type = ValidationError


class CompoundError(BaseError):

    def __init__(self, errors):
        if not isinstance(errors, dict):
            raise TypeError("Compound errors must be reported as a dictionary.")
        for key, value in errors.items():
            if isinstance(value, CompoundError):
                errors[key] = value.errors
            else:
                errors[key] = value
        super(CompoundError, self).__init__(errors)


class DataError(CompoundError):

    def __init__(self, errors, partial_data=None):
        super(DataError, self).__init__(errors)
        self.partial_data = partial_data


class MockCreationError(ValueError):
    """Exception raised when a mock value cannot be generated."""
    pass


class UndefinedValueError(AttributeError, KeyError):
    """Exception raised when accessing a field with an undefined value."""
    def __init__(self, model, name):
        msg = "'%s' instance has no value for field '%s'" % (model.__class__.__name__, name)
        super(UndefinedValueError, self).__init__(msg)


class UnknownFieldError(KeyError):
    """Exception raised when attempting to access a nonexistent field using the subscription syntax."""
    def __init__(self, model, name):
        msg = "Model '%s' has no field named '%s'" % (model.__class__.__name__, name)
        super(UnknownFieldError, self).__init__(msg)


if PY2:
    # Python 2 names cannot be unicode
    __all__ = [n.encode('ascii') for n in __all__]