Skip to content

input4mips_validation.validation.error_catching#

input4mips_validation.validation.error_catching #

Tools for catching errors in validation without stopping

ValidationResult #

The result of a validation operation

This is basically a result class, similar to what is often used in railway oriented programming.

Source code in src/input4mips_validation/validation/error_catching.py
@define
class ValidationResult:
    """
    The result of a validation operation

    This is basically a result class,
    similar to what is often used in railway oriented programming.
    """

    description: str
    """Description of the validation operation"""

    passed: bool
    """Whether the validation passed or not"""

    result: Any = field(default=None)
    """If the validation passed, the result of the function that was called."""

    exception: Union[Exception, None] = field(
        default=None, validator=exception_info_consistent_with_passed
    )
    """If the validation failed, the exception that was raised."""

    exception_info: Union[str, None] = field(
        default=None, validator=exception_info_consistent_with_passed
    )

    """
    If the validation failed, the exception information.

    This is typically created with `traceback.format_exc`.
    """

    @property
    def failed(self) -> bool:
        """Whether the validation failed or not"""
        return not self.passed

description: str instance-attribute #

Description of the validation operation

exception: Union[Exception, None] = field(default=None, validator=exception_info_consistent_with_passed) class-attribute instance-attribute #

If the validation failed, the exception that was raised.

exception_info: Union[str, None] = field(default=None, validator=exception_info_consistent_with_passed) class-attribute instance-attribute #

If the validation failed, the exception information.

This is typically created with traceback.format_exc.

failed: bool property #

Whether the validation failed or not

passed: bool instance-attribute #

Whether the validation passed or not

result: Any = field(default=None) class-attribute instance-attribute #

If the validation passed, the result of the function that was called.

ValidationResultsStore #

Store for validation results

Source code in src/input4mips_validation/validation/error_catching.py
@define
class ValidationResultsStore:
    """
    Store for validation results
    """

    validation_results: list[ValidationResult] = field(factory=list)
    """Stored validation results"""

    @property
    def all_passed(self) -> bool:
        """Whether all the validation steps passed or not"""
        return all(v.passed for v in self.validation_results)

    @property
    def checks_passing(self) -> tuple[ValidationResult, ...]:
        """Checks that passed"""
        return tuple(v for v in self.validation_results if v.passed)

    @property
    def checks_failing(self) -> tuple[ValidationResult, ...]:
        """Checks that failed"""
        return tuple(v for v in self.validation_results if v.failed)

    def wrap(
        self, func_to_call: Callable[P, T], func_description: str
    ) -> Callable[P, ValidationResult]:
        """
        Wrap a validation function

        The results of calling the validation function will be stored by `self`.

        Parameters
        ----------
        func_to_call
            Function to call

        func_description
            A description of `func_to_call`.

            This helps us create clearer messages when processing validation results.

        Returns
        -------
        :
            The wrapped function.

            The wrapped function is altered so it always returns a result,
            irrespective of whether `func_to_call` raised an error not.
        """

        @wraps(func_to_call)
        def decorated(*args: P.args, **kwargs: P.kwargs) -> ValidationResult:
            try:
                res_func = func_to_call(*args, **kwargs)
                res = ValidationResult(
                    description=func_description,
                    passed=True,
                    result=res_func,
                )
                logger.log(
                    LOG_LEVEL_INFO_INDIVIDUAL_CHECK.name,
                    f"{func_description} ran without error",
                )

            except Exception as exc:
                logger.log(
                    LOG_LEVEL_INFO_INDIVIDUAL_CHECK_ERROR.name,
                    f"{func_description} raised an error ({type(exc).__name__})",
                )
                res = ValidationResult(
                    description=func_description,
                    passed=False,
                    exception=exc,
                    exception_info=traceback.format_exc(),
                )

            self.validation_results.append(res)

            return res

        return decorated

    def raise_if_errors(self) -> None:
        """
        Raise a `ValidationError` if any of the validation steps failed

        Raises
        ------
        ValidationError
            One of the validation steps in `self.validation_results` failed.
        """
        if not self.all_passed:
            raise ValidationResultsStoreError(self)

    def checks_summary_str(self, passing: bool) -> str:
        """
        Get a summary of the checks we have performed

        Parameters
        ----------
        passing
            Should we return the summary as the number of checks
            which are passing (`True`) or failing (`False`)?

        Returns
        -------
        :
            Summary of the checks
        """
        denominator = len(self.validation_results)
        if passing:
            numerator = len(self.checks_passing)

        else:
            numerator = len(self.checks_failing)

        pct = numerator / denominator * 100
        return f"{pct:.2f}% ({numerator} / {denominator})"

all_passed: bool property #

Whether all the validation steps passed or not

checks_failing: tuple[ValidationResult, ...] property #

Checks that failed

checks_passing: tuple[ValidationResult, ...] property #

Checks that passed

validation_results: list[ValidationResult] = field(factory=list) class-attribute instance-attribute #

Stored validation results

checks_summary_str(passing) #

Get a summary of the checks we have performed

Parameters:

Name Type Description Default
passing bool

Should we return the summary as the number of checks which are passing (True) or failing (False)?

required

Returns:

Type Description
str

Summary of the checks

Source code in src/input4mips_validation/validation/error_catching.py
def checks_summary_str(self, passing: bool) -> str:
    """
    Get a summary of the checks we have performed

    Parameters
    ----------
    passing
        Should we return the summary as the number of checks
        which are passing (`True`) or failing (`False`)?

    Returns
    -------
    :
        Summary of the checks
    """
    denominator = len(self.validation_results)
    if passing:
        numerator = len(self.checks_passing)

    else:
        numerator = len(self.checks_failing)

    pct = numerator / denominator * 100
    return f"{pct:.2f}% ({numerator} / {denominator})"

raise_if_errors() #

Raise a ValidationError if any of the validation steps failed

Raises:

Type Description
ValidationError

One of the validation steps in self.validation_results failed.

Source code in src/input4mips_validation/validation/error_catching.py
def raise_if_errors(self) -> None:
    """
    Raise a `ValidationError` if any of the validation steps failed

    Raises
    ------
    ValidationError
        One of the validation steps in `self.validation_results` failed.
    """
    if not self.all_passed:
        raise ValidationResultsStoreError(self)

wrap(func_to_call, func_description) #

Wrap a validation function

The results of calling the validation function will be stored by self.

Parameters:

Name Type Description Default
func_to_call Callable[P, T]

Function to call

required
func_description str

A description of func_to_call.

This helps us create clearer messages when processing validation results.

required

Returns:

Type Description
Callable[P, ValidationResult]

The wrapped function.

The wrapped function is altered so it always returns a result, irrespective of whether func_to_call raised an error not.

Source code in src/input4mips_validation/validation/error_catching.py
def wrap(
    self, func_to_call: Callable[P, T], func_description: str
) -> Callable[P, ValidationResult]:
    """
    Wrap a validation function

    The results of calling the validation function will be stored by `self`.

    Parameters
    ----------
    func_to_call
        Function to call

    func_description
        A description of `func_to_call`.

        This helps us create clearer messages when processing validation results.

    Returns
    -------
    :
        The wrapped function.

        The wrapped function is altered so it always returns a result,
        irrespective of whether `func_to_call` raised an error not.
    """

    @wraps(func_to_call)
    def decorated(*args: P.args, **kwargs: P.kwargs) -> ValidationResult:
        try:
            res_func = func_to_call(*args, **kwargs)
            res = ValidationResult(
                description=func_description,
                passed=True,
                result=res_func,
            )
            logger.log(
                LOG_LEVEL_INFO_INDIVIDUAL_CHECK.name,
                f"{func_description} ran without error",
            )

        except Exception as exc:
            logger.log(
                LOG_LEVEL_INFO_INDIVIDUAL_CHECK_ERROR.name,
                f"{func_description} raised an error ({type(exc).__name__})",
            )
            res = ValidationResult(
                description=func_description,
                passed=False,
                exception=exc,
                exception_info=traceback.format_exc(),
            )

        self.validation_results.append(res)

        return res

    return decorated

ValidationResultsStoreError #

Bases: ValueError

Raised to signal that an error occured during validation

Specifically, that a ValidationResultsStore object contains failed validation results.

Source code in src/input4mips_validation/validation/error_catching.py
class ValidationResultsStoreError(ValueError):
    """
    Raised to signal that an error occured during validation

    Specifically, that a
    [`ValidationResultsStore`][input4mips_validation.validation.error_catching.ValidationResultsStore]
    object contains failed validation results.
    """

    def __init__(self, vrs: ValidationResultsStore) -> None:
        """
        Initialise the error

        Parameters
        ----------
        vrs
            The validation results store that contains failures.
        """
        error_msg_l: list[str] = [
            f"Checks passing: {vrs.checks_summary_str(passing=True)}",
            f"Checks failing: {vrs.checks_summary_str(passing=False)}",
        ]

        checks_failing = vrs.checks_failing
        if checks_failing:
            error_msg_l.append("")
            error_msg_l.append("Failing checks details")
            for gcf in checks_failing:
                error_msg_l.append("")
                error_msg_l.append(
                    f"{gcf.description} ({type(gcf.exception).__name__})"
                )
                if gcf.exception_info is None:
                    msg = "Should have an exception here"
                    raise AssertionError(msg)

                error_msg_l.extend(gcf.exception_info.splitlines())

        error_msg = "\n".join(error_msg_l)

        super().__init__(error_msg)

__init__(vrs) #

Initialise the error

Parameters:

Name Type Description Default
vrs ValidationResultsStore

The validation results store that contains failures.

required
Source code in src/input4mips_validation/validation/error_catching.py
def __init__(self, vrs: ValidationResultsStore) -> None:
    """
    Initialise the error

    Parameters
    ----------
    vrs
        The validation results store that contains failures.
    """
    error_msg_l: list[str] = [
        f"Checks passing: {vrs.checks_summary_str(passing=True)}",
        f"Checks failing: {vrs.checks_summary_str(passing=False)}",
    ]

    checks_failing = vrs.checks_failing
    if checks_failing:
        error_msg_l.append("")
        error_msg_l.append("Failing checks details")
        for gcf in checks_failing:
            error_msg_l.append("")
            error_msg_l.append(
                f"{gcf.description} ({type(gcf.exception).__name__})"
            )
            if gcf.exception_info is None:
                msg = "Should have an exception here"
                raise AssertionError(msg)

            error_msg_l.extend(gcf.exception_info.splitlines())

    error_msg = "\n".join(error_msg_l)

    super().__init__(error_msg)

exception_info_consistent_with_passed(instance, attribute, value) #

Check the exception information is consistent with the passed status

Parameters:

Name Type Description Default
instance ValidationResult

Instance to check

required
attribute Attribute[Any]

Attribute being set

required
value Union[Any, None]

Value being set

required

Raises:

Type Description
ValueError

value is inconsistent with instance.passed.

Source code in src/input4mips_validation/validation/error_catching.py
def exception_info_consistent_with_passed(
    instance: ValidationResult,
    attribute: attr.Attribute[Any],
    value: Union[Any, None],
) -> None:
    """
    Check the exception information is consistent with the passed status

    Parameters
    ----------
    instance
        Instance to check

    attribute
        Attribute being set

    value
        Value being set

    Raises
    ------
    ValueError
        `value` is inconsistent with `instance.passed`.
    """
    if instance.passed and value is not None:
        msg = (
            "If the validation passed, "
            f"{attribute.name} must be `None`. "
            f"Received exception={value}"
        )
        raise ValueError(msg)

    if not instance.passed and value is None:
        msg = (
            "If the validation didn't pass, "
            f"you must provide {attribute.name}. "
            f"Received exception={value}"
        )
        raise ValueError(msg)