0.1.33
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 inspect
10import math
11from typing import Any, Callable, Dict, Union
12
13from .exceptions import ValidationError, create_validation_error
14
15
16def validate_input(param_validators: Dict[str, Callable] = None,
17 type_coercions: Dict[str, Callable] = None):
18 """
19 Decorator for comprehensive parameter validation.
20
21 Performs type coercion first, then validation, following the pattern:
22 1. Bind all arguments (positional and keyword) to parameter names
23 2. Coerce types where safe (e.g., list to vec3)
24 3. Validate all parameters meet requirements
25 4. Call original function if validation passes
26
27 Args:
28 param_validators: Dict mapping parameter names to validation functions
29 type_coercions: Dict mapping parameter names to coercion functions
30 """
31 def decorator(func):
32 # The signature is fixed once the function is defined, so resolve it
33 # here rather than on every call. inspect.signature() is not cached by
34 # CPython and dominated the cost of the primitive-creation APIs, which
35 # users call in per-primitive loops.
36 param_names = list(inspect.signature(func).parameters.keys())
37
38 @functools.wraps(func)
39 def wrapper(*args, **kwargs):
40 # Map positional args to their parameter names so that positional
41 # and keyword arguments validate identically.
42 positional_params = {}
43 for i, arg in enumerate(args):
44 if i < len(param_names):
45 positional_params[param_names[i]] = i
46
47 # Convert args to a mutable list for coercion
48 args_list = list(args)
49
50 # Perform type coercion first (on both positional and keyword args)
51 if type_coercions:
52 for param, coercion_func in type_coercions.items():
53 value = None
54 has_value = False
55
56 if param in kwargs:
57 value = kwargs[param]
58 has_value = True
59 elif param in positional_params:
60 value = args_list[positional_params[param]]
61 has_value = True
62
63 if has_value:
64 try:
65 coerced = coercion_func(value, param_name=param)
66 # Write back the coerced value
67 if param in kwargs:
68 kwargs[param] = coerced
69 elif param in positional_params:
70 args_list[positional_params[param]] = coerced
71 except ValidationError:
72 raise
73 except Exception as e:
74 raise create_validation_error(
75 f"Failed to coerce parameter to expected type: {str(e)}",
76 param_name=param,
77 function_name=func.__name__
78 )
79
80 # Then validate parameters (on both positional and keyword args)
81 if param_validators:
82 for param, validator in param_validators.items():
83 value = None
84 has_value = False
85
86 if param in kwargs:
87 value = kwargs[param]
88 has_value = True
89 elif param in positional_params:
90 value = args_list[positional_params[param]]
91 has_value = True
92
93 if has_value:
94 try:
95 validator(value, param_name=param, function_name=func.__name__)
96 except ValidationError:
97 raise
98 except Exception as e:
99 raise create_validation_error(
100 f"Parameter validation failed: {str(e)}",
101 param_name=param,
102 function_name=func.__name__
103 )
104
105 return func(*tuple(args_list), **kwargs)
106 return wrapper
107 return decorator
108
109
110def is_finite_numeric(value: Any) -> bool:
111 """Check if value is a finite number (not NaN or inf)."""
112 try:
113 float_value = float(value)
114 return math.isfinite(float_value)
115 except (ValueError, TypeError, OverflowError):
116 return False
117
118
119def validate_positive_value(value: Any, param_name: str = "value", function_name: str = None):
120 """
121 Validate value is positive and finite.
122
123 Args:
124 value: Value to validate
125 param_name: Parameter name for error messages
126 function_name: Function name for error messages
127
128 Raises:
129 ValidationError: If value is not positive or not finite
130 """
131 if not is_finite_numeric(value):
132 raise create_validation_error(
133 f"Parameter must be a finite number, got {value} ({type(value).__name__})",
134 param_name=param_name,
135 function_name=function_name,
136 expected_type="positive finite number",
137 actual_value=value
138 )
139
140 if value <= 0:
141 raise create_validation_error(
142 f"Parameter must be positive, got {value}",
143 param_name=param_name,
144 function_name=function_name,
145 expected_type="positive number",
146 actual_value=value,
147 suggestion="Use a value greater than 0."
148 )
149
150
151def validate_non_negative_value(value: Any, param_name: str = "value", function_name: str = None):
152 """
153 Validate value is non-negative and finite.
154
155 Args:
156 value: Value to validate
157 param_name: Parameter name for error messages
158 function_name: Function name for error messages
159
160 Raises:
161 ValidationError: If value is negative or not finite
162 """
163 if not is_finite_numeric(value):
164 raise create_validation_error(
165 f"Parameter must be a finite number, got {value} ({type(value).__name__})",
166 param_name=param_name,
167 function_name=function_name,
168 expected_type="non-negative finite number",
169 actual_value=value
170 )
171
172 if value < 0:
173 raise create_validation_error(
174 f"Parameter must be non-negative, got {value}",
175 param_name=param_name,
176 function_name=function_name,
177 expected_type="non-negative number",
178 actual_value=value,
179 suggestion="Use a value >= 0."
180 )
181
182
183def coerce_to_vec3(value: Any, param_name: str = "parameter") -> 'vec3':
184 """
185 Safely coerce list/tuple to vec3 with validation.
186
187 Args:
188 value: Value to coerce (vec3, list, or tuple)
189 param_name: Parameter name for error messages
190
191 Returns:
192 vec3 object
193
194 Raises:
195 ValidationError: If coercion fails or values are invalid
196 """
197 from ..wrappers.DataTypes import vec3
198
199 # Check if it's already a vec3 (using duck typing to avoid import issues)
200 if hasattr(value, 'x') and hasattr(value, 'y') and hasattr(value, 'z') and hasattr(value, 'to_list'):
201 return value
202
203 if isinstance(value, (list, tuple)):
204 if len(value) != 3:
205 raise create_validation_error(
206 f"Parameter must have exactly 3 elements for vec3 conversion, got {len(value)} elements: {value}",
207 param_name=param_name,
208 expected_type="3-element list or tuple",
209 actual_value=value,
210 suggestion="Provide exactly 3 numeric values like [x, y, z] or (x, y, z)."
211 )
212
213 # Validate each component is finite
214 for i, component in enumerate(value):
215 if not is_finite_numeric(component):
216 raise create_validation_error(
217 f"Parameter element [{i}] must be a finite number, got {component} ({type(component).__name__})",
218 param_name=f"{param_name}[{i}]",
219 expected_type="finite number",
220 actual_value=component,
221 suggestion="Ensure all coordinate values are finite numbers (not NaN or infinity)."
222 )
223
224 return vec3(float(value[0]), float(value[1]), float(value[2]))
225
226 raise create_validation_error(
227 f"Parameter must be a vec3, list, or tuple, got {type(value).__name__}",
228 param_name=param_name,
229 expected_type="vec3, list, or tuple",
230 actual_value=value,
231 suggestion="Use vec3(x, y, z), [x, y, z], or (x, y, z) format."
232 )
233
234
235def coerce_to_vec2(value: Any, param_name: str = "parameter") -> 'vec2':
236 """
237 Safely coerce list/tuple to vec2 with validation.
238
239 Args:
240 value: Value to coerce (vec2, list, or tuple)
241 param_name: Parameter name for error messages
242
243 Returns:
244 vec2 object
245
246 Raises:
247 ValidationError: If coercion fails or values are invalid
248 """
249 from ..wrappers.DataTypes import vec2
250
251 # Check if it's already a vec2 (using duck typing to avoid import issues)
252 if hasattr(value, 'x') and hasattr(value, 'y') and hasattr(value, 'to_list') and not hasattr(value, 'z'):
253 return value
254
255 if isinstance(value, (list, tuple)):
256 if len(value) != 2:
257 raise create_validation_error(
258 f"Parameter must have exactly 2 elements for vec2 conversion, got {len(value)} elements: {value}",
259 param_name=param_name,
260 expected_type="2-element list or tuple",
261 actual_value=value,
262 suggestion="Provide exactly 2 numeric values like [x, y] or (x, y)."
263 )
264
265 # Validate each component is finite
266 for i, component in enumerate(value):
267 if not is_finite_numeric(component):
268 raise create_validation_error(
269 f"Parameter element [{i}] must be a finite number, got {component} ({type(component).__name__})",
270 param_name=f"{param_name}[{i}]",
271 expected_type="finite number",
272 actual_value=component,
273 suggestion="Ensure all coordinate values are finite numbers (not NaN or infinity)."
274 )
275
276 return vec2(float(value[0]), float(value[1]))
277
278 raise create_validation_error(
279 f"Parameter must be a vec2, list, or tuple, got {type(value).__name__}",
280 param_name=param_name,
281 expected_type="vec2, list, or tuple",
282 actual_value=value,
283 suggestion="Use vec2(x, y), [x, y], or (x, y) format."
284 )
'vec3' coerce_to_vec3(Any value, str param_name="parameter")
Safely coerce list/tuple to vec3 with validation.
Definition core.py:196
validate_positive_value(Any value, str param_name="value", str function_name=None)
Validate value is positive and finite.
Definition core.py:130
'vec2' coerce_to_vec2(Any value, str param_name="parameter")
Safely coerce list/tuple to vec2 with validation.
Definition core.py:248
validate_non_negative_value(Any value, str param_name="value", str function_name=None)
Validate value is non-negative and finite.
Definition core.py:162
validate_input(Dict[str, Callable] param_validators=None, Dict[str, Callable] type_coercions=None)
Decorator for comprehensive parameter validation.
Definition core.py:30
bool is_finite_numeric(Any value)
Check if value is a finite number (not NaN or inf).
Definition core.py:111