v0.165.0
  1"""
  2Global Plain exception and warning classes.
  3"""
  4
  5import operator
  6from collections.abc import Iterator
  7from itertools import chain
  8from typing import Any
  9
 10from plain.utils.hashable import make_hashable
 11
 12# MARK: Configuration and Package Registry
 13
 14
 15class PackageRegistryNotReady(Exception):
 16    """The plain.packages registry is not populated yet"""
 17
 18
 19class ImproperlyConfigured(Exception):
 20    """Plain is somehow improperly configured"""
 21
 22
 23# MARK: Validation
 24
 25NON_FIELD_ERRORS = "__all__"
 26
 27
 28class ValidationError(Exception):
 29    """An error while validating data."""
 30
 31    def __init__(
 32        self,
 33        message: str | list[Any] | dict[str, Any] | ValidationError,
 34        code: str | None = None,
 35        params: dict[str, Any] | None = None,
 36    ):
 37        """
 38        The `message` argument can be a single error, a list of errors, or a
 39        dictionary that maps field names to lists of errors. What we define as
 40        an "error" can be either a simple string or an instance of
 41        ValidationError with its message attribute set, and what we define as
 42        list or dictionary can be an actual `list` or `dict` or an instance
 43        of ValidationError with its `error_list` or `error_dict` attribute set.
 44        """
 45        super().__init__(message, code, params)
 46
 47        if isinstance(message, ValidationError):
 48            if hasattr(message, "error_dict"):
 49                message = message.error_dict
 50            elif not hasattr(message, "message"):
 51                message = message.error_list
 52            else:
 53                message, code, params = message.message, message.code, message.params
 54
 55        if isinstance(message, dict):
 56            self.error_dict = {}
 57            for field, messages in message.items():
 58                if not isinstance(messages, ValidationError):
 59                    messages = ValidationError(messages)
 60                self.error_dict[field] = messages.error_list
 61
 62        elif isinstance(message, list):
 63            self.error_list = []
 64            for entry in message:
 65                # Normalize plain strings to instances of ValidationError.
 66                if not isinstance(entry, ValidationError):
 67                    entry = ValidationError(entry)
 68                if hasattr(entry, "error_dict"):
 69                    self.error_list.extend(
 70                        chain.from_iterable(entry.error_dict.values())
 71                    )
 72                else:
 73                    self.error_list.extend(entry.error_list)
 74
 75        else:
 76            self.message = message
 77            self.code = code
 78            self.params = params
 79            self.error_list = [self]
 80
 81    @property
 82    def messages(self) -> list[str]:
 83        if hasattr(self, "error_dict"):
 84            # dict(self) relies on __iter__ yielding pairs in the error_dict case.
 85            return list(chain.from_iterable(dict(self).values()))  # ty: ignore[no-matching-overload]
 86        return list(self)
 87
 88    def update_error_dict(
 89        self, error_dict: dict[str, list[ValidationError]]
 90    ) -> dict[str, list[ValidationError]]:
 91        if hasattr(self, "error_dict"):
 92            for field, error_list in self.error_dict.items():
 93                error_dict.setdefault(field, []).extend(error_list)
 94        else:
 95            error_dict.setdefault(NON_FIELD_ERRORS, []).extend(self.error_list)
 96        return error_dict
 97
 98    def __iter__(self) -> Iterator[tuple[str, list[str]] | str]:
 99        if hasattr(self, "error_dict"):
100            for field, errors in self.error_dict.items():
101                yield field, list(ValidationError(errors))  # ty: ignore[invalid-yield]
102        else:
103            for error in self.error_list:
104                message = error.message
105                if error.params:
106                    message %= error.params
107                yield str(message)
108
109    def __str__(self) -> str:
110        if hasattr(self, "error_dict"):
111            return repr(dict(self))  # ty: ignore[no-matching-overload]
112        return repr(list(self))
113
114    def __repr__(self) -> str:
115        return f"ValidationError({self})"
116
117    def __eq__(self, other: object) -> bool:
118        if not isinstance(other, ValidationError):
119            return NotImplemented
120        return hash(self) == hash(other)
121
122    def __hash__(self) -> int:
123        if hasattr(self, "message"):
124            return hash(
125                (
126                    self.message,
127                    self.code,
128                    make_hashable(self.params),
129                )
130            )
131        if hasattr(self, "error_dict"):
132            return hash(make_hashable(self.error_dict))
133        return hash(tuple(sorted(self.error_list, key=operator.attrgetter("message"))))