Coverage for src/dataknobs_data/validation_v2/result.py: 100%

43 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-15 12:29 -0500

1""" 

2Validation result types with consistent, predictable behavior. 

3""" 

4 

5from dataclasses import dataclass, field 

6from typing import Any, List, Optional, Dict, Set 

7 

8 

9@dataclass 

10class ValidationResult: 

11 """ 

12 Unified result object for all validation operations. 

13  

14 This class provides a consistent return type for all validation operations, 

15 making the API predictable and easy to use. 

16 """ 

17 

18 valid: bool 

19 value: Any # The (possibly coerced) value 

20 errors: List[str] = field(default_factory=list) 

21 warnings: List[str] = field(default_factory=list) 

22 

23 def __bool__(self) -> bool: 

24 """Allow 'if result:' usage to check validity.""" 

25 return self.valid 

26 

27 def merge(self, other: 'ValidationResult') -> 'ValidationResult': 

28 """ 

29 Combine results for composite validation. 

30  

31 Args: 

32 other: Another ValidationResult to merge with this one 

33  

34 Returns: 

35 New ValidationResult with combined state 

36 """ 

37 return ValidationResult( 

38 valid=self.valid and other.valid, 

39 value=other.value if other.valid else self.value, 

40 errors=self.errors + other.errors, 

41 warnings=self.warnings + other.warnings 

42 ) 

43 

44 def add_error(self, error: str) -> 'ValidationResult': 

45 """ 

46 Add an error and mark as invalid (fluent API). 

47  

48 Args: 

49 error: Error message to add 

50  

51 Returns: 

52 Self for chaining 

53 """ 

54 self.errors.append(error) 

55 self.valid = False 

56 return self 

57 

58 def add_warning(self, warning: str) -> 'ValidationResult': 

59 """ 

60 Add a warning without affecting validity (fluent API). 

61  

62 Args: 

63 warning: Warning message to add 

64  

65 Returns: 

66 Self for chaining 

67 """ 

68 self.warnings.append(warning) 

69 return self 

70 

71 @classmethod 

72 def success(cls, value: Any, warnings: Optional[List[str]] = None) -> 'ValidationResult': 

73 """ 

74 Create a successful validation result. 

75  

76 Args: 

77 value: The validated value 

78 warnings: Optional list of warnings 

79  

80 Returns: 

81 Successful ValidationResult 

82 """ 

83 return cls( 

84 valid=True, 

85 value=value, 

86 errors=[], 

87 warnings=warnings or [] 

88 ) 

89 

90 @classmethod 

91 def failure(cls, value: Any, errors: List[str], warnings: Optional[List[str]] = None) -> 'ValidationResult': 

92 """ 

93 Create a failed validation result. 

94  

95 Args: 

96 value: The value that failed validation 

97 errors: List of error messages 

98 warnings: Optional list of warnings 

99  

100 Returns: 

101 Failed ValidationResult 

102 """ 

103 return cls( 

104 valid=False, 

105 value=value, 

106 errors=errors, 

107 warnings=warnings or [] 

108 ) 

109 

110 

111@dataclass 

112class ValidationContext: 

113 """ 

114 Context for stateful validation operations. 

115  

116 Used by constraints like Unique that need to track state across 

117 multiple validations. 

118 """ 

119 

120 seen_values: Dict[str, Set[Any]] = field(default_factory=dict) 

121 metadata: Dict[str, Any] = field(default_factory=dict) 

122 

123 def has_seen(self, field: str, value: Any) -> bool: 

124 """ 

125 Check if a value has been seen for a field. 

126  

127 Args: 

128 field: Field name 

129 value: Value to check 

130  

131 Returns: 

132 True if value has been seen for this field 

133 """ 

134 return field in self.seen_values and value in self.seen_values[field] 

135 

136 def mark_seen(self, field: str, value: Any) -> None: 

137 """ 

138 Mark a value as seen for a field. 

139  

140 Args: 

141 field: Field name 

142 value: Value to mark as seen 

143 """ 

144 if field not in self.seen_values: 

145 self.seen_values[field] = set() 

146 self.seen_values[field].add(value) 

147 

148 def clear(self, field: Optional[str] = None) -> None: 

149 """ 

150 Clear seen values. 

151  

152 Args: 

153 field: Optional field to clear. If None, clears all fields. 

154 """ 

155 if field: 

156 self.seen_values.pop(field, None) 

157 else: 

158 self.seen_values.clear() 

159 

160 def set_metadata(self, key: str, value: Any) -> None: 

161 """ 

162 Store metadata in the context. 

163  

164 Args: 

165 key: Metadata key 

166 value: Metadata value 

167 """ 

168 self.metadata[key] = value 

169 

170 def get_metadata(self, key: str, default: Any = None) -> Any: 

171 """ 

172 Retrieve metadata from the context. 

173  

174 Args: 

175 key: Metadata key 

176 default: Default value if key not found 

177  

178 Returns: 

179 Metadata value or default 

180 """ 

181 return self.metadata.get(key, default)