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

1"""Invariant errors. 

2 

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. 

6 

7Exception hierarchy: 

8 InvariantViolationError (base) 

9 ├── DuplicatesError (for duplicate element violations) 

10 └── AggregateInvariantViolationError (for multiple violations) 

11 

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}") 

17 

18""" 

19 

20from typing import TYPE_CHECKING, Any 

21 

22if TYPE_CHECKING: 

23 from collections.abc import Sequence 

24 

25 

26class InvariantViolationError(Exception): 

27 """Base exception raised when an invariant is violated. 

28 

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. 

31 

32 Example: 

33 >>> raise InvariantViolationError("Value must be positive") 

34 Traceback (most recent call last): 

35 ... 

36 InvariantViolationError: Value must be positive 

37 

38 """ 

39 

40 

41class DuplicatesError(InvariantViolationError): 

42 """Exception raised when duplicate elements are detected in a collection. 

43 

44 This specialized error stores the duplicate values for inspection and 

45 generates a formatted error message listing all duplicates found. 

46 

47 Attributes: 

48 duplicates: Sequence of duplicate values that were found. 

49 

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 

58 

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 

64 

65 """ 

66 

67 def __init__(self, duplicates: Sequence[Any], message: str | None = None) -> None: 

68 """Initialize DuplicatesError with duplicate values. 

69 

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. 

74 

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) 

81 

82 

83class AggregateInvariantViolationError(InvariantViolationError): 

84 """Exception that aggregates multiple invariant violations. 

85 

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. 

90 

91 Attributes: 

92 errors: Sequence of individual InvariantViolationError instances. 

93 

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 

105 

106 Note: 

107 This is typically raised automatically by the validation chain's 

108 execute().raise_if_invalid() method when multiple assertions fail. 

109 

110 """ 

111 

112 def __init__(self, errors: Sequence[InvariantViolationError]) -> None: 

113 """Initialize AggregateInvariantViolationError with multiple errors. 

114 

115 Automatically formats the error message to list all violations, 

116 using singular or plural form based on the number of errors. 

117 

118 Args: 

119 errors: Sequence of InvariantViolationError instances to aggregate. 

120 Should contain at least one error. 

121 

122 """ 

123 self.errors = errors 

124 count = len(errors) 

125 is_plural = count > 1 

126 

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 ) 

137 

138 super().__init__(message)