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