Coverage for openhcs/pyqt_gui/services/async_service_bridge.py: 0.0%
104 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""
2Async Service Bridge
4Bridges async OpenHCS services to PyQt6 threading model,
5converting async/await patterns to Qt signals/slots.
6"""
8import logging
9from typing import Any, Callable, Optional
10import asyncio
12from PyQt6.QtCore import QObject, QThread, pyqtSignal
14logger = logging.getLogger(__name__)
17class AsyncServiceBridge(QObject):
18 """
19 Bridge for converting async service operations to Qt threading model.
21 Handles the conversion of async/await patterns used in OpenHCS services
22 to Qt's signal/slot threading model.
23 """
25 # Signals for async operation results
26 operation_completed = pyqtSignal(object) # result
27 operation_failed = pyqtSignal(str) # error_message
28 operation_progress = pyqtSignal(int) # progress_percentage
30 def __init__(self, service_adapter):
31 """
32 Initialize async service bridge.
34 Args:
35 service_adapter: PyQtServiceAdapter instance
36 """
37 super().__init__()
38 self.service_adapter = service_adapter
39 self.active_threads = []
41 def execute_async_operation(self, async_func: Callable, *args, **kwargs) -> None:
42 """
43 Execute async operation in Qt thread.
45 Args:
46 async_func: Async function to execute
47 *args: Function arguments
48 **kwargs: Function keyword arguments
49 """
50 thread = AsyncOperationThread(async_func, *args, **kwargs)
52 # Connect thread signals
53 thread.result_ready.connect(self.operation_completed.emit)
54 thread.error_occurred.connect(self.operation_failed.emit)
55 thread.finished.connect(lambda: self._cleanup_thread(thread))
57 # Track active thread
58 self.active_threads.append(thread)
60 # Start thread
61 thread.start()
63 def _cleanup_thread(self, thread: QThread) -> None:
64 """
65 Clean up completed thread.
67 Args:
68 thread: Completed thread to clean up
69 """
70 if thread in self.active_threads:
71 self.active_threads.remove(thread)
72 thread.deleteLater()
74 def wait_for_all_operations(self, timeout_ms: int = 30000) -> bool:
75 """
76 Wait for all active async operations to complete.
78 Args:
79 timeout_ms: Timeout in milliseconds
81 Returns:
82 True if all operations completed, False if timeout
83 """
84 for thread in self.active_threads[:]: # Copy list to avoid modification during iteration
85 if not thread.wait(timeout_ms):
86 logger.warning(f"Async operation timed out after {timeout_ms}ms")
87 return False
88 return True
91class AsyncOperationThread(QThread):
92 """
93 Thread for executing async operations.
95 Converts async/await patterns to Qt threading model.
96 """
98 result_ready = pyqtSignal(object)
99 error_occurred = pyqtSignal(str)
100 progress_updated = pyqtSignal(int)
102 def __init__(self, async_func: Callable, *args, **kwargs):
103 super().__init__()
104 self.async_func = async_func
105 self.args = args
106 self.kwargs = kwargs
108 def run(self):
109 """Execute async function in thread with new event loop."""
110 try:
111 # Create new event loop for this thread
112 loop = asyncio.new_event_loop()
113 asyncio.set_event_loop(loop)
115 try:
116 # Execute async function
117 result = loop.run_until_complete(
118 self.async_func(*self.args, **self.kwargs)
119 )
120 self.result_ready.emit(result)
122 finally:
123 # Clean up event loop
124 loop.close()
126 except Exception as e:
127 logger.error(f"Async operation failed: {e}")
128 self.error_occurred.emit(str(e))
131class PatternFileServiceBridge:
132 """
133 Bridge for PatternFileService async operations.
135 Adapts PatternFileService to work with PyQt6 service adapter.
136 """
138 def __init__(self, service_adapter):
139 """
140 Initialize pattern file service bridge.
142 Args:
143 service_adapter: PyQtServiceAdapter instance
144 """
145 self.service_adapter = service_adapter
146 self.async_bridge = AsyncServiceBridge(service_adapter)
148 # Import and adapt the original service
149 from openhcs.textual_tui.services.pattern_file_service import PatternFileService
150 self.original_service = PatternFileService(service_adapter)
152 def load_pattern_from_file(self, file_path, callback: Callable = None):
153 """
154 Load pattern from file using Qt threading.
156 Args:
157 file_path: Path to pattern file
158 callback: Optional callback for result
159 """
160 if callback:
161 self.async_bridge.operation_completed.connect(callback)
162 self.async_bridge.operation_failed.connect(
163 lambda error: self.service_adapter.show_error_dialog(f"Load failed: {error}")
164 )
166 self.async_bridge.execute_async_operation(
167 self.original_service.load_pattern_from_file,
168 file_path
169 )
171 def save_pattern_to_file(self, pattern, file_path, callback: Callable = None):
172 """
173 Save pattern to file using Qt threading.
175 Args:
176 pattern: Pattern to save
177 file_path: Path to save to
178 callback: Optional callback for completion
179 """
180 if callback:
181 self.async_bridge.operation_completed.connect(callback)
182 self.async_bridge.operation_failed.connect(
183 lambda error: self.service_adapter.show_error_dialog(f"Save failed: {error}")
184 )
186 self.async_bridge.execute_async_operation(
187 self.original_service.save_pattern_to_file,
188 pattern,
189 file_path
190 )
193class ExternalEditorServiceBridge:
194 """
195 Bridge for ExternalEditorService with PyQt6 integration.
197 Replaces prompt_toolkit dependencies with Qt equivalents.
198 """
200 def __init__(self, service_adapter):
201 """
202 Initialize external editor service bridge.
204 Args:
205 service_adapter: PyQtServiceAdapter instance
206 """
207 self.service_adapter = service_adapter
208 self.async_bridge = AsyncServiceBridge(service_adapter)
210 def edit_pattern_in_external_editor(self, initial_content: str, callback: Callable = None):
211 """
212 Edit pattern in external editor using Qt process management.
214 Args:
215 initial_content: Initial content for editor
216 callback: Optional callback for result
217 """
218 if callback:
219 self.async_bridge.operation_completed.connect(callback)
220 self.async_bridge.operation_failed.connect(
221 lambda error: self.service_adapter.show_error_dialog(f"Editor failed: {error}")
222 )
224 # Use Qt-based external editor implementation
225 self.async_bridge.execute_async_operation(
226 self._qt_external_editor_operation,
227 initial_content
228 )
230 async def _qt_external_editor_operation(self, initial_content: str):
231 """
232 Qt-based external editor operation.
234 Args:
235 initial_content: Initial content for editor
237 Returns:
238 Tuple of (success, pattern, error_message)
239 """
240 import tempfile
241 import os
242 from pathlib import Path
244 # Create temporary file
245 with tempfile.NamedTemporaryFile(delete=False, mode='w+', suffix='.py', encoding='utf-8') as tmp_file:
246 tmp_file.write(initial_content)
247 tmp_file_path = Path(tmp_file.name)
249 try:
250 # Get editor command
251 editor = os.environ.get('EDITOR', 'vim')
252 command = f"{editor} {tmp_file_path}"
254 # Use service adapter to run command
255 success = self.service_adapter.run_system_command(command, wait_for_finish=True)
257 if not success:
258 return False, None, "Editor command failed"
260 # Read modified content
261 with open(tmp_file_path, "r", encoding="utf-8") as f:
262 modified_content = f.read()
264 # Validate content (simplified validation)
265 try:
266 import ast
267 tree = ast.parse(modified_content)
269 # Extract pattern assignment
270 for node in tree.body:
271 if isinstance(node, ast.Assign) and len(node.targets) == 1:
272 target = node.targets[0]
273 if isinstance(target, ast.Name) and target.id == 'pattern':
274 pattern = ast.literal_eval(ast.unparse(node.value))
275 return True, pattern, None
277 return False, None, "No valid pattern assignment found"
279 except Exception as e:
280 return False, None, f"Pattern validation failed: {e}"
282 finally:
283 # Clean up temporary file
284 if tmp_file_path.exists():
285 tmp_file_path.unlink()