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

88 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 18:33 +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 

10import asyncio 

11from typing import Optional 

12from pathlib import Path 

13 

14from PyQt6.QtWidgets import QApplication, QMessageBox 

15from PyQt6.QtCore import QTimer 

16from PyQt6.QtGui import QIcon 

17 

18from openhcs.core.config import GlobalPipelineConfig 

19from openhcs.core.orchestrator.gpu_scheduler import setup_global_gpu_registry 

20from openhcs.io.base import storage_registry 

21from openhcs.io.filemanager import FileManager 

22 

23from openhcs.pyqt_gui.main import OpenHCSMainWindow 

24 

25logger = logging.getLogger(__name__) 

26 

27 

28class OpenHCSPyQtApp(QApplication): 

29 """ 

30 OpenHCS PyQt6 Application. 

31  

32 Main application class that manages global state, configuration, 

33 and the main window lifecycle. 

34 """ 

35 

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

37 """ 

38 Initialize the OpenHCS PyQt6 application. 

39  

40 Args: 

41 argv: Command line arguments 

42 global_config: Global configuration (uses default if None) 

43 """ 

44 super().__init__(argv) 

45 

46 # Application metadata 

47 self.setApplicationName("OpenHCS") 

48 self.setApplicationVersion("1.0.0") 

49 self.setOrganizationName("OpenHCS Development Team") 

50 self.setOrganizationDomain("openhcs.org") 

51 

52 # Global configuration 

53 self.global_config = global_config or GlobalPipelineConfig() 

54 

55 # Shared components 

56 self.storage_registry = storage_registry 

57 self.file_manager = FileManager(self.storage_registry) 

58 

59 # Main window 

60 self.main_window: Optional[OpenHCSMainWindow] = None 

61 

62 # Setup application 

63 self.setup_application() 

64 

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

66 

67 def setup_application(self): 

68 """Setup application-wide configuration.""" 

69 # Setup GPU registry 

70 setup_global_gpu_registry(global_config=self.global_config) 

71 logger.info("GPU registry setup completed") 

72 

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

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

75 from openhcs.config_framework.global_config import set_global_config_for_editing 

76 from openhcs.config_framework.lazy_factory import ensure_global_config_context 

77 from openhcs.core.config import GlobalPipelineConfig 

78 

79 # Set for editing (UI placeholders) 

80 set_global_config_for_editing(GlobalPipelineConfig, self.global_config) 

81 

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

83 ensure_global_config_context(GlobalPipelineConfig, self.global_config) 

84 

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

86 

87 # Set application icon (if available) 

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

89 if icon_path.exists(): 

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

91 

92 # Setup exception handling 

93 sys.excepthook = self.handle_exception 

94 

95 def create_main_window(self) -> OpenHCSMainWindow: 

96 """ 

97 Create and show the main window. 

98  

99 Returns: 

100 Created main window 

101 """ 

102 if self.main_window is None: 

103 self.main_window = OpenHCSMainWindow(self.global_config) 

104 

105 # Connect application-level signals 

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

107 

108 return self.main_window 

109 

110 def show_main_window(self): 

111 """Show the main window.""" 

112 if self.main_window is None: 

113 self.create_main_window() 

114 

115 self.main_window.show() 

116 self.main_window.raise_() 

117 self.main_window.activateWindow() 

118 

119 def on_config_changed(self, new_config: GlobalPipelineConfig): 

120 """ 

121 Handle global configuration changes. 

122  

123 Args: 

124 new_config: New global configuration 

125 """ 

126 self.global_config = new_config 

127 logger.info("Global configuration updated") 

128 

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

130 """ 

131 Handle uncaught exceptions. 

132  

133 Args: 

134 exc_type: Exception type 

135 exc_value: Exception value 

136 exc_traceback: Exception traceback 

137 """ 

138 if issubclass(exc_type, KeyboardInterrupt): 

139 # Handle Ctrl+C gracefully 

140 sys.__excepthook__(exc_type, exc_value, exc_traceback) 

141 return 

142 

143 # Log the exception 

144 logger.critical( 

145 "Uncaught exception", 

146 exc_info=(exc_type, exc_value, exc_traceback) 

147 ) 

148 

149 # Show error dialog 

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

151 

152 if self.main_window: 

153 QMessageBox.critical( 

154 self.main_window, 

155 "Unexpected Error", 

156 error_msg 

157 ) 

158 else: 

159 # No main window - application is in invalid state 

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

161 

162 def run(self) -> int: 

163 """ 

164 Run the application. 

165 

166 Returns: 

167 Application exit code 

168 """ 

169 try: 

170 # Show main window 

171 self.show_main_window() 

172 

173 # Start event loop 

174 exit_code = self.exec() 

175 

176 # Ensure clean shutdown 

177 self.cleanup() 

178 

179 return exit_code 

180 

181 except Exception as e: 

182 logger.error(f"Error during application run: {e}") 

183 self.cleanup() 

184 return 1 

185 

186 def cleanup(self): 

187 """Clean up application resources.""" 

188 try: 

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

190 

191 # Process any remaining events 

192 self.processEvents() 

193 

194 # Clean up main window 

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

196 # Force close if not already closed 

197 if not self.main_window.isHidden(): 

198 self.main_window.close() 

199 self.main_window.deleteLater() 

200 self.main_window = None 

201 

202 # Process events again to handle deleteLater 

203 self.processEvents() 

204 

205 # Force garbage collection 

206 import gc 

207 gc.collect() 

208 

209 logger.info("Application cleanup completed") 

210 

211 except Exception as e: 

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

213 

214 

215if __name__ == "__main__": 

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

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

218 sys.exit(1)