Module typing_validation.validation_failure

Validation failure tracking.

Expand source code
"""
    Validation failure tracking.
"""

import sys
import typing
from typing import Any, Optional

def _indent(msg: str) -> str:
    """ Indent a block of text (possibly with newlines) """
    ind = " "*2
    return ind+msg.replace("\n", "\n"+ind)

_Acc = typing.TypeVar("_Acc")
_T = typing.TypeVar("_T", bound="ValidationFailure")

class ValidationFailure:
    """
        Simple container class for validation failures.
    """

    _val: Any
    _t: Any
    _causes: typing.Tuple["ValidationFailure", ...]
    _is_union: bool

    def __new__(cls: typing.Type[_T],
                val: Any, t: Any,
                *causes: "ValidationFailure",
                is_union: bool = False) -> _T:
        instance: _T = super().__new__(cls)
        instance._val = val
        instance._t = t
        instance._causes = causes
        instance._is_union = is_union
        if is_union:
            assert all(cause.val == val for cause in causes)
        return instance

    @property
    def val(self) -> Any:
        """ The value involved in the validation failure. """
        return self._val

    @property
    def t(self) -> Any:
        """ The type involved in the validation failure. """
        return self._t

    @property
    def causes(self) -> typing.Tuple["ValidationFailure", ...]:
        """ Validation failure that in turn caused this failure (if any). """
        return self._causes

    @property
    def is_union(self) -> bool:
        """ Whether this validation failure concerns a union type. """
        return self._is_union

    def visit(self, fun: typing.Callable[[Any, Any, _Acc], _Acc], acc: _Acc) -> None:
        """
            Performs a pre-order visit of the validation failure tree:

            1. applies `fun(self.val, self.t, acc)` to the failure,
            2. saves the return value as `new_acc`
            3. recurses on all causes using `new_acc`.

            Example usage to pretty-print the validation failure tree using [rich](https://github.com/willmcgugan/rich):

            ```py
            Python 3.9.7
            >>> import rich
            >>> from typing import Union, Collection
            >>> from typing_validation import validate, latest_validation_failure
            >>> validate([[0, 1, 2], {"hi": 0}], list[Union[Collection[int], dict[str, str]]])
            TypeError: ...
            >>> failure_tree = rich.tree.Tree("Failure tree")
            >>> def tree_builder(val, t, tree_tip) -> None:
            ...     label = rich.text.Text(f"({repr(t)}, {repr(val)})")
            ...     return tree_tip.add(label) # see https://rich.readthedocs.io/en/latest/tree.html
            ...
            >>> latest_validation_failure().visit(tree_builder, failure_tree)
            >>> rich.print(failure_tree)
            Failure tree
            └── (list[typing.Union[typing.Collection[int], dict[str, str]]], [[0, 1, 2], {'hi': 0}])
                └── (typing.Union[typing.Collection[int], dict[str, str]], {'hi': 0})
                    ├── (typing.Collection[int], {'hi': 0})
                    │   └── (<class 'int'>, 'hi')
                    └── (dict[str, str], {'hi': 0})
                        └── (<class 'str'>, 0)
            ```
        """
        new_acc = fun(self.val, self.t, acc)
        for cause in self.causes:
            cause.visit(fun, new_acc)

    def __str__(self) -> str:
        msg = f"For type {repr(self.t)}, invalid value: {repr(self.val)}"
        if self._is_union:
            for cause in (cause for cause in self.causes if cause.causes):
                msg += "\n"+_indent(f"Detailed failures for member type {repr(cause.t)}:")
                for sub_cause in cause.causes:
                    msg += "\n"+_indent(_indent(str(sub_cause)))
        else:
            for cause in self.causes:
                msg += "\n"+_indent(str(cause))
        return msg

    def __repr__(self) -> str:
        causes_str = ""
        if self.causes:
            causes_str = ", "+", ".join(repr(cause) for cause in self.causes)
        is_union_str = ""
        if self._is_union:
            is_union_str = ", is_union=True"
        return f"ValidationFailure({repr(self.val)}, {repr(self.t)}{causes_str}{is_union_str})"

def get_validation_failure(err: TypeError) -> Optional[ValidationFailure]:
    """
        Programmatic access to the validation failure tree for the latest validation call.
        Must be called on the type error raised by `validate`.

        ```py
        Python 3.9.7
        >>> from typing_validation import validate, get_validation_failure
        >>> try:
        ...     validate([[0, 1], [1, 2], [2, "hi"]], list[list[int]])
        ... except TypeError as err:
        ...     validation_failure = get_validation_failure(err)
        ...
        >>> validation_failure
        ValidationFailure([[0, 1], [1, 2], [2, 'hi']], list[list[int]],
            ValidationFailure([2, 'hi'], list[int],
                ValidationFailure('hi', <class 'int'>)))
        ```
    """
    if not isinstance(err, TypeError):
        raise TypeError(f"Expected TypeError, found {type(err)}")
    if not hasattr(err, "validation_failure"):
        return None
    validation_failure = getattr(err, "validation_failure")
    if not isinstance(validation_failure, ValidationFailure):
        return None
    return validation_failure

def latest_validation_failure() -> Optional[ValidationFailure]:
    """
        Programmatic access to the validation failure tree for the latest validation call.
        Uses `sys.last_value()`, so it must be called immediately after the error occurred.

        ```py
        Python 3.9.7
        >>> from typing_validation import validate, latest_validation_failure
        >>> validate([[0, 1], [1, 2], [2, "hi"]], list[list[int]])
        TypeError: ...
        >>> latest_validation_failure()
        ValidationFailure([[0, 1], [1, 2], [2, 'hi']], list[list[int]],
            ValidationFailure([2, 'hi'], list[int],
                ValidationFailure('hi', <class 'int'>)))
        ```
    """
    try:
        err = sys.last_value # pylint: disable = no-member
    except AttributeError:
        return None
    if not isinstance(err, TypeError):
        return None
    return get_validation_failure(err)

Functions

def get_validation_failure(err: TypeError) ‑> Optional[ValidationFailure]

Programmatic access to the validation failure tree for the latest validation call. Must be called on the type error raised by validate.

Python 3.9.7
>>> from typing_validation import validate, get_validation_failure
>>> try:
...     validate([[0, 1], [1, 2], [2, "hi"]], list[list[int]])
... except TypeError as err:
...     validation_failure = get_validation_failure(err)
...
>>> validation_failure
ValidationFailure([[0, 1], [1, 2], [2, 'hi']], list[list[int]],
    ValidationFailure([2, 'hi'], list[int],
        ValidationFailure('hi', <class 'int'>)))
Expand source code
def get_validation_failure(err: TypeError) -> Optional[ValidationFailure]:
    """
        Programmatic access to the validation failure tree for the latest validation call.
        Must be called on the type error raised by `validate`.

        ```py
        Python 3.9.7
        >>> from typing_validation import validate, get_validation_failure
        >>> try:
        ...     validate([[0, 1], [1, 2], [2, "hi"]], list[list[int]])
        ... except TypeError as err:
        ...     validation_failure = get_validation_failure(err)
        ...
        >>> validation_failure
        ValidationFailure([[0, 1], [1, 2], [2, 'hi']], list[list[int]],
            ValidationFailure([2, 'hi'], list[int],
                ValidationFailure('hi', <class 'int'>)))
        ```
    """
    if not isinstance(err, TypeError):
        raise TypeError(f"Expected TypeError, found {type(err)}")
    if not hasattr(err, "validation_failure"):
        return None
    validation_failure = getattr(err, "validation_failure")
    if not isinstance(validation_failure, ValidationFailure):
        return None
    return validation_failure
def latest_validation_failure() ‑> Optional[ValidationFailure]

Programmatic access to the validation failure tree for the latest validation call. Uses sys.last_value(), so it must be called immediately after the error occurred.

Python 3.9.7
>>> from typing_validation import validate, latest_validation_failure
>>> validate([[0, 1], [1, 2], [2, "hi"]], list[list[int]])
TypeError: ...
>>> latest_validation_failure()
ValidationFailure([[0, 1], [1, 2], [2, 'hi']], list[list[int]],
    ValidationFailure([2, 'hi'], list[int],
        ValidationFailure('hi', <class 'int'>)))
Expand source code
def latest_validation_failure() -> Optional[ValidationFailure]:
    """
        Programmatic access to the validation failure tree for the latest validation call.
        Uses `sys.last_value()`, so it must be called immediately after the error occurred.

        ```py
        Python 3.9.7
        >>> from typing_validation import validate, latest_validation_failure
        >>> validate([[0, 1], [1, 2], [2, "hi"]], list[list[int]])
        TypeError: ...
        >>> latest_validation_failure()
        ValidationFailure([[0, 1], [1, 2], [2, 'hi']], list[list[int]],
            ValidationFailure([2, 'hi'], list[int],
                ValidationFailure('hi', <class 'int'>)))
        ```
    """
    try:
        err = sys.last_value # pylint: disable = no-member
    except AttributeError:
        return None
    if not isinstance(err, TypeError):
        return None
    return get_validation_failure(err)

Classes

class ValidationFailure (val: Any, t: Any, *causes: ValidationFailure, is_union: bool = False)

Simple container class for validation failures.

Expand source code
class ValidationFailure:
    """
        Simple container class for validation failures.
    """

    _val: Any
    _t: Any
    _causes: typing.Tuple["ValidationFailure", ...]
    _is_union: bool

    def __new__(cls: typing.Type[_T],
                val: Any, t: Any,
                *causes: "ValidationFailure",
                is_union: bool = False) -> _T:
        instance: _T = super().__new__(cls)
        instance._val = val
        instance._t = t
        instance._causes = causes
        instance._is_union = is_union
        if is_union:
            assert all(cause.val == val for cause in causes)
        return instance

    @property
    def val(self) -> Any:
        """ The value involved in the validation failure. """
        return self._val

    @property
    def t(self) -> Any:
        """ The type involved in the validation failure. """
        return self._t

    @property
    def causes(self) -> typing.Tuple["ValidationFailure", ...]:
        """ Validation failure that in turn caused this failure (if any). """
        return self._causes

    @property
    def is_union(self) -> bool:
        """ Whether this validation failure concerns a union type. """
        return self._is_union

    def visit(self, fun: typing.Callable[[Any, Any, _Acc], _Acc], acc: _Acc) -> None:
        """
            Performs a pre-order visit of the validation failure tree:

            1. applies `fun(self.val, self.t, acc)` to the failure,
            2. saves the return value as `new_acc`
            3. recurses on all causes using `new_acc`.

            Example usage to pretty-print the validation failure tree using [rich](https://github.com/willmcgugan/rich):

            ```py
            Python 3.9.7
            >>> import rich
            >>> from typing import Union, Collection
            >>> from typing_validation import validate, latest_validation_failure
            >>> validate([[0, 1, 2], {"hi": 0}], list[Union[Collection[int], dict[str, str]]])
            TypeError: ...
            >>> failure_tree = rich.tree.Tree("Failure tree")
            >>> def tree_builder(val, t, tree_tip) -> None:
            ...     label = rich.text.Text(f"({repr(t)}, {repr(val)})")
            ...     return tree_tip.add(label) # see https://rich.readthedocs.io/en/latest/tree.html
            ...
            >>> latest_validation_failure().visit(tree_builder, failure_tree)
            >>> rich.print(failure_tree)
            Failure tree
            └── (list[typing.Union[typing.Collection[int], dict[str, str]]], [[0, 1, 2], {'hi': 0}])
                └── (typing.Union[typing.Collection[int], dict[str, str]], {'hi': 0})
                    ├── (typing.Collection[int], {'hi': 0})
                    │   └── (<class 'int'>, 'hi')
                    └── (dict[str, str], {'hi': 0})
                        └── (<class 'str'>, 0)
            ```
        """
        new_acc = fun(self.val, self.t, acc)
        for cause in self.causes:
            cause.visit(fun, new_acc)

    def __str__(self) -> str:
        msg = f"For type {repr(self.t)}, invalid value: {repr(self.val)}"
        if self._is_union:
            for cause in (cause for cause in self.causes if cause.causes):
                msg += "\n"+_indent(f"Detailed failures for member type {repr(cause.t)}:")
                for sub_cause in cause.causes:
                    msg += "\n"+_indent(_indent(str(sub_cause)))
        else:
            for cause in self.causes:
                msg += "\n"+_indent(str(cause))
        return msg

    def __repr__(self) -> str:
        causes_str = ""
        if self.causes:
            causes_str = ", "+", ".join(repr(cause) for cause in self.causes)
        is_union_str = ""
        if self._is_union:
            is_union_str = ", is_union=True"
        return f"ValidationFailure({repr(self.val)}, {repr(self.t)}{causes_str}{is_union_str})"

Instance variables

var causes : Tuple[ValidationFailure, ...]

Validation failure that in turn caused this failure (if any).

Expand source code
@property
def causes(self) -> typing.Tuple["ValidationFailure", ...]:
    """ Validation failure that in turn caused this failure (if any). """
    return self._causes
var is_union : bool

Whether this validation failure concerns a union type.

Expand source code
@property
def is_union(self) -> bool:
    """ Whether this validation failure concerns a union type. """
    return self._is_union
var t : Any

The type involved in the validation failure.

Expand source code
@property
def t(self) -> Any:
    """ The type involved in the validation failure. """
    return self._t
var val : Any

The value involved in the validation failure.

Expand source code
@property
def val(self) -> Any:
    """ The value involved in the validation failure. """
    return self._val

Methods

def visit(self, fun: Callable[[Any, Any, ~_Acc], ~_Acc], acc: ~_Acc) ‑> None

Performs a pre-order visit of the validation failure tree:

  1. applies fun(self.val, self.t, acc) to the failure,
  2. saves the return value as new_acc
  3. recurses on all causes using new_acc.

Example usage to pretty-print the validation failure tree using rich:

Python 3.9.7
>>> import rich
>>> from typing import Union, Collection
>>> from typing_validation import validate, latest_validation_failure
>>> validate([[0, 1, 2], {"hi": 0}], list[Union[Collection[int], dict[str, str]]])
TypeError: ...
>>> failure_tree = rich.tree.Tree("Failure tree")
>>> def tree_builder(val, t, tree_tip) -> None:
...     label = rich.text.Text(f"({repr(t)}, {repr(val)})")
...     return tree_tip.add(label) # see https://rich.readthedocs.io/en/latest/tree.html
...
>>> latest_validation_failure().visit(tree_builder, failure_tree)
>>> rich.print(failure_tree)
Failure tree
└── (list[typing.Union[typing.Collection[int], dict[str, str]]], [[0, 1, 2], {'hi': 0}])
    └── (typing.Union[typing.Collection[int], dict[str, str]], {'hi': 0})
        ├── (typing.Collection[int], {'hi': 0})
        │   └── (<class 'int'>, 'hi')
        └── (dict[str, str], {'hi': 0})
            └── (<class 'str'>, 0)
Expand source code
def visit(self, fun: typing.Callable[[Any, Any, _Acc], _Acc], acc: _Acc) -> None:
    """
        Performs a pre-order visit of the validation failure tree:

        1. applies `fun(self.val, self.t, acc)` to the failure,
        2. saves the return value as `new_acc`
        3. recurses on all causes using `new_acc`.

        Example usage to pretty-print the validation failure tree using [rich](https://github.com/willmcgugan/rich):

        ```py
        Python 3.9.7
        >>> import rich
        >>> from typing import Union, Collection
        >>> from typing_validation import validate, latest_validation_failure
        >>> validate([[0, 1, 2], {"hi": 0}], list[Union[Collection[int], dict[str, str]]])
        TypeError: ...
        >>> failure_tree = rich.tree.Tree("Failure tree")
        >>> def tree_builder(val, t, tree_tip) -> None:
        ...     label = rich.text.Text(f"({repr(t)}, {repr(val)})")
        ...     return tree_tip.add(label) # see https://rich.readthedocs.io/en/latest/tree.html
        ...
        >>> latest_validation_failure().visit(tree_builder, failure_tree)
        >>> rich.print(failure_tree)
        Failure tree
        └── (list[typing.Union[typing.Collection[int], dict[str, str]]], [[0, 1, 2], {'hi': 0}])
            └── (typing.Union[typing.Collection[int], dict[str, str]], {'hi': 0})
                ├── (typing.Collection[int], {'hi': 0})
                │   └── (<class 'int'>, 'hi')
                └── (dict[str, str], {'hi': 0})
                    └── (<class 'str'>, 0)
        ```
    """
    new_acc = fun(self.val, self.t, acc)
    for cause in self.causes:
        cause.visit(fun, new_acc)