Coverage for openhcs/ui/shared/parameter_type_utils.py: 26.2%
92 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
1"""
2Parameter type utilities for parameter form managers.
4This module provides centralized type checking and resolution methods to eliminate
5code duplication between PyQt and Textual parameter form implementations.
6"""
8import dataclasses
9from typing import Dict, Optional, Type, Union, get_origin, get_args
10from enum import Enum
12from openhcs.ui.shared.parameter_form_constants import CONSTANTS
15class ParameterTypeUtils:
16 """
17 Utility class for parameter type checking and resolution.
19 This class provides static methods for common type operations used throughout
20 the parameter form system, including Optional type handling, dataclass detection,
21 and Union type resolution.
22 """
24 @staticmethod
25 def is_optional(param_type: Type) -> bool:
26 """
27 Check if parameter type is Optional[T] (Union[T, None]).
29 This method determines whether a type annotation represents an optional
30 parameter of any type.
32 Args:
33 param_type: The type to check
35 Returns:
36 True if the type is Optional[T], False otherwise
38 Example:
39 >>> from typing import Optional
40 >>> ParameterTypeUtils.is_optional(Optional[str])
41 True
42 >>> ParameterTypeUtils.is_optional(str)
43 False
44 """
45 if get_origin(param_type) is Union:
46 args = get_args(param_type)
47 # Check if it's Optional (Union with None)
48 return len(args) == 2 and type(None) in args
49 return False
51 @staticmethod
52 def is_optional_dataclass(param_type: Type) -> bool:
53 """
54 Check if parameter type is Optional[dataclass].
56 This method determines whether a type annotation represents an optional
57 dataclass parameter (Union[DataclassType, None]).
59 Args:
60 param_type: The type to check
62 Returns:
63 True if the type is Optional[dataclass], False otherwise
65 Example:
66 >>> from typing import Optional
67 >>> @dataclass
68 ... class Config: pass
69 >>> ParameterTypeUtils.is_optional_dataclass(Optional[Config])
70 True
71 >>> ParameterTypeUtils.is_optional_dataclass(Config)
72 False
73 """
74 if get_origin(param_type) is Union:
75 args = get_args(param_type)
76 # Check if it's Optional (Union with None)
77 if len(args) == 2 and type(None) in args:
78 non_none_type = next(arg for arg in args if arg is not type(None))
79 return dataclasses.is_dataclass(non_none_type)
80 return False
82 @staticmethod
83 def get_optional_inner_type(param_type: Type) -> Type:
84 """
85 Extract the inner type from Optional[T].
87 This method extracts the non-None type from an Optional type annotation.
89 Args:
90 param_type: The Optional type to extract from
92 Returns:
93 The inner type (T from Optional[T])
95 Raises:
96 ValueError: If the type is not Optional
98 Example:
99 >>> from typing import Optional
100 >>> ParameterTypeUtils.get_optional_inner_type(Optional[str])
101 <class 'str'>
102 """
103 if get_origin(param_type) is Union:
104 args = get_args(param_type)
105 if len(args) == 2 and type(None) in args:
106 return next(arg for arg in args if arg is not type(None))
108 raise ValueError(f"Type {param_type} is not Optional")
110 @staticmethod
111 def get_dataclass_type_for_param(param_name: str, parameter_types: Dict[str, Type]) -> Optional[Type]:
112 """
113 Get the dataclass type for a parameter, handling Optional types.
115 This method retrieves the dataclass type for a parameter, automatically
116 unwrapping Optional types to get the underlying dataclass.
118 Args:
119 param_name: The parameter name to look up
120 parameter_types: Dictionary mapping parameter names to types
122 Returns:
123 The dataclass type, or None if parameter not found or not a dataclass
125 Example:
126 >>> types = {"config": Optional[MyConfig]}
127 >>> ParameterTypeUtils.get_dataclass_type_for_param("config", types)
128 <class 'MyConfig'>
129 """
130 if param_name not in parameter_types:
131 return None
133 param_type = parameter_types[param_name]
135 # Handle Optional[dataclass] types
136 if ParameterTypeUtils.is_optional_dataclass(param_type):
137 return ParameterTypeUtils.get_optional_inner_type(param_type)
139 # Handle direct dataclass types
140 if dataclasses.is_dataclass(param_type):
141 return param_type
143 return None
145 @staticmethod
146 def resolve_union_type(param_type: Type) -> Type:
147 """
148 Resolve Union types to their primary type.
150 This method handles Union types by extracting the primary (non-None) type.
151 For Optional types, it returns the inner type. For other Union types,
152 it returns the first non-None type.
154 Args:
155 param_type: The Union type to resolve
157 Returns:
158 The resolved primary type
160 Example:
161 >>> from typing import Union, Optional
162 >>> ParameterTypeUtils.resolve_union_type(Optional[str])
163 <class 'str'>
164 >>> ParameterTypeUtils.resolve_union_type(Union[int, str])
165 <class 'int'>
166 """
167 if get_origin(param_type) is Union:
168 args = get_args(param_type)
169 # Filter out None type and return the first remaining type
170 non_none_types = [arg for arg in args if arg is not type(None)]
171 if non_none_types:
172 return non_none_types[0]
174 return param_type
176 @staticmethod
177 def is_enum_type(param_type: Type) -> bool:
178 """
179 Check if a type is an Enum type.
181 Args:
182 param_type: The type to check
184 Returns:
185 True if the type is an Enum, False otherwise
186 """
187 return (hasattr(param_type, CONSTANTS.BASES_ATTR) and
188 Enum in getattr(param_type, CONSTANTS.BASES_ATTR))
190 @staticmethod
191 def is_list_of_enums(param_type: Type) -> bool:
192 """
193 Check if parameter type is List[Enum].
195 Args:
196 param_type: The type to check
198 Returns:
199 True if the type is List[Enum], False otherwise
200 """
201 try:
202 # Check if it's a generic type (like List[Something])
203 if hasattr(param_type, '__origin__') and hasattr(param_type, '__args__'):
204 origin = getattr(param_type, '__origin__')
205 if origin is list:
206 args = getattr(param_type, '__args__')
207 if args:
208 inner_type = args[0]
209 return ParameterTypeUtils.is_enum_type(inner_type)
210 return False
211 except Exception:
212 return False
214 @staticmethod
215 def get_enum_from_list_type(param_type: Type) -> Optional[Type]:
216 """
217 Extract enum type from List[Enum] type.
219 Args:
220 param_type: The List[Enum] type
222 Returns:
223 The Enum type, or None if not a List[Enum]
224 """
225 try:
226 if hasattr(param_type, '__origin__') and hasattr(param_type, '__args__'):
227 origin = getattr(param_type, '__origin__')
228 if origin is list:
229 args = getattr(param_type, '__args__')
230 if args and ParameterTypeUtils.is_enum_type(args[0]):
231 return args[0]
232 return None
233 except Exception:
234 return None
236 @staticmethod
237 def has_dataclass_fields(obj: any) -> bool:
238 """
239 Check if an object has dataclass fields.
241 Args:
242 obj: The object to check
244 Returns:
245 True if the object has __dataclass_fields__ attribute
246 """
247 return hasattr(obj, CONSTANTS.DATACLASS_FIELDS_ATTR)
249 @staticmethod
250 def has_resolve_field_value(obj: any) -> bool:
251 """
252 Check if an object has the _resolve_field_value method (lazy dataclass).
254 Args:
255 obj: The object to check
257 Returns:
258 True if the object has _resolve_field_value attribute
259 """
260 return hasattr(obj, CONSTANTS.RESOLVE_FIELD_VALUE_ATTR)
262 @staticmethod
263 def is_concrete_dataclass(obj: any) -> bool:
264 """
265 Check if an object is a concrete (non-lazy) dataclass.
267 Args:
268 obj: The object to check
270 Returns:
271 True if the object is a concrete dataclass
272 """
273 return (ParameterTypeUtils.has_dataclass_fields(obj) and
274 not ParameterTypeUtils.has_resolve_field_value(obj))
276 @staticmethod
277 def is_lazy_dataclass(obj: any) -> bool:
278 """
279 Check if an object is a lazy dataclass.
281 Args:
282 obj: The object to check
284 Returns:
285 True if the object is a lazy dataclass
286 """
287 return ParameterTypeUtils.has_resolve_field_value(obj)
289 @staticmethod
290 def extract_value_attribute(obj: any) -> any:
291 """
292 Extract the value attribute from an object if it exists.
294 This is commonly used for enum values and other wrapped types.
296 Args:
297 obj: The object to extract value from
299 Returns:
300 The value attribute if it exists, otherwise the original object
301 """
302 if hasattr(obj, CONSTANTS.VALUE_ATTR):
303 return getattr(obj, CONSTANTS.VALUE_ATTR)
304 return obj
306 @staticmethod
307 def convert_string_to_bool(value: str) -> bool:
308 """
309 Convert string to boolean using standard true/false patterns.
311 Args:
312 value: The string value to convert
314 Returns:
315 True if the string represents a true value, False otherwise
316 """
317 return value.lower() in CONSTANTS.TRUE_STRINGS