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

1import ast 

2import os 

3import tempfile 

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

5 

6from prompt_toolkit.application import get_app 

7from prompt_toolkit.widgets import Dialog, Label, Button 

8from prompt_toolkit.layout import HSplit 

9 

10from openhcs.core.pipeline.funcstep_contract_validator import FuncStepContractValidator 

11 

12# SafeButton eliminated - use Button directly 

13 

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 

21 

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. 

26 

27 Args: 

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

29 

30 Returns: 

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

32 """ 

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

34 

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 

39 

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) 

44 

45 # Read the modified content 

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

47 modified_content = f.read() 

48 

49 # Validate the modified content 

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

51 

52 if not is_valid: 

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

54 return False, None, error_message 

55 

56 return True, pattern, None 

57 

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) 

68 

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. 

72 

73 Args: 

74 content: The content of the file to validate 

75 

76 Returns: 

77 Tuple of (is_valid, pattern, error_message) 

78 """ 

79 try: 

80 # Parse the file 

81 tree = ast.parse(content) 

82 

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

86 

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" 

91 

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

95 

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) 

100 

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

112 

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

116 

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

121 

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