Source code for typing_validation.errors

# SPDX-License-Identifier: LGPL-3.0-or-later

"""
Errors raised by validation.

The two errors answer different questions — *"I cannot check this"* versus
*"I checked this and it is wrong"* — and they deliberately share no base beyond
:class:`Exception`. A caller must be able to tell them apart, and a test must
never be able to mistake one for the other.
"""

from typing import TYPE_CHECKING, Any, Self, final

if TYPE_CHECKING:
    from .diagnosis import ValidationFailure

__all__ = ("UnsupportedTypeError", "ValidationError")


[docs] @final class UnsupportedTypeError(NotImplementedError): """ Raised when a type is not one that this library knows how to validate against. Support is all-or-nothing: an unsupported component makes the whole type unsupported, so this is raised for ``tuple[int, Callable[[int], int]]`` even though the ``int`` component is perfectly checkable. This is a :class:`NotImplementedError` rather than a validation failure: the value was never in question. Use :func:`~typing_validation.inspection.can_validate` to ask in advance whether a type can be honoured at all. """ __slots__ = ("_t", "_explanation") _t: Any """The type that cannot be validated against.""" _explanation: str | None """Additional detail about what is unsupported, and what would fix it.""" def __new__(cls, t: Any, explanation: str | None = None, /) -> Self: """ ``t`` is the **whole** type, not the component at fault. The explanation is where the component is named, along with whatever would gain support for it. """ self: Self = NotImplementedError.__new__(cls, t, explanation) self._t = t self._explanation = explanation return self @property def t(self) -> Any: """The type that cannot be validated against.""" return self._t @property def explanation(self) -> str | None: """Additional detail about what is unsupported, and what would fix it.""" return self._explanation def __str__(self) -> str: msg = f"Unsupported validation for type {self._t!r}." if self._explanation is not None: msg += "\n" + self._explanation return msg
[docs] @final class ValidationError(TypeError): """ Raised when a value is not valid for a type. This is a :class:`TypeError`, so ``except TypeError`` catches it. It is a *distinct* subclass so that you can tell *"this library rejected the value"* apart from *"something raised a TypeError"* — catch this rather than the base class if you mean the former. The structured explanation of what went wrong, and where, is on :attr:`failure`. """ # The distinct subclass is not decoration. v1 raised a bare TypeError, so its # own test sweep read a crash inside isinstance as a correct rejection, and # the bug shipped for eleven releases. The exception hierarchy is what makes # the test rule in DESIGN.md §10 enforceable. __slots__ = ("_val", "_t", "_failure") _val: Any """The value that failed validation.""" _t: Any """The type the value was validated against.""" _failure: "ValidationFailure | None" """The structured explanation, when one was built.""" def __new__( cls, val: Any, t: Any, failure: "ValidationFailure | None" = None, / ) -> Self: """ The failure is optional because not every raiser builds one: ``validated_iter`` reports the item it stopped at without diagnosing the whole iterable. """ self: Self = TypeError.__new__(cls, val, t, failure) self._val = val self._t = t self._failure = failure return self @property def failure(self) -> "ValidationFailure | None": """ The structured explanation of what went wrong, and where. :obj:`None` when nothing built one — ``validated_iter`` reports the item it stopped at without diagnosing the whole iterable. """ return self._failure @property def val(self) -> Any: """The value that failed validation.""" return self._val @property def t(self) -> Any: """The type the value was validated against.""" return self._t def __str__(self) -> str: if self._failure is not None: return str(self._failure) return f"For type {self._t!r}, invalid value: {self._val!r}"