Coverage for openhcs/textual_tui/widgets/shared/parameter_form_manager.py: 0.0%

126 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 18:33 +0000

1""" 

2Dramatically simplified Textual parameter form manager. 

3 

4This demonstrates how the widget implementation can be drastically simplified 

5by leveraging the comprehensive shared infrastructure we've built. 

6""" 

7 

8from typing import Any, Dict, Type, Optional 

9from textual.containers import Vertical, Horizontal 

10from textual.widgets import Static, Button, Collapsible 

11from textual.app import ComposeResult 

12 

13# Import our comprehensive shared infrastructure 

14from openhcs.ui.shared.parameter_form_base import ParameterFormManagerBase, ParameterFormConfig 

15from openhcs.ui.shared.parameter_form_service import ParameterFormService 

16from openhcs.ui.shared.parameter_form_config_factory import textual_config 

17from openhcs.ui.shared.parameter_form_constants import CONSTANTS 

18# Old field path detection removed - using simple field name matching 

19 

20# Import Textual-specific components 

21from .typed_widget_factory import TypedWidgetFactory 

22from .clickable_help_label import ClickableParameterLabel 

23from ..different_values_input import DifferentValuesInput 

24 

25 

26class ParameterFormManager(ParameterFormManagerBase): 

27 """ 

28 Mathematical: (parameters, types, field_id) → parameter form 

29 

30 Dramatically simplified implementation using shared infrastructure while maintaining 

31 exact backward compatibility with the original API. 

32 

33 Key improvements: 

34 - Internal implementation reduced by ~80% 

35 - Parameter analysis delegated to service layer 

36 - Widget creation patterns centralized 

37 - All magic strings eliminated 

38 - Type checking delegated to utilities 

39 - Debug logging handled by base class 

40 """ 

41 

42 def __init__(self, parameters: Dict[str, Any], parameter_types: Dict[str, Type], 

43 field_id: str, parameter_info: Dict = None, is_global_config_editing: bool = False, 

44 global_config_type: Optional[Type] = None, placeholder_prefix: str = None): 

45 """ 

46 Initialize Textual parameter form manager with backward-compatible API. 

47 

48 Args: 

49 parameters: Dictionary of parameter names to current values 

50 parameter_types: Dictionary of parameter names to types 

51 field_id: Unique identifier for the form 

52 parameter_info: Optional parameter information dictionary 

53 is_global_config_editing: Whether editing global configuration 

54 global_config_type: Type of global configuration being edited 

55 placeholder_prefix: Prefix for placeholder text 

56 """ 

57 # Convert old API to new config object internally 

58 if placeholder_prefix is None: 

59 placeholder_prefix = CONSTANTS.DEFAULT_PLACEHOLDER_PREFIX 

60 

61 config = textual_config( 

62 field_id=field_id, 

63 parameter_info=parameter_info 

64 ) 

65 config.is_global_config_editing = is_global_config_editing 

66 config.global_config_type = global_config_type 

67 config.placeholder_prefix = placeholder_prefix 

68 

69 # Initialize base class with shared infrastructure 

70 super().__init__(parameters, parameter_types, config) 

71 

72 # Store public API attributes for backward compatibility 

73 self.field_id = field_id 

74 self.parameter_info = parameter_info or {} 

75 self.is_global_config_editing = is_global_config_editing 

76 self.global_config_type = global_config_type 

77 self.placeholder_prefix = placeholder_prefix 

78 

79 # Initialize service layer for business logic 

80 self.service = ParameterFormService(self.debugger.config) 

81 

82 # Analyze form structure once using service layer 

83 self.form_structure = self.service.analyze_parameters( 

84 parameters, parameter_types, config.field_id, config.parameter_info 

85 ) 

86 

87 

88 

89 

90 

91 

92 # Initialize tracking attributes for backward compatibility 

93 self.nested_managers = {} 

94 self.optional_checkboxes = {} 

95 

96 def build_form(self) -> ComposeResult: 

97 """ 

98 Build the complete form UI. 

99  

100 Dramatically simplified by delegating analysis to service layer 

101 and using centralized widget creation patterns. 

102 """ 

103 with Vertical() as form: 

104 form.styles.height = CONSTANTS.AUTO_SIZE 

105 

106 # Iterate through analyzed parameter structure 

107 for param_info in self.form_structure.parameters: 

108 if param_info.is_optional and param_info.is_nested: 

109 yield from self._create_optional_dataclass_widget(param_info) 

110 elif param_info.is_optional: 

111 yield from self._create_optional_regular_widget(param_info) 

112 elif param_info.is_nested: 

113 yield from self._create_nested_dataclass_widget(param_info) 

114 else: 

115 yield from self._create_regular_parameter_widget(param_info) 

116 

117 def _create_regular_parameter_widget(self, param_info) -> ComposeResult: 

118 """Create widget for regular (non-dataclass) parameter.""" 

119 # Get display information from service 

120 display_info = self.service.get_parameter_display_info( 

121 param_info.name, param_info.type, param_info.description 

122 ) 

123 

124 # Direct field ID generation - no artificial complexity 

125 field_ids = self.service.generate_field_ids_direct(self.config.field_id, param_info.name) 

126 

127 # Create 3-column layout: label + input + reset 

128 with Horizontal() as row: 

129 row.styles.height = CONSTANTS.AUTO_SIZE 

130 

131 # Parameter label with help - use description from parameter analysis 

132 label = ClickableParameterLabel( 

133 param_info.name, 

134 display_info['description'], 

135 param_info.type, 

136 classes=CONSTANTS.PARAM_LABEL_CLASS 

137 ) 

138 label.styles.width = CONSTANTS.AUTO_SIZE 

139 label.styles.text_align = CONSTANTS.LEFT_ALIGN 

140 label.styles.height = "1" 

141 yield label 

142 

143 # Input widget 

144 input_widget = self.create_parameter_widget( 

145 param_info.name, param_info.type, param_info.current_value 

146 ) 

147 input_widget.styles.width = CONSTANTS.FLEXIBLE_WIDTH 

148 input_widget.styles.text_align = CONSTANTS.LEFT_ALIGN 

149 input_widget.styles.margin = CONSTANTS.LEFT_MARGIN_ONLY 

150 yield input_widget 

151 

152 # Reset button 

153 reset_btn = Button( 

154 CONSTANTS.RESET_BUTTON_TEXT, 

155 id=field_ids['reset_button_id'], 

156 compact=CONSTANTS.COMPACT_WIDGET 

157 ) 

158 reset_btn.styles.width = CONSTANTS.AUTO_SIZE 

159 yield reset_btn 

160 

161 def _create_nested_dataclass_widget(self, param_info) -> ComposeResult: 

162 """Create widget for nested dataclass parameter.""" 

163 # Get nested form structure from pre-analyzed structure 

164 nested_structure = self.form_structure.nested_forms[param_info.name] 

165 

166 # Create collapsible container 

167 collapsible = TypedWidgetFactory.create_widget( 

168 param_info.type, param_info.current_value, None 

169 ) 

170 

171 # Create nested form manager using simplified constructor 

172 nested_config = textual_config( 

173 field_id=nested_structure.field_id, 

174 parameter_info=self.config.parameter_info 

175 ).with_debug( 

176 self.config.enable_debug, 

177 self.config.debug_target_params 

178 ) 

179 

180 nested_manager = ParameterFormManager( 

181 {p.name: p.current_value for p in nested_structure.parameters}, 

182 {p.name: p.type for p in nested_structure.parameters}, 

183 nested_structure.field_id, 

184 self.parameter_info, 

185 self.is_global_config_editing, 

186 self.global_config_type, 

187 self.placeholder_prefix 

188 ) 

189 

190 # Store reference for updates 

191 self.nested_managers[param_info.name] = nested_manager 

192 

193 # Build nested form 

194 with collapsible: 

195 yield from nested_manager.build_form() 

196 

197 yield collapsible 

198 

199 def _create_optional_dataclass_widget(self, param_info) -> ComposeResult: 

200 """Create widget for Optional[dataclass] parameter with checkbox.""" 

201 # Get display information 

202 display_info = self.service.get_parameter_display_info( 

203 param_info.name, param_info.type, param_info.description 

204 ) 

205 

206 # Direct field ID generation - no artificial complexity 

207 field_ids = self.service.generate_field_ids_direct(self.config.field_id, param_info.name) 

208 

209 # Create checkbox 

210 from textual.widgets import Checkbox 

211 checkbox = Checkbox( 

212 value=param_info.current_value is not None, 

213 label=display_info['checkbox_label'], 

214 id=field_ids['optional_checkbox_id'], 

215 compact=CONSTANTS.COMPACT_WIDGET 

216 ) 

217 yield checkbox 

218 

219 # Always create nested form, but disable if None 

220 # Note: In Textual, we'll need to handle the enable/disable logic in the event handler 

221 yield from self._create_nested_dataclass_widget(param_info) 

222 

223 def _create_optional_regular_widget(self, param_info) -> ComposeResult: 

224 """Create widget for Optional[regular_type] parameter with checkbox.""" 

225 # Get display information 

226 display_info = self.service.get_parameter_display_info( 

227 param_info.name, param_info.type, param_info.description 

228 ) 

229 

230 # Direct field ID generation 

231 field_ids = self.service.generate_field_ids_direct(self.config.field_id, param_info.name) 

232 

233 # Create checkbox 

234 from textual.widgets import Checkbox 

235 checkbox = Checkbox( 

236 value=param_info.current_value is not None, 

237 label=display_info['checkbox_label'], 

238 id=field_ids['optional_checkbox_id'], 

239 compact=CONSTANTS.COMPACT_WIDGET 

240 ) 

241 yield checkbox 

242 

243 # Get inner type and create widget for it 

244 from openhcs.ui.shared.parameter_type_utils import ParameterTypeUtils 

245 inner_type = ParameterTypeUtils.get_optional_inner_type(param_info.type) 

246 

247 # Create the actual widget for the inner type 

248 inner_widget = TypedWidgetFactory.create_widget(inner_type, param_info.current_value, field_ids['widget_id']) 

249 inner_widget.disabled = param_info.current_value is None # Disable if None 

250 yield inner_widget 

251 

252 # Abstract method implementations (dramatically simplified) 

253 

254 def create_parameter_widget(self, param_name: str, param_type: Type, current_value: Any) -> Any: 

255 """Create a widget for a single parameter using existing factory.""" 

256 # Direct field ID generation - no artificial complexity 

257 field_ids = self.service.generate_field_ids_direct(self.config.field_id, param_name) 

258 return TypedWidgetFactory.create_widget(param_type, current_value, field_ids['widget_id']) 

259 

260 def create_nested_form(self, param_name: str, param_type: Type, current_value: Any) -> Any: 

261 """Create a nested form using actual field path instead of artificial field IDs""" 

262 # Get parent dataclass type for context 

263 parent_dataclass_type = getattr(self.config, 'dataclass_type', None) if hasattr(self.config, 'dataclass_type') else None 

264 

265 # Get actual field path from FieldPathDetector (no artificial "nested_" prefix) 

266 field_path = self.service.get_field_path_with_fail_loud(parent_dataclass_type or type(None), param_type) 

267 

268 # Extract nested parameters using service with parent context 

269 nested_params, nested_types = self.service.extract_nested_parameters( 

270 current_value, param_type, parent_dataclass_type 

271 ) 

272 

273 # Create nested config with actual field path 

274 nested_config = textual_config(field_path) 

275 

276 # Return nested manager with backward-compatible API 

277 return ParameterFormManager( 

278 nested_params, 

279 nested_types, 

280 field_path, # Use actual dataclass field name directly 

281 None, # parameter_info 

282 False, # is_global_config_editing 

283 None, # global_config_type 

284 CONSTANTS.DEFAULT_PLACEHOLDER_PREFIX 

285 ) 

286 

287 def update_widget_value(self, widget: Any, value: Any) -> None: 

288 """Update a widget's value using framework-specific methods.""" 

289 if hasattr(widget, CONSTANTS.SET_VALUE_METHOD): 

290 getattr(widget, CONSTANTS.SET_VALUE_METHOD)(value) 

291 elif hasattr(widget, CONSTANTS.SET_TEXT_METHOD): 

292 getattr(widget, CONSTANTS.SET_TEXT_METHOD)(str(value)) 

293 

294 def get_widget_value(self, widget: Any) -> Any: 

295 """Get a widget's current value using framework-specific methods.""" 

296 if hasattr(widget, CONSTANTS.GET_VALUE_METHOD): 

297 return getattr(widget, CONSTANTS.GET_VALUE_METHOD)() 

298 elif hasattr(widget, 'text'): 

299 return widget.text 

300 return None 

301 

302 # Framework-specific methods for backward compatibility 

303 

304 def handle_optional_checkbox_change(self, param_name: str, enabled: bool) -> None: 

305 """ 

306 Handle checkbox change for Optional[dataclass] parameters. 

307 

308 Args: 

309 param_name: The parameter name 

310 enabled: Whether the checkbox is enabled 

311 """ 

312 self.debugger.log_form_manager_operation("optional_checkbox_change", { 

313 "param_name": param_name, 

314 "enabled": enabled 

315 }) 

316 

317 if enabled: 

318 # Create default instance of the dataclass 

319 param_type = self.parameter_types.get(param_name) 

320 if param_type and ParameterTypeUtils.is_optional_dataclass(param_type): 

321 inner_type = ParameterTypeUtils.get_optional_inner_type(param_type) 

322 default_instance = inner_type() # Create with defaults 

323 self.update_parameter(param_name, default_instance) 

324 else: 

325 # Set to None 

326 self.update_parameter(param_name, None) 

327 

328 def reset_parameter_by_path(self, parameter_path: str) -> None: 

329 """ 

330 Reset a parameter by its full path (supports nested parameters). 

331 

332 Args: 

333 parameter_path: Full path to parameter (e.g., "config.nested.param") 

334 """ 

335 self.debugger.log_form_manager_operation("reset_parameter_by_path", { 

336 "parameter_path": parameter_path 

337 }) 

338 

339 # Handle nested parameter paths 

340 if CONSTANTS.DOT_SEPARATOR in parameter_path: 

341 parts = parameter_path.split(CONSTANTS.DOT_SEPARATOR) 

342 param_name = CONSTANTS.FIELD_ID_SEPARATOR.join(parts) 

343 else: 

344 param_name = parameter_path 

345 

346 # Delegate to standard reset logic 

347 self.reset_parameter(param_name) 

348 

349 @staticmethod 

350 def convert_string_to_type(string_value: str, param_type: type, strict: bool = False) -> Any: 

351 """ 

352 Convert string value to appropriate type. 

353 

354 This is a backward compatibility method that delegates to the shared utilities. 

355 

356 Args: 

357 string_value: String value to convert 

358 param_type: Target parameter type 

359 strict: Whether to use strict conversion 

360 

361 Returns: 

362 Converted value 

363 """ 

364 # Delegate to shared service layer 

365 from openhcs.ui.shared.parameter_form_service import ParameterFormService 

366 service = ParameterFormService() 

367 return service.convert_value_to_type(string_value, param_type, "convert_string_to_type") 

368 

369 

370