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

1import ast 

2import os 

3import subprocess 

4import tempfile 

5from typing import Any, Dict, List, Optional, Tuple, Union 

6 

7from prompt_toolkit.application import get_app 

8from prompt_toolkit.widgets import Dialog, Label, Button 

9from prompt_toolkit.layout import HSplit 

10 

11from openhcs.core.pipeline.funcstep_contract_validator import FuncStepContractValidator 

12 

13# SafeButton eliminated - use Button directly 

14 

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 

22 

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. 

27 

28 Args: 

29 initial_content: The initial string content to put into the editor. 

30 

31 Returns: 

32 A tuple: (success: bool, pattern: Optional[Union[List, Dict]], error_message: Optional[str]) 

33 """ 

34 editor = os.environ.get('EDITOR', 'vim') 

35 

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 

40 

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) 

45 

46 # Read the modified content 

47 with open(tmp_file_path, "r", encoding="utf-8") as f: 

48 modified_content = f.read() 

49 

50 # Validate the modified content 

51 is_valid, pattern, error_message = self._validate_pattern_file(modified_content) 

52 

53 if not is_valid: 

54 await self._show_error_dialog(f"Validation Error:\n{error_message}") 

55 return False, None, error_message 

56 

57 return True, pattern, None 

58 

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) 

69 

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. 

73 

74 Args: 

75 content: The content of the file to validate 

76 

77 Returns: 

78 Tuple of (is_valid, pattern, error_message) 

79 """ 

80 try: 

81 # Parse the file 

82 tree = ast.parse(content) 

83 

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)" 

87 

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" 

92 

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'" 

96 

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) 

101 

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)}" 

113 

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)}" 

117 

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)}" 

122 

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