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