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