Coverage for openhcs/core/components/validation.py: 64.6%
63 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
1"""
2Generic validation system for component-agnostic validation.
4This module provides a generic replacement for the component-specific validation
5logic, supporting any component configuration and validation patterns.
6"""
8import logging
9from typing import Generic, TypeVar, List, Optional, Dict, Any, Union, Type
10from enum import Enum
11from dataclasses import dataclass
13from .framework import ComponentConfiguration
15logger = logging.getLogger(__name__)
17T = TypeVar('T', bound=Enum)
18U = TypeVar('U', bound=Enum)
21def convert_enum_by_value(source_enum: T, target_enum_class: Type[U]) -> Optional[U]:
22 """
23 Generic utility to convert between enum types with matching .value attributes.
25 This function enables conversion between any two enum classes that have
26 overlapping values, without requiring hardcoded mappings.
28 Args:
29 source_enum: Source enum instance to convert from
30 target_enum_class: Target enum class to convert to
32 Returns:
33 Target enum instance with matching value, or None if no match found
35 Example:
36 >>> convert_enum_by_value(VariableComponents.CHANNEL, GroupBy)
37 <GroupBy.CHANNEL: 'channel'>
38 """
39 source_value = source_enum.value
41 for target_enum in target_enum_class: 41 ↛ 45line 41 didn't jump to line 45 because the loop on line 41 didn't complete
42 if target_enum.value == source_value:
43 return target_enum
45 return None
48@dataclass
49class ValidationResult:
50 """Result of a validation operation."""
51 is_valid: bool
52 error_message: Optional[str] = None
53 warnings: Optional[List[str]] = None
56class GenericValidator(Generic[T]):
57 """
58 Generic validator for component-agnostic validation.
60 This class replaces the hardcoded component-specific validation logic
61 with a configurable system that works with any component configuration.
62 """
64 def __init__(self, config: ComponentConfiguration[T]):
65 """
66 Initialize the validator with a component configuration.
68 Args:
69 config: ComponentConfiguration for validation rules
70 """
71 self.config = config
72 logger.debug(f"GenericValidator initialized for components: {[c.value for c in config.all_components]}")
74 def validate_step(
75 self,
76 variable_components: List[T],
77 group_by: Optional[Union[T, 'GroupBy']],
78 func_pattern: Any,
79 step_name: str
80 ) -> ValidationResult:
81 """
82 Validate a step configuration using generic rules.
84 Args:
85 variable_components: List of variable components
86 group_by: Optional group_by component
87 func_pattern: Function pattern (callable, dict, or list)
88 step_name: Name of the step for error reporting
90 Returns:
91 ValidationResult indicating success or failure
92 """
93 try:
94 # 1. Validate component combination
95 self.config.validate_combination(variable_components, group_by)
97 # 2. Validate dict pattern requirements
98 if isinstance(func_pattern, dict) and not group_by: 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true
99 return ValidationResult(
100 is_valid=False,
101 error_message=f"Dict pattern requires group_by in step '{step_name}'"
102 )
104 # 3. Validate components are in remaining components (not multiprocessing axis)
105 remaining_components = self.config.get_remaining_components()
106 remaining_values = {comp.value for comp in remaining_components}
108 for component in variable_components:
109 if component.value not in remaining_values: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 return ValidationResult(
111 is_valid=False,
112 error_message=f"Variable component {component.value} not available (multiprocessing axis: {self.config.multiprocessing_axis.value})"
113 )
115 if group_by and group_by.value not in remaining_values: 115 ↛ 116line 115 didn't jump to line 116 because the condition on line 115 was never true
116 return ValidationResult(
117 is_valid=False,
118 error_message=f"Group_by component {group_by.value} not available (multiprocessing axis: {self.config.multiprocessing_axis.value})"
119 )
121 return ValidationResult(is_valid=True)
123 except ValueError as e:
124 return ValidationResult(
125 is_valid=False,
126 error_message=str(e)
127 )
129 def validate_dict_pattern_keys(
130 self,
131 func_pattern: Dict[str, Any],
132 group_by: T,
133 step_name: str,
134 orchestrator
135 ) -> ValidationResult:
136 """
137 Validate that dict function pattern keys match available component keys.
139 This validation ensures compile-time guarantee that dict patterns will work
140 at runtime by checking that all dict keys exist in the actual component data.
142 Args:
143 func_pattern: Dict function pattern to validate
144 group_by: GroupBy component specifying component type
145 step_name: Name of the step containing the function
146 orchestrator: Orchestrator for component key access
148 Returns:
149 ValidationResult indicating success or failure
150 """
151 try:
152 # Use enum objects directly - orchestrator now accepts VariableComponents
153 available_keys = orchestrator.get_component_keys(group_by)
154 available_keys_set = set(str(key) for key in available_keys)
156 # Check each dict key against available keys
157 pattern_keys = list(func_pattern.keys())
158 pattern_keys_set = set(str(key) for key in pattern_keys)
160 # Try direct string match first
161 missing_keys = pattern_keys_set - available_keys_set
163 if missing_keys: 163 ↛ 165line 163 didn't jump to line 165 because the condition on line 163 was never true
164 # Try numeric conversion for better error reporting
165 try:
166 available_numeric = {str(int(float(k))) for k in available_keys if str(k).replace('.', '').isdigit()}
167 pattern_numeric = {str(int(float(k))) for k in pattern_keys if str(k).replace('.', '').isdigit()}
168 missing_numeric = pattern_numeric - available_numeric
170 if missing_numeric:
171 return ValidationResult(
172 is_valid=False,
173 error_message=(
174 f"Function pattern keys {sorted(missing_numeric)} not found in available "
175 f"{group_by.value} components {sorted(available_numeric)} for step '{step_name}'"
176 )
177 )
178 except (ValueError, TypeError):
179 # Fall back to string comparison
180 return ValidationResult(
181 is_valid=False,
182 error_message=(
183 f"Function pattern keys {sorted(missing_keys)} not found in available "
184 f"{group_by.value} components {sorted(available_keys_set)} for step '{step_name}'"
185 )
186 )
188 return ValidationResult(is_valid=True)
190 except Exception as e:
191 return ValidationResult(
192 is_valid=False,
193 error_message=f"Failed to validate dict pattern keys for {group_by.value}: {e}"
194 )
196 def validate_component_combination_constraint(
197 self,
198 variable_components: List[T],
199 group_by: Optional[T]
200 ) -> ValidationResult:
201 """
202 Validate the core constraint: group_by ∉ variable_components.
204 Args:
205 variable_components: List of variable components
206 group_by: Optional group_by component
208 Returns:
209 ValidationResult indicating success or failure
210 """
211 try:
212 self.config.validate_combination(variable_components, group_by)
213 return ValidationResult(is_valid=True)
214 except ValueError as e:
215 return ValidationResult(
216 is_valid=False,
217 error_message=str(e)
218 )