2Core validation utilities for PyHelios.
4Provides decorators, type coercion, and standardized error handling
5following PyHelios's fail-fast philosophy.
11from typing
import Any, Callable, Dict, Union
13from .exceptions
import ValidationError, create_validation_error
17 """Bind positional and keyword arguments to parameter names.
19 Returns a dict mapping parameter names to their values for all
20 explicitly provided arguments (excludes defaults for unprovided params).
21 Also returns updated args/kwargs suitable for calling the function
22 with any coerced values applied.
24 sig = inspect.signature(func)
26 bound = sig.bind(*args, **kwargs)
29 return {}, args, kwargs
31 return dict(bound.arguments), args, kwargs
35 type_coercions: Dict[str, Callable] =
None):
37 Decorator for comprehensive parameter validation.
39 Performs type coercion first, then validation, following the pattern:
40 1. Bind all arguments (positional and keyword) to parameter names
41 2. Coerce types where safe (e.g., list to vec3)
42 3. Validate all parameters meet requirements
43 4. Call original function if validation passes
46 param_validators: Dict mapping parameter names to validation functions
47 type_coercions: Dict mapping parameter names to coercion functions
50 @functools.wraps(func)
51 def wrapper(*args, **kwargs):
58 sig = inspect.signature(func)
59 param_names = list(sig.parameters.keys())
62 positional_params = {}
63 for i, arg
in enumerate(args):
64 if i < len(param_names):
65 positional_params[param_names[i]] = i
68 args_list = list(args)
72 for param, coercion_func
in type_coercions.items():
79 elif param
in positional_params:
80 value = args_list[positional_params[param]]
85 coerced = coercion_func(value, param_name=param)
88 kwargs[param] = coerced
89 elif param
in positional_params:
90 args_list[positional_params[param]] = coerced
91 except ValidationError:
93 except Exception
as e:
94 raise create_validation_error(
95 f
"Failed to coerce parameter to expected type: {str(e)}",
97 function_name=func.__name__
102 for param, validator
in param_validators.items():
107 value = kwargs[param]
109 elif param
in positional_params:
110 value = args_list[positional_params[param]]
115 validator(value, param_name=param, function_name=func.__name__)
116 except ValidationError:
118 except Exception
as e:
119 raise create_validation_error(
120 f
"Parameter validation failed: {str(e)}",
122 function_name=func.__name__
125 return func(*tuple(args_list), **kwargs)
131 """Check if value is a finite number (not NaN or inf)."""
133 float_value = float(value)
134 return math.isfinite(float_value)
135 except (ValueError, TypeError, OverflowError):
141 Validate value is positive and finite.
144 value: Value to validate
145 param_name: Parameter name for error messages
146 function_name: Function name for error messages
149 ValidationError: If value is not positive or not finite
152 raise create_validation_error(
153 f
"Parameter must be a finite number, got {value} ({type(value).__name__})",
154 param_name=param_name,
155 function_name=function_name,
156 expected_type=
"positive finite number",
161 raise create_validation_error(
162 f
"Parameter must be positive, got {value}",
163 param_name=param_name,
164 function_name=function_name,
165 expected_type=
"positive number",
167 suggestion=
"Use a value greater than 0."
173 Validate value is non-negative and finite.
176 value: Value to validate
177 param_name: Parameter name for error messages
178 function_name: Function name for error messages
181 ValidationError: If value is negative or not finite
184 raise create_validation_error(
185 f
"Parameter must be a finite number, got {value} ({type(value).__name__})",
186 param_name=param_name,
187 function_name=function_name,
188 expected_type=
"non-negative finite number",
193 raise create_validation_error(
194 f
"Parameter must be non-negative, got {value}",
195 param_name=param_name,
196 function_name=function_name,
197 expected_type=
"non-negative number",
199 suggestion=
"Use a value >= 0."
203def coerce_to_vec3(value: Any, param_name: str =
"parameter") ->
'vec3':
205 Safely coerce list/tuple to vec3 with validation.
208 value: Value to coerce (vec3, list, or tuple)
209 param_name: Parameter name for error messages
215 ValidationError: If coercion fails or values are invalid
217 from ..wrappers.DataTypes
import vec3
220 if hasattr(value,
'x')
and hasattr(value,
'y')
and hasattr(value,
'z')
and hasattr(value,
'to_list'):
223 if isinstance(value, (list, tuple)):
225 raise create_validation_error(
226 f
"Parameter must have exactly 3 elements for vec3 conversion, got {len(value)} elements: {value}",
227 param_name=param_name,
228 expected_type=
"3-element list or tuple",
230 suggestion=
"Provide exactly 3 numeric values like [x, y, z] or (x, y, z)."
234 for i, component
in enumerate(value):
236 raise create_validation_error(
237 f
"Parameter element [{i}] must be a finite number, got {component} ({type(component).__name__})",
238 param_name=f
"{param_name}[{i}]",
239 expected_type=
"finite number",
240 actual_value=component,
241 suggestion=
"Ensure all coordinate values are finite numbers (not NaN or infinity)."
244 return vec3(float(value[0]), float(value[1]), float(value[2]))
246 raise create_validation_error(
247 f
"Parameter must be a vec3, list, or tuple, got {type(value).__name__}",
248 param_name=param_name,
249 expected_type=
"vec3, list, or tuple",
251 suggestion=
"Use vec3(x, y, z), [x, y, z], or (x, y, z) format."
255def coerce_to_vec2(value: Any, param_name: str =
"parameter") ->
'vec2':
257 Safely coerce list/tuple to vec2 with validation.
260 value: Value to coerce (vec2, list, or tuple)
261 param_name: Parameter name for error messages
267 ValidationError: If coercion fails or values are invalid
269 from ..wrappers.DataTypes
import vec2
272 if hasattr(value,
'x')
and hasattr(value,
'y')
and hasattr(value,
'to_list')
and not hasattr(value,
'z'):
275 if isinstance(value, (list, tuple)):
277 raise create_validation_error(
278 f
"Parameter must have exactly 2 elements for vec2 conversion, got {len(value)} elements: {value}",
279 param_name=param_name,
280 expected_type=
"2-element list or tuple",
282 suggestion=
"Provide exactly 2 numeric values like [x, y] or (x, y)."
286 for i, component
in enumerate(value):
288 raise create_validation_error(
289 f
"Parameter element [{i}] must be a finite number, got {component} ({type(component).__name__})",
290 param_name=f
"{param_name}[{i}]",
291 expected_type=
"finite number",
292 actual_value=component,
293 suggestion=
"Ensure all coordinate values are finite numbers (not NaN or infinity)."
296 return vec2(float(value[0]), float(value[1]))
298 raise create_validation_error(
299 f
"Parameter must be a vec2, list, or tuple, got {type(value).__name__}",
300 param_name=param_name,
301 expected_type=
"vec2, list, or tuple",
303 suggestion=
"Use vec2(x, y), [x, y], or (x, y) format."
'vec3' coerce_to_vec3(Any value, str param_name="parameter")
Safely coerce list/tuple to vec3 with validation.
validate_positive_value(Any value, str param_name="value", str function_name=None)
Validate value is positive and finite.
'vec2' coerce_to_vec2(Any value, str param_name="parameter")
Safely coerce list/tuple to vec2 with validation.
validate_non_negative_value(Any value, str param_name="value", str function_name=None)
Validate value is non-negative and finite.
validate_input(Dict[str, Callable] param_validators=None, Dict[str, Callable] type_coercions=None)
Decorator for comprehensive parameter validation.
bool is_finite_numeric(Any value)
Check if value is a finite number (not NaN or inf).
_bind_args_to_params(func, args, kwargs)
Bind positional and keyword arguments to parameter names.