Coverage for openhcs/textual_tui/services/external_editor_service.py: 0.0%
60 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
1import ast
2import os
3import tempfile
4from typing import Any, Dict, List, Optional, Tuple, Union
6from prompt_toolkit.application import get_app
7from prompt_toolkit.widgets import Dialog, Label, Button
8from prompt_toolkit.layout import HSplit
10from openhcs.core.pipeline.funcstep_contract_validator import FuncStepContractValidator
12# SafeButton eliminated - use Button directly
14class ExternalEditorService:
15 """
16 Service for handling external text editor interactions,
17 specifically for editing Python literal patterns.
18 """
19 def __init__(self, state: Any):
20 self.state = state # TUIState instance
22 async def edit_pattern_in_external_editor(self, initial_content: str) -> Tuple[bool, Optional[Union[List, Dict]], Optional[str]]:
23 """
24 Launches an external editor (e.g., Vim) with the given content,
25 waits for the user to edit, and then validates the content.
27 Args:
28 initial_content: The initial string content to put into the editor.
30 Returns:
31 A tuple: (success: bool, pattern: Optional[Union[List, Dict]], error_message: Optional[str])
32 """
33 editor = os.environ.get('EDITOR', 'vim')
35 # Create a temporary file with the initial content
36 with tempfile.NamedTemporaryFile(delete=False, mode='w+', suffix='.py', encoding='utf-8') as tmp_file:
37 tmp_file.write(initial_content)
38 tmp_file_path = tmp_file.name
40 try:
41 # Launch the external editor
42 # Use get_app().run_system_command for integration with prompt_toolkit's event loop
43 await get_app().run_system_command(f"{editor} {tmp_file_path}", wait_for_enter=True)
45 # Read the modified content
46 with open(tmp_file_path, "r", encoding="utf-8") as f:
47 modified_content = f.read()
49 # Validate the modified content
50 is_valid, pattern, error_message = self._validate_pattern_file(modified_content)
52 if not is_valid:
53 await self._show_error_dialog(f"Validation Error:\n{error_message}")
54 return False, None, error_message
56 return True, pattern, None
58 except FileNotFoundError:
59 await self._show_error_dialog(f"Editor '{editor}' not found. Please ensure it's installed and in your PATH.")
60 return False, None, f"Editor '{editor}' not found."
61 except Exception as e:
62 await self._show_error_dialog(f"An error occurred while editing: {e}")
63 return False, None, str(e)
64 finally:
65 # Clean up the temporary file
66 if os.path.exists(tmp_file_path):
67 os.remove(tmp_file_path)
69 def _validate_pattern_file(self, content: str) -> Tuple[bool, Optional[Any], Optional[str]]:
70 """
71 Validate that a file contains only a valid pattern assignment.
73 Args:
74 content: The content of the file to validate
76 Returns:
77 Tuple of (is_valid, pattern, error_message)
78 """
79 try:
80 # Parse the file
81 tree = ast.parse(content)
83 # Check that there's only one statement
84 if len(tree.body) != 1:
85 return False, None, "File must contain exactly one statement (pattern assignment)"
87 # Check that the statement is an assignment
88 stmt = tree.body[0]
89 if not isinstance(stmt, ast.Assign):
90 return False, None, "File must contain a pattern assignment"
92 # Check that the assignment target is 'pattern'
93 if len(stmt.targets) != 1 or not isinstance(stmt.targets[0], ast.Name) or stmt.targets[0].id != 'pattern':
94 return False, None, "Assignment target must be 'pattern'"
96 # Use ast.literal_eval to safely evaluate the pattern
97 pattern_str = ast.unparse(stmt.value) if hasattr(ast, 'unparse') else content.strip().split('=', 1)[1].strip()
98 try:
99 pattern = ast.literal_eval(pattern_str)
101 # Validate pattern structure using FuncStepContractValidator
102 # This will raise ValueError if the pattern is invalid
103 try:
104 # Extract functions to validate pattern structure
105 # We don't care about the actual functions, just that the structure is valid
106 FuncStepContractValidator._extract_functions_from_pattern(
107 pattern, "External Editor Service"
108 )
109 return True, pattern, None
110 except ValueError as e:
111 return False, None, f"Invalid pattern structure: {str(e)}"
113 except (ValueError, SyntaxError) as e:
114 # If literal_eval fails, the pattern contains non-literal expressions
115 return False, None, f"Pattern must contain only literals: {str(e)}"
117 except SyntaxError as e:
118 return False, None, f"Syntax error: {str(e)}"
119 except Exception as e:
120 return False, None, f"Validation error: {str(e)}"
122 async def _show_error_dialog(self, message: str):
123 """Helper to show an error dialog."""
124 dialog = Dialog(
125 title="Error",
126 body=HSplit([Label(message)]),
127 buttons=[Button("OK", width=len("OK") + 2)]
128 )
129 await self.state.show_dialog(dialog) # Assuming TUIState has a show_dialog method