0.1.19
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 _bind_args_to_params(func, args, kwargs):
17 """Bind positional and keyword arguments to parameter names.
18
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.
23 """
24 sig = inspect.signature(func)
25 try:
26 bound = sig.bind(*args, **kwargs)
27 except TypeError:
28 # If binding fails, let the actual function call produce the error
29 return {}, args, kwargs
30 # Don't apply defaults - we only want to validate explicitly provided args
31 return dict(bound.arguments), args, kwargs
32
33
34def validate_input(param_validators: Dict[str, Callable] = None,
35 type_coercions: Dict[str, Callable] = None):
36 """
37 Decorator for comprehensive parameter validation.
38
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
44
45 Args:
46 param_validators: Dict mapping parameter names to validation functions
47 type_coercions: Dict mapping parameter names to coercion functions
48 """
49 def decorator(func):
50 @functools.wraps(func)
51 def wrapper(*args, **kwargs):
52 # Bind positional args to parameter names so we can validate them
53 bound_params, _, _ = _bind_args_to_params(func, args, kwargs)
54
55 # Build a mutable copy of all named arguments
56 # We need to track which params came as positional vs keyword
57 # so we can pass coerced values back correctly
58 sig = inspect.signature(func)
59 param_names = list(sig.parameters.keys())
60
61 # Map positional args to their parameter names
62 positional_params = {}
63 for i, arg in enumerate(args):
64 if i < len(param_names):
65 positional_params[param_names[i]] = i
66
67 # Convert args to a mutable list for coercion
68 args_list = list(args)
69
70 # Perform type coercion first (on both positional and keyword args)
71 if type_coercions:
72 for param, coercion_func in type_coercions.items():
73 value = None
74 has_value = False
75
76 if param in kwargs:
77 value = kwargs[param]
78 has_value = True
79 elif param in positional_params:
80 value = args_list[positional_params[param]]
81 has_value = True
82
83 if has_value:
84 try:
85 coerced = coercion_func(value, param_name=param)
86 # Write back the coerced value
87 if param in kwargs:
88 kwargs[param] = coerced
89 elif param in positional_params:
90 args_list[positional_params[param]] = coerced
91 except ValidationError:
92 raise
93 except Exception as e:
94 raise create_validation_error(
95 f"Failed to coerce parameter to expected type: {str(e)}",
96 param_name=param,
97 function_name=func.__name__
98 )
99
100 # Then validate parameters (on both positional and keyword args)
101 if param_validators:
102 for param, validator in param_validators.items():
103 value = None
104 has_value = False
105
106 if param in kwargs:
107 value = kwargs[param]
108 has_value = True
109 elif param in positional_params:
110 value = args_list[positional_params[param]]
111 has_value = True
112
113 if has_value:
114 try:
115 validator(value, param_name=param, function_name=func.__name__)
116 except ValidationError:
117 raise
118 except Exception as e:
119 raise create_validation_error(
120 f"Parameter validation failed: {str(e)}",
121 param_name=param,
122 function_name=func.__name__
123 )
124
125 return func(*tuple(args_list), **kwargs)
126 return wrapper
127 return decorator
128
129
130def is_finite_numeric(value: Any) -> bool:
131 """Check if value is a finite number (not NaN or inf)."""
132 try:
133 float_value = float(value)
134 return math.isfinite(float_value)
135 except (ValueError, TypeError, OverflowError):
136 return False
137
138
139def validate_positive_value(value: Any, param_name: str = "value", function_name: str = None):
140 """
141 Validate value is positive and finite.
142
143 Args:
144 value: Value to validate
145 param_name: Parameter name for error messages
146 function_name: Function name for error messages
147
148 Raises:
149 ValidationError: If value is not positive or not finite
150 """
151 if not is_finite_numeric(value):
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",
157 actual_value=value
158 )
159
160 if value <= 0:
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",
166 actual_value=value,
167 suggestion="Use a value greater than 0."
168 )
169
170
171def validate_non_negative_value(value: Any, param_name: str = "value", function_name: str = None):
172 """
173 Validate value is non-negative and finite.
174
175 Args:
176 value: Value to validate
177 param_name: Parameter name for error messages
178 function_name: Function name for error messages
179
180 Raises:
181 ValidationError: If value is negative or not finite
182 """
183 if not is_finite_numeric(value):
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",
189 actual_value=value
190 )
191
192 if value < 0:
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",
198 actual_value=value,
199 suggestion="Use a value >= 0."
200 )
201
202
203def coerce_to_vec3(value: Any, param_name: str = "parameter") -> 'vec3':
204 """
205 Safely coerce list/tuple to vec3 with validation.
206
207 Args:
208 value: Value to coerce (vec3, list, or tuple)
209 param_name: Parameter name for error messages
210
211 Returns:
212 vec3 object
213
214 Raises:
215 ValidationError: If coercion fails or values are invalid
216 """
217 from ..wrappers.DataTypes import vec3
219 # Check if it's already a vec3 (using duck typing to avoid import issues)
220 if hasattr(value, 'x') and hasattr(value, 'y') and hasattr(value, 'z') and hasattr(value, 'to_list'):
221 return value
222
223 if isinstance(value, (list, tuple)):
224 if len(value) != 3:
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",
229 actual_value=value,
230 suggestion="Provide exactly 3 numeric values like [x, y, z] or (x, y, z)."
231 )
232
233 # Validate each component is finite
234 for i, component in enumerate(value):
235 if not is_finite_numeric(component):
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)."
242 )
243
244 return vec3(float(value[0]), float(value[1]), float(value[2]))
245
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",
250 actual_value=value,
251 suggestion="Use vec3(x, y, z), [x, y, z], or (x, y, z) format."
252 )
253
254
255def coerce_to_vec2(value: Any, param_name: str = "parameter") -> 'vec2':
256 """
257 Safely coerce list/tuple to vec2 with validation.
258
259 Args:
260 value: Value to coerce (vec2, list, or tuple)
261 param_name: Parameter name for error messages
262
263 Returns:
264 vec2 object
265
266 Raises:
267 ValidationError: If coercion fails or values are invalid
268 """
269 from ..wrappers.DataTypes import vec2
271 # Check if it's already a vec2 (using duck typing to avoid import issues)
272 if hasattr(value, 'x') and hasattr(value, 'y') and hasattr(value, 'to_list') and not hasattr(value, 'z'):
273 return value
274
275 if isinstance(value, (list, tuple)):
276 if len(value) != 2:
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",
281 actual_value=value,
282 suggestion="Provide exactly 2 numeric values like [x, y] or (x, y)."
283 )
284
285 # Validate each component is finite
286 for i, component in enumerate(value):
287 if not is_finite_numeric(component):
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)."
294 )
295
296 return vec2(float(value[0]), float(value[1]))
297
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",
302 actual_value=value,
303 suggestion="Use vec2(x, y), [x, y], or (x, y) format."
304 )
'vec3' coerce_to_vec3(Any value, str param_name="parameter")
Safely coerce list/tuple to vec3 with validation.
Definition core.py:218
validate_positive_value(Any value, str param_name="value", str function_name=None)
Validate value is positive and finite.
Definition core.py:152
'vec2' coerce_to_vec2(Any value, str param_name="parameter")
Safely coerce list/tuple to vec2 with validation.
Definition core.py:270
validate_non_negative_value(Any value, str param_name="value", str function_name=None)
Validate value is non-negative and finite.
Definition core.py:184
validate_input(Dict[str, Callable] param_validators=None, Dict[str, Callable] type_coercions=None)
Decorator for comprehensive parameter validation.
Definition core.py:50
bool is_finite_numeric(Any value)
Check if value is a finite number (not NaN or inf).
Definition core.py:133
_bind_args_to_params(func, args, kwargs)
Bind positional and keyword arguments to parameter names.
Definition core.py:25