Coverage for src / dataknobs_data / validation / result.py: 59%

44 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-26 15:45 -0700

1"""Validation result types with consistent, predictable behavior. 

2""" 

3 

4from __future__ import annotations 

5 

6from dataclasses import dataclass, field 

7from typing import Any 

8 

9 

10@dataclass 

11class ValidationResult: 

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 """Combine results for composite validation. 

29  

30 Args: 

31 other: Another ValidationResult to merge with this one 

32  

33 Returns: 

34 New ValidationResult with combined state 

35 """ 

36 return ValidationResult( 

37 valid=self.valid and other.valid, 

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

39 errors=self.errors + other.errors, 

40 warnings=self.warnings + other.warnings 

41 ) 

42 

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

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

45  

46 Args: 

47 error: Error message to add 

48  

49 Returns: 

50 Self for chaining 

51 """ 

52 self.errors.append(error) 

53 self.valid = False 

54 return self 

55 

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

57 """Add a warning without affecting validity (fluent API). 

58  

59 Args: 

60 warning: Warning message to add 

61  

62 Returns: 

63 Self for chaining 

64 """ 

65 self.warnings.append(warning) 

66 return self 

67 

68 @classmethod 

69 def success(cls, value: Any, warnings: list[str] | None = None) -> ValidationResult: 

70 """Create a successful validation result. 

71  

72 Args: 

73 value: The validated value 

74 warnings: Optional list of warnings 

75  

76 Returns: 

77 Successful ValidationResult 

78 """ 

79 return cls( 

80 valid=True, 

81 value=value, 

82 errors=[], 

83 warnings=warnings or [] 

84 ) 

85 

86 @classmethod 

87 def failure(cls, value: Any, errors: list[str], warnings: list[str] | None = None) -> ValidationResult: 

88 """Create a failed validation result. 

89  

90 Args: 

91 value: The value that failed validation 

92 errors: List of error messages 

93 warnings: Optional list of warnings 

94  

95 Returns: 

96 Failed ValidationResult 

97 """ 

98 return cls( 

99 valid=False, 

100 value=value, 

101 errors=errors, 

102 warnings=warnings or [] 

103 ) 

104 

105 

106@dataclass 

107class ValidationContext: 

108 """Context for stateful validation operations. 

109  

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

111 multiple validations. 

112 """ 

113 

114 seen_values: dict[str, set[Any]] = field(default_factory=dict) 

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

116 

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

118 """Check if a value has been seen for a field. 

119  

120 Args: 

121 field: Field name 

122 value: Value to check 

123  

124 Returns: 

125 True if value has been seen for this field 

126 """ 

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

128 

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

130 """Mark a value as seen for a field. 

131  

132 Args: 

133 field: Field name 

134 value: Value to mark as seen 

135 """ 

136 if field not in self.seen_values: 

137 self.seen_values[field] = set() 

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

139 

140 def clear(self, field: str | None = None) -> None: 

141 """Clear seen values. 

142  

143 Args: 

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

145 """ 

146 if field: 

147 self.seen_values.pop(field, None) 

148 else: 

149 self.seen_values.clear() 

150 

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

152 """Store metadata in the context. 

153  

154 Args: 

155 key: Metadata key 

156 value: Metadata value 

157 """ 

158 self.metadata[key] = value 

159 

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

161 """Retrieve metadata from the context. 

162  

163 Args: 

164 key: Metadata key 

165 default: Default value if key not found 

166  

167 Returns: 

168 Metadata value or default 

169 """ 

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