0.1.8
Loading...
Searching...
No Matches
core.py
Go to the documentation of this file.
1"""
2Core validation utilities for PyHelios.
3
4Provides decorators, type coercion, and standardized error handling
5following PyHelios's fail-fast philosophy.
6"""
7
8import functools
9import math
10from typing import Any, Callable, Dict, Union
11
12from .exceptions import ValidationError, create_validation_error
13
14
15def validate_input(param_validators: Dict[str, Callable] = None,
16 type_coercions: Dict[str, Callable] = None):
17 """
18 Decorator for comprehensive parameter validation.
19
20 Performs type coercion first, then validation, following the pattern:
21 1. Coerce types where safe (e.g., list to vec3)
22 2. Validate all parameters meet requirements
23 3. Call original function if validation passes
24
25 Args:
26 param_validators: Dict mapping parameter names to validation functions
27 type_coercions: Dict mapping parameter names to coercion functions
28
29 Example:
30 @validate_input(
31 param_validators={'center': validate_vec3, 'radius': validate_positive_value},
32 type_coercions={'center': coerce_to_vec3}
33 )
34 def addSphere(self, center, radius, **kwargs):
35 # center is guaranteed to be vec3, radius is positive
36 """
37 def decorator(func):
38 @functools.wraps(func)
39 def wrapper(*args, **kwargs):
40 # Perform type coercion first
41 if type_coercions:
42 for param, coercion_func in type_coercions.items():
43 if param in kwargs:
44 try:
45 kwargs[param] = coercion_func(kwargs[param], param_name=param)
46 except ValidationError:
47 raise # Re-raise validation errors from coercion
48 except Exception as e:
49 raise create_validation_error(
50 f"Failed to coerce parameter to expected type: {str(e)}",
51 param_name=param,
52 function_name=func.__name__
53 )
54
55 # Then validate parameters
56 if param_validators:
57 for param, validator in param_validators.items():
58 if param in kwargs:
59 try:
60 validator(kwargs[param], param_name=param, function_name=func.__name__)
61 except ValidationError:
62 raise # Re-raise validation errors
63 except Exception as e:
64 raise create_validation_error(
65 f"Parameter validation failed: {str(e)}",
66 param_name=param,
67 function_name=func.__name__
68 )
69
70 return func(*args, **kwargs)
71 return wrapper
72 return decorator
73
74
75def is_finite_numeric(value: Any) -> bool:
76 """Check if value is a finite number (not NaN or inf)."""
77 try:
78 float_value = float(value)
79 return math.isfinite(float_value)
80 except (ValueError, TypeError, OverflowError):
81 return False
82
83
84def validate_positive_value(value: Any, param_name: str = "value", function_name: str = None):
85 """
86 Validate value is positive and finite.
87
88 Args:
89 value: Value to validate
90 param_name: Parameter name for error messages
91 function_name: Function name for error messages
92
93 Raises:
94 ValidationError: If value is not positive or not finite
95 """
96 if not is_finite_numeric(value):
97 raise create_validation_error(
98 f"Parameter must be a finite number, got {value} ({type(value).__name__})",
99 param_name=param_name,
100 function_name=function_name,
101 expected_type="positive finite number",
102 actual_value=value
103 )
104
105 if value <= 0:
106 raise create_validation_error(
107 f"Parameter must be positive, got {value}",
108 param_name=param_name,
109 function_name=function_name,
110 expected_type="positive number",
111 actual_value=value,
112 suggestion="Use a value greater than 0."
113 )
114
115
116def validate_non_negative_value(value: Any, param_name: str = "value", function_name: str = None):
117 """
118 Validate value is non-negative and finite.
119
120 Args:
121 value: Value to validate
122 param_name: Parameter name for error messages
123 function_name: Function name for error messages
124
125 Raises:
126 ValidationError: If value is negative or not finite
127 """
128 if not is_finite_numeric(value):
129 raise create_validation_error(
130 f"Parameter must be a finite number, got {value} ({type(value).__name__})",
131 param_name=param_name,
132 function_name=function_name,
133 expected_type="non-negative finite number",
134 actual_value=value
135 )
136
137 if value < 0:
138 raise create_validation_error(
139 f"Parameter must be non-negative, got {value}",
140 param_name=param_name,
141 function_name=function_name,
142 expected_type="non-negative number",
143 actual_value=value,
144 suggestion="Use a value >= 0."
145 )
146
147
148def coerce_to_vec3(value: Any, param_name: str = "parameter") -> 'vec3':
149 """
150 Safely coerce list/tuple to vec3 with validation.
151
152 Args:
153 value: Value to coerce (vec3, list, or tuple)
154 param_name: Parameter name for error messages
155
156 Returns:
157 vec3 object
158
159 Raises:
160 ValidationError: If coercion fails or values are invalid
161 """
162 from ..wrappers.DataTypes import vec3
164 # Check if it's already a vec3 (using duck typing to avoid import issues)
165 if hasattr(value, 'x') and hasattr(value, 'y') and hasattr(value, 'z') and hasattr(value, 'to_list'):
166 return value
167
168 if isinstance(value, (list, tuple)):
169 if len(value) != 3:
170 raise create_validation_error(
171 f"Parameter must have exactly 3 elements for vec3 conversion, got {len(value)} elements: {value}",
172 param_name=param_name,
173 expected_type="3-element list or tuple",
174 actual_value=value,
175 suggestion="Provide exactly 3 numeric values like [x, y, z] or (x, y, z)."
176 )
177
178 # Validate each component is finite
179 for i, component in enumerate(value):
180 if not is_finite_numeric(component):
181 raise create_validation_error(
182 f"Parameter element [{i}] must be a finite number, got {component} ({type(component).__name__})",
183 param_name=f"{param_name}[{i}]",
184 expected_type="finite number",
185 actual_value=component,
186 suggestion="Ensure all coordinate values are finite numbers (not NaN or infinity)."
187 )
188
189 return vec3(float(value[0]), float(value[1]), float(value[2]))
190
191 raise create_validation_error(
192 f"Parameter must be a vec3, list, or tuple, got {type(value).__name__}",
193 param_name=param_name,
194 expected_type="vec3, list, or tuple",
195 actual_value=value,
196 suggestion="Use vec3(x, y, z), [x, y, z], or (x, y, z) format."
197 )
198
199
200def coerce_to_vec2(value: Any, param_name: str = "parameter") -> 'vec2':
201 """
202 Safely coerce list/tuple to vec2 with validation.
203
204 Args:
205 value: Value to coerce (vec2, list, or tuple)
206 param_name: Parameter name for error messages
207
208 Returns:
209 vec2 object
210
211 Raises:
212 ValidationError: If coercion fails or values are invalid
213 """
214 from ..wrappers.DataTypes import vec2
216 # Check if it's already a vec2 (using duck typing to avoid import issues)
217 if hasattr(value, 'x') and hasattr(value, 'y') and hasattr(value, 'to_list') and not hasattr(value, 'z'):
218 return value
219
220 if isinstance(value, (list, tuple)):
221 if len(value) != 2:
222 raise create_validation_error(
223 f"Parameter must have exactly 2 elements for vec2 conversion, got {len(value)} elements: {value}",
224 param_name=param_name,
225 expected_type="2-element list or tuple",
226 actual_value=value,
227 suggestion="Provide exactly 2 numeric values like [x, y] or (x, y)."
228 )
229
230 # Validate each component is finite
231 for i, component in enumerate(value):
232 if not is_finite_numeric(component):
233 raise create_validation_error(
234 f"Parameter element [{i}] must be a finite number, got {component} ({type(component).__name__})",
235 param_name=f"{param_name}[{i}]",
236 expected_type="finite number",
237 actual_value=component,
238 suggestion="Ensure all coordinate values are finite numbers (not NaN or infinity)."
239 )
240
241 return vec2(float(value[0]), float(value[1]))
242
243 raise create_validation_error(
244 f"Parameter must be a vec2, list, or tuple, got {type(value).__name__}",
245 param_name=param_name,
246 expected_type="vec2, list, or tuple",
247 actual_value=value,
248 suggestion="Use vec2(x, y), [x, y], or (x, y) format."
249 )
'vec3' coerce_to_vec3(Any value, str param_name="parameter")
Safely coerce list/tuple to vec3 with validation.
Definition core.py:163
validate_positive_value(Any value, str param_name="value", str function_name=None)
Validate value is positive and finite.
Definition core.py:97
'vec2' coerce_to_vec2(Any value, str param_name="parameter")
Safely coerce list/tuple to vec2 with validation.
Definition core.py:215
validate_non_negative_value(Any value, str param_name="value", str function_name=None)
Validate value is non-negative and finite.
Definition core.py:129
validate_input(Dict[str, Callable] param_validators=None, Dict[str, Callable] type_coercions=None)
Decorator for comprehensive parameter validation.
Definition core.py:38
bool is_finite_numeric(Any value)
Check if value is a finite number (not NaN or inf).
Definition core.py:78