Coverage for src / ocarina / dsl / invariants / errors.py: 33.33%
16 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-04 16:22 +0200
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-04 16:22 +0200
1"""Invariant errors.
3This module defines exception types for invariant violations. These exceptions
4are raised when validation predicates fail, allowing for structured error handling
5and aggregation of multiple validation failures.
7Exception hierarchy:
8 InvariantViolationError (base)
9 ├── DuplicatesError (for duplicate element violations)
10 └── AggregateInvariantViolationError (for multiple violations)
12Example:
13 >>> try:
14 ... validate(data).assert_that(is_positive).execute().raise_if_invalid()
15 ... except InvariantViolationError as e:
16 ... logger.error(f"Validation failed: {e}")
18"""
20from typing import TYPE_CHECKING, Any
22if TYPE_CHECKING:
23 from collections.abc import Sequence
26class InvariantViolationError(Exception):
27 """Base exception raised when an invariant is violated.
29 This is the base exception for all validation failures in the invariants
30 system. It can be caught to handle any validation error uniformly.
32 Example:
33 >>> raise InvariantViolationError("Value must be positive")
34 Traceback (most recent call last):
35 ...
36 InvariantViolationError: Value must be positive
38 """
41class DuplicatesError(InvariantViolationError):
42 """Exception raised when duplicate elements are detected in a collection.
44 This specialized error stores the duplicate values for inspection and
45 generates a formatted error message listing all duplicates found.
47 Attributes:
48 duplicates: Sequence of duplicate values that were found.
50 Example:
51 >>> duplicates = ["apple", "banana"]
52 >>> raise DuplicatesError(duplicates)
53 Traceback (most recent call last):
54 ...
55 DuplicatesError: Duplicate elements detected:
56 - apple
57 - banana
59 >>> # With custom message
60 >>> raise DuplicatesError(duplicates, "Test names must be unique")
61 Traceback (most recent call last):
62 ...
63 DuplicatesError: Test names must be unique
65 """
67 def __init__(self, duplicates: Sequence[Any], message: str | None = None) -> None:
68 """Initialize DuplicatesError with duplicate values.
70 Args:
71 duplicates: Sequence of duplicate values found in the collection.
72 message: Optional custom error message. If None, a default message
73 listing all duplicates will be generated.
75 """
76 self.duplicates = duplicates
77 msg = message or "Duplicate elements detected:\n" + "\n".join(
78 f" - {d}" for d in duplicates
79 )
80 super().__init__(msg)
83class AggregateInvariantViolationError(InvariantViolationError):
84 """Exception that aggregates multiple invariant violations.
86 This exception is raised when multiple validation predicates fail, allowing
87 all failures to be reported together rather than stopping at the first error.
88 This is particularly useful in validation chains where you want to collect
89 all errors before raising.
91 Attributes:
92 errors: Sequence of individual InvariantViolationError instances.
94 Example:
95 >>> errors = [
96 ... InvariantViolationError("Value must be positive"),
97 ... InvariantViolationError("Value must be less than 100")
98 ... ]
99 >>> raise AggregateInvariantViolationError(errors)
100 Traceback (most recent call last):
101 ...
102 AggregateInvariantViolationError: 2 invariant violations occurred:
103 > Value must be positive
104 > Value must be less than 100
106 Note:
107 This is typically raised automatically by the validation chain's
108 execute().raise_if_invalid() method when multiple assertions fail.
110 """
112 def __init__(self, errors: Sequence[InvariantViolationError]) -> None:
113 """Initialize AggregateInvariantViolationError with multiple errors.
115 Automatically formats the error message to list all violations,
116 using singular or plural form based on the number of errors.
118 Args:
119 errors: Sequence of InvariantViolationError instances to aggregate.
120 Should contain at least one error.
122 """
123 self.errors = errors
124 count = len(errors)
125 is_plural = count > 1
127 if is_plural:
128 message = f"{len(errors)} invariant violations occurred:\n" + "\n".join(
129 f"› {e}" # noqa: RUF001 -> This is intentional.
130 for e in errors
131 )
132 else:
133 message = "Invariant violation occurred:\n" + "\n".join(
134 f"› {e}" # noqa: RUF001 -> This is intentional.
135 for e in errors
136 )
138 super().__init__(message)