Coverage for openhcs/pyqt_gui/app.py: 0.0%

100 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-04 02:09 +0000

1""" 

2OpenHCS PyQt6 Application 

3 

4Main application class that initializes the PyQt6 application and 

5manages global configuration and services. 

6""" 

7 

8import sys 

9import logging 

10from typing import Optional 

11from pathlib import Path 

12 

13from PyQt6.QtWidgets import QApplication, QMessageBox 

14from PyQt6.QtGui import QIcon 

15 

16from openhcs.core.config import GlobalPipelineConfig 

17from openhcs.io.base import storage_registry 

18from openhcs.io.filemanager import FileManager 

19 

20from openhcs.pyqt_gui.main import OpenHCSMainWindow 

21 

22logger = logging.getLogger(__name__) 

23 

24 

25class OpenHCSPyQtApp(QApplication): 

26 """ 

27 OpenHCS PyQt6 Application. 

28  

29 Main application class that manages global state, configuration, 

30 and the main window lifecycle. 

31 """ 

32 

33 def __init__(self, argv: list, global_config: Optional[GlobalPipelineConfig] = None): 

34 """ 

35 Initialize the OpenHCS PyQt6 application. 

36  

37 Args: 

38 argv: Command line arguments 

39 global_config: Global configuration (uses default if None) 

40 """ 

41 super().__init__(argv) 

42 

43 # Application metadata 

44 self.setApplicationName("OpenHCS") 

45 self.setApplicationVersion("1.0.0") 

46 self.setOrganizationName("OpenHCS Development Team") 

47 self.setOrganizationDomain("openhcs.org") 

48 

49 # Global configuration 

50 self.global_config = global_config or GlobalPipelineConfig() 

51 

52 # Shared components 

53 self.storage_registry = storage_registry 

54 self.file_manager = FileManager(self.storage_registry) 

55 

56 # Main window 

57 self.main_window: Optional[OpenHCSMainWindow] = None 

58 

59 # Setup application 

60 self.setup_application() 

61 

62 logger.info("OpenHCS PyQt6 application initialized") 

63 

64 def setup_application(self): 

65 """Setup application-wide configuration.""" 

66 # Start async storage registry initialization in background thread 

67 import threading 

68 def init_storage_registry_background(): 

69 from openhcs.io.base import ensure_storage_registry 

70 ensure_storage_registry() 

71 logger.info("Storage registry initialized in background") 

72 

73 thread = threading.Thread(target=init_storage_registry_background, daemon=True, name="storage-registry-init") 

74 thread.start() 

75 logger.info("Storage registry initialization started in background") 

76 

77 # Start async function registry initialization in background thread 

78 # This creates virtual modules (openhcs.cucim, openhcs.pyclesperanto, etc.) 

79 def init_function_registry_background(): 

80 from openhcs.processing.func_registry import initialize_registry 

81 initialize_registry() 

82 logger.info("Function registry initialized in background - virtual modules created") 

83 

84 func_thread = threading.Thread(target=init_function_registry_background, daemon=True, name="function-registry-init") 

85 func_thread.start() 

86 logger.info("Function registry initialization started in background") 

87 

88 # CRITICAL FIX: Establish global config context for lazy dataclass resolution 

89 # This was missing and caused placeholder resolution to fall back to static defaults 

90 from openhcs.config_framework.global_config import set_global_config_for_editing 

91 from openhcs.config_framework.lazy_factory import ensure_global_config_context 

92 from openhcs.core.config import GlobalPipelineConfig 

93 

94 # Set for editing (UI placeholders) 

95 set_global_config_for_editing(GlobalPipelineConfig, self.global_config) 

96 

97 # ALSO ensure context for orchestrator creation (required by orchestrator.__init__) 

98 ensure_global_config_context(GlobalPipelineConfig, self.global_config) 

99 

100 logger.info("Global configuration context established for lazy dataclass resolution") 

101 

102 # Set application icon (if available) 

103 icon_path = Path(__file__).parent / "resources" / "openhcs_icon.png" 

104 if icon_path.exists(): 

105 self.setWindowIcon(QIcon(str(icon_path))) 

106 

107 # Setup exception handling 

108 sys.excepthook = self.handle_exception 

109 

110 def create_main_window(self) -> OpenHCSMainWindow: 

111 """ 

112 Create and show the main window. 

113  

114 Returns: 

115 Created main window 

116 """ 

117 if self.main_window is None: 

118 self.main_window = OpenHCSMainWindow(self.global_config) 

119 

120 # Connect application-level signals 

121 self.main_window.config_changed.connect(self.on_config_changed) 

122 

123 return self.main_window 

124 

125 def show_main_window(self): 

126 """Show the main window.""" 

127 if self.main_window is None: 

128 self.create_main_window() 

129 

130 self.main_window.show() 

131 self.main_window.raise_() 

132 self.main_window.activateWindow() 

133 

134 # Trigger deferred initialization AFTER window is visible 

135 # This includes log viewer and default windows (pipeline editor) 

136 from PyQt6.QtCore import QTimer 

137 QTimer.singleShot(100, self.main_window._deferred_initialization) 

138 

139 def on_config_changed(self, new_config: GlobalPipelineConfig): 

140 """ 

141 Handle global configuration changes. 

142  

143 Args: 

144 new_config: New global configuration 

145 """ 

146 self.global_config = new_config 

147 logger.info("Global configuration updated") 

148 

149 def handle_exception(self, exc_type, exc_value, exc_traceback): 

150 """ 

151 Handle uncaught exceptions. 

152  

153 Args: 

154 exc_type: Exception type 

155 exc_value: Exception value 

156 exc_traceback: Exception traceback 

157 """ 

158 if issubclass(exc_type, KeyboardInterrupt): 

159 # Handle Ctrl+C gracefully 

160 sys.__excepthook__(exc_type, exc_value, exc_traceback) 

161 return 

162 

163 # Log the exception 

164 logger.critical( 

165 "Uncaught exception", 

166 exc_info=(exc_type, exc_value, exc_traceback) 

167 ) 

168 

169 # Show error dialog 

170 error_msg = f"An unexpected error occurred:\n\n{exc_type.__name__}: {exc_value}" 

171 

172 if self.main_window: 

173 QMessageBox.critical( 

174 self.main_window, 

175 "Unexpected Error", 

176 error_msg 

177 ) 

178 else: 

179 # No main window - application is in invalid state 

180 raise RuntimeError("Uncaught exception occurred but no main window available for error dialog") 

181 

182 def run(self) -> int: 

183 """ 

184 Run the application. 

185 

186 Returns: 

187 Application exit code 

188 """ 

189 try: 

190 # Show main window 

191 self.show_main_window() 

192 

193 # Start event loop 

194 exit_code = self.exec() 

195 

196 # Ensure clean shutdown 

197 self.cleanup() 

198 

199 return exit_code 

200 

201 except Exception as e: 

202 logger.error(f"Error during application run: {e}", exc_info=True) 

203 self.cleanup() 

204 return 1 

205 

206 def cleanup(self): 

207 """Clean up application resources.""" 

208 try: 

209 logger.info("Starting application cleanup...") 

210 

211 # Process any remaining events 

212 self.processEvents() 

213 

214 # Clean up main window 

215 if hasattr(self, 'main_window') and self.main_window: 

216 # Force close if not already closed 

217 if not self.main_window.isHidden(): 

218 self.main_window.close() 

219 self.main_window.deleteLater() 

220 self.main_window = None 

221 

222 # Process events again to handle deleteLater 

223 self.processEvents() 

224 

225 # Force garbage collection 

226 import gc 

227 gc.collect() 

228 

229 logger.info("Application cleanup completed") 

230 

231 except Exception as e: 

232 logger.warning(f"Error during application cleanup: {e}") 

233 

234 

235if __name__ == "__main__": 

236 # Don't run directly - use launch.py instead 

237 print("Use 'python -m openhcs.pyqt_gui' or 'python -m openhcs.pyqt_gui.launch' to start the GUI") 

238 sys.exit(1)