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

1""" 

2Parameter type utilities for parameter form managers. 

3 

4This module provides centralized type checking and resolution methods to eliminate 

5code duplication between PyQt and Textual parameter form implementations. 

6""" 

7 

8import dataclasses 

9from typing import Dict, Optional, Type, Union, get_origin, get_args 

10from enum import Enum 

11 

12from openhcs.ui.shared.parameter_form_constants import CONSTANTS 

13 

14 

15class ParameterTypeUtils: 

16 """ 

17 Utility class for parameter type checking and resolution. 

18  

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 """ 

23 

24 @staticmethod 

25 def is_optional(param_type: Type) -> bool: 

26 """ 

27 Check if parameter type is Optional[T] (Union[T, None]). 

28 

29 This method determines whether a type annotation represents an optional 

30 parameter of any type. 

31 

32 Args: 

33 param_type: The type to check 

34 

35 Returns: 

36 True if the type is Optional[T], False otherwise 

37 

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 

50 

51 @staticmethod 

52 def is_optional_dataclass(param_type: Type) -> bool: 

53 """ 

54 Check if parameter type is Optional[dataclass]. 

55  

56 This method determines whether a type annotation represents an optional 

57 dataclass parameter (Union[DataclassType, None]). 

58  

59 Args: 

60 param_type: The type to check 

61  

62 Returns: 

63 True if the type is Optional[dataclass], False otherwise 

64  

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 

81 

82 @staticmethod 

83 def get_optional_inner_type(param_type: Type) -> Type: 

84 """ 

85 Extract the inner type from Optional[T]. 

86  

87 This method extracts the non-None type from an Optional type annotation. 

88  

89 Args: 

90 param_type: The Optional type to extract from 

91  

92 Returns: 

93 The inner type (T from Optional[T]) 

94  

95 Raises: 

96 ValueError: If the type is not Optional 

97  

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)) 

107 

108 raise ValueError(f"Type {param_type} is not Optional") 

109 

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. 

114  

115 This method retrieves the dataclass type for a parameter, automatically 

116 unwrapping Optional types to get the underlying dataclass. 

117  

118 Args: 

119 param_name: The parameter name to look up 

120 parameter_types: Dictionary mapping parameter names to types 

121  

122 Returns: 

123 The dataclass type, or None if parameter not found or not a dataclass 

124  

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 

132 

133 param_type = parameter_types[param_name] 

134 

135 # Handle Optional[dataclass] types 

136 if ParameterTypeUtils.is_optional_dataclass(param_type): 

137 return ParameterTypeUtils.get_optional_inner_type(param_type) 

138 

139 # Handle direct dataclass types 

140 if dataclasses.is_dataclass(param_type): 

141 return param_type 

142 

143 return None 

144 

145 @staticmethod 

146 def resolve_union_type(param_type: Type) -> Type: 

147 """ 

148 Resolve Union types to their primary type. 

149  

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. 

153  

154 Args: 

155 param_type: The Union type to resolve 

156  

157 Returns: 

158 The resolved primary type 

159  

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] 

173 

174 return param_type 

175 

176 @staticmethod 

177 def is_enum_type(param_type: Type) -> bool: 

178 """ 

179 Check if a type is an Enum type. 

180  

181 Args: 

182 param_type: The type to check 

183  

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)) 

189 

190 @staticmethod 

191 def is_list_of_enums(param_type: Type) -> bool: 

192 """ 

193 Check if parameter type is List[Enum]. 

194  

195 Args: 

196 param_type: The type to check 

197  

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 

213 

214 @staticmethod 

215 def get_enum_from_list_type(param_type: Type) -> Optional[Type]: 

216 """ 

217 Extract enum type from List[Enum] type. 

218  

219 Args: 

220 param_type: The List[Enum] type 

221  

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 

235 

236 @staticmethod 

237 def has_dataclass_fields(obj: any) -> bool: 

238 """ 

239 Check if an object has dataclass fields. 

240  

241 Args: 

242 obj: The object to check 

243  

244 Returns: 

245 True if the object has __dataclass_fields__ attribute 

246 """ 

247 return hasattr(obj, CONSTANTS.DATACLASS_FIELDS_ATTR) 

248 

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). 

253  

254 Args: 

255 obj: The object to check 

256  

257 Returns: 

258 True if the object has _resolve_field_value attribute 

259 """ 

260 return hasattr(obj, CONSTANTS.RESOLVE_FIELD_VALUE_ATTR) 

261 

262 @staticmethod 

263 def is_concrete_dataclass(obj: any) -> bool: 

264 """ 

265 Check if an object is a concrete (non-lazy) dataclass. 

266  

267 Args: 

268 obj: The object to check 

269  

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)) 

275 

276 @staticmethod 

277 def is_lazy_dataclass(obj: any) -> bool: 

278 """ 

279 Check if an object is a lazy dataclass. 

280  

281 Args: 

282 obj: The object to check 

283  

284 Returns: 

285 True if the object is a lazy dataclass 

286 """ 

287 return ParameterTypeUtils.has_resolve_field_value(obj) 

288 

289 @staticmethod 

290 def extract_value_attribute(obj: any) -> any: 

291 """ 

292 Extract the value attribute from an object if it exists. 

293  

294 This is commonly used for enum values and other wrapped types. 

295  

296 Args: 

297 obj: The object to extract value from 

298  

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 

305 

306 @staticmethod 

307 def convert_string_to_bool(value: str) -> bool: 

308 """ 

309 Convert string to boolean using standard true/false patterns. 

310  

311 Args: 

312 value: The string value to convert 

313  

314 Returns: 

315 True if the string represents a true value, False otherwise 

316 """ 

317 return value.lower() in CONSTANTS.TRUE_STRINGS