repl-toolkit 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,251 @@
1
+ from typing import Optional
2
+
3
+ from loguru import logger
4
+
5
+ from .actions.registry import ActionRegistry
6
+ from .ptypes import ActionHandler, AsyncBackend
7
+
8
+
9
+ class HeadlessREPL:
10
+ """
11
+ Headless REPL that reads from stdin and processes through action framework.
12
+
13
+ Similar to interactive mode but:
14
+ - Reads from stdin instead of prompt_toolkit
15
+ - Accumulates content in buffer until /send
16
+ - Processes commands through action system
17
+ - Supports multiple /send cycles
18
+ - Auto-sends remaining buffer on EOF
19
+ """
20
+
21
+ def __init__(self, action_registry: Optional[ActionHandler] = None):
22
+ """Initialize headless REPL."""
23
+ logger.trace("HeadlessREPL.__init__() entry")
24
+
25
+ # Simple string buffer for content accumulation
26
+ self.buffer = ""
27
+ self.action_registry = action_registry or ActionRegistry()
28
+
29
+ # State tracking
30
+ self.send_count = 0
31
+ self.total_success = True
32
+ self.running = True
33
+
34
+ logger.trace("HeadlessREPL.__init__() exit")
35
+
36
+ async def run(self, backend: AsyncBackend, initial_message: Optional[str] = None) -> bool:
37
+ """
38
+ Run headless mode with stdin processing.
39
+
40
+ Args:
41
+ backend: Backend for processing input
42
+ initial_message: Optional message to process before stdin loop
43
+
44
+ Returns:
45
+ bool: True if all operations succeeded
46
+ """
47
+ logger.trace("HeadlessREPL.run() entry")
48
+
49
+ # Set backend in action registry
50
+ self.action_registry.backend = backend # type: ignore[attr-defined]
51
+
52
+ try:
53
+ # Process initial message if provided
54
+ if initial_message:
55
+ logger.info(f"Processing initial message: {initial_message}")
56
+ success = await backend.handle_input(initial_message)
57
+ if not success:
58
+ logger.warning("Initial message processing failed")
59
+ self.total_success = False
60
+
61
+ # Enter stdin processing loop
62
+ await self._stdin_loop(backend)
63
+
64
+ logger.trace("HeadlessREPL.run() exit - success")
65
+ return self.total_success
66
+
67
+ except Exception as e:
68
+ logger.error(f"Error in headless processing: {e}")
69
+ logger.trace("HeadlessREPL.run() exit - exception")
70
+ return False
71
+
72
+ async def _stdin_loop(self, backend: AsyncBackend):
73
+ """
74
+ Process stdin with synchronous reading, async backend calls.
75
+
76
+ Reads lines from stdin synchronously (blocking), processes commands
77
+ through the action system, and accumulates content in buffer until
78
+ /send commands trigger backend processing.
79
+
80
+ Args:
81
+ backend: Backend for processing accumulated content
82
+ """
83
+ import sys
84
+
85
+ line_num = 0
86
+
87
+ while True:
88
+ # Simple synchronous readline - blocks until line available
89
+ # This is perfectly fine! We want to wait for the next line.
90
+ line = sys.stdin.readline()
91
+
92
+ if not line: # EOF
93
+ break
94
+
95
+ line_num += 1
96
+ line = line.rstrip("\n\r")
97
+
98
+ if line.startswith("/"):
99
+ if line == "/send":
100
+ await self._execute_send(backend, f"line {line_num}")
101
+ else:
102
+ # Synchronous command processing
103
+ self._execute_command(line)
104
+ else:
105
+ # Synchronous buffer addition
106
+ self._add_to_buffer(line)
107
+
108
+ await self._handle_eof(backend)
109
+
110
+ def _add_to_buffer(self, line: str):
111
+ """
112
+ Add a line to the buffer.
113
+
114
+ Args:
115
+ line: Line of text to add to the buffer
116
+ """
117
+ logger.trace("HeadlessREPL._add_to_buffer() entry")
118
+
119
+ if self.buffer:
120
+ self.buffer += "\n" + line
121
+ else:
122
+ self.buffer = line
123
+
124
+ logger.debug(f"Added line to buffer, total length: {len(self.buffer)}")
125
+ logger.trace("HeadlessREPL._add_to_buffer() exit")
126
+
127
+ async def _execute_send(self, backend: AsyncBackend, context_info: str):
128
+ """
129
+ Execute /send command - send buffer to backend and wait.
130
+
131
+ Args:
132
+ backend: Backend to send buffer content to
133
+ context_info: Context information for logging (e.g., "line 5", "EOF")
134
+ """
135
+ logger.trace("HeadlessREPL._execute_send() entry")
136
+
137
+ buffer_content = self.buffer.strip()
138
+
139
+ if not buffer_content:
140
+ logger.debug(f"Send #{self.send_count + 1} at {context_info}: empty buffer, skipping")
141
+ logger.trace("HeadlessREPL._execute_send() exit - empty buffer")
142
+ return
143
+
144
+ self.send_count += 1
145
+ logger.info(
146
+ f"Send #{self.send_count} at {context_info}: sending {len(buffer_content)} characters"
147
+ )
148
+
149
+ try:
150
+ # Send to backend and wait for completion
151
+ success = await backend.handle_input(buffer_content)
152
+
153
+ if success:
154
+ logger.info(f"Send #{self.send_count} completed successfully")
155
+ else:
156
+ logger.warning(f"Send #{self.send_count} completed with backend reporting failure")
157
+ self.total_success = False
158
+
159
+ # Clear buffer after send (successful or not)
160
+ self.buffer = ""
161
+
162
+ except Exception as e:
163
+ logger.error(f"Send #{self.send_count} failed with exception: {e}")
164
+ self.total_success = False
165
+ # Clear buffer even on exception to continue processing
166
+ self.buffer = ""
167
+
168
+ logger.trace("HeadlessREPL._execute_send() exit")
169
+
170
+ def _execute_command(self, command: str):
171
+ """
172
+ Execute a command through the action system.
173
+
174
+ Args:
175
+ command: Command string to execute (e.g., "/help", "/shortcuts")
176
+ """
177
+ logger.trace("HeadlessREPL._execute_command() entry")
178
+
179
+ try:
180
+ self.action_registry.handle_command(command, headless_mode=True, buffer=self.buffer)
181
+
182
+ except Exception as e:
183
+ logger.error(f"Error executing command '{command}': {e}")
184
+ # Don't fail entire process for command errors
185
+
186
+ logger.trace("HeadlessREPL._execute_command() exit")
187
+
188
+ async def _handle_eof(self, backend: AsyncBackend):
189
+ """
190
+ Handle end of stdin - send remaining buffer if not empty.
191
+
192
+ Args:
193
+ backend: Backend to send remaining buffer content to
194
+ """
195
+ logger.trace("HeadlessREPL._handle_eof() entry")
196
+
197
+ buffer_content = self.buffer.strip()
198
+
199
+ if buffer_content:
200
+ logger.info("EOF reached with non-empty buffer, triggering final send")
201
+ await self._execute_send(backend, "EOF")
202
+ else:
203
+ logger.debug("EOF reached with empty buffer")
204
+
205
+ logger.trace("HeadlessREPL._handle_eof() exit")
206
+
207
+
208
+ async def run_headless_mode(
209
+ backend: AsyncBackend,
210
+ action_registry: Optional[ActionHandler] = None,
211
+ initial_message: Optional[str] = None,
212
+ ) -> bool:
213
+ """
214
+ Run headless mode reading from stdin with action framework support.
215
+
216
+ Processes an optional initial message, then reads from stdin line by line.
217
+ Content lines are accumulated in a buffer, commands are processed through
218
+ the action system, and /send commands trigger backend processing.
219
+
220
+ Args:
221
+ backend: Backend for processing input
222
+ action_registry: Optional action registry for command processing
223
+ initial_message: Optional message to process before stdin loop
224
+
225
+ Returns:
226
+ bool: True if all send operations succeeded
227
+
228
+ Example:
229
+ # stdin input:
230
+ # Message part 1
231
+ # Message part 2
232
+ # /send # Send parts 1-2, wait for completion
233
+ # Message part 3
234
+ # /help # Process command
235
+ # Message part 4
236
+ # /send # Send parts 3-4, wait for completion
237
+ # Final message
238
+ # ^D # EOF triggers final send
239
+
240
+ success = await run_headless_mode(
241
+ backend=my_backend,
242
+ initial_message="Starting headless session"
243
+ )
244
+ """
245
+ logger.trace("run_headless_mode() entry")
246
+
247
+ headless_repl = HeadlessREPL(action_registry)
248
+ result = await headless_repl.run(backend, initial_message)
249
+
250
+ logger.trace("run_headless_mode() exit")
251
+ return result
repl_toolkit/ptypes.py ADDED
@@ -0,0 +1,122 @@
1
+ """
2
+ Protocol types for repl_toolkit.
3
+
4
+ Defines the interface contracts that backends and handlers must implement
5
+ for compatibility with the REPL toolkit.
6
+ """
7
+
8
+ from typing import TYPE_CHECKING, List, Protocol, runtime_checkable
9
+
10
+ if TYPE_CHECKING:
11
+ from .actions.action import ActionContext # Avoid circular import
12
+
13
+
14
+ @runtime_checkable
15
+ class AsyncBackend(Protocol):
16
+ """
17
+ Protocol for async backends that process user input.
18
+
19
+ Backends are responsible for handling user input and generating responses
20
+ in an asynchronous manner, supporting cancellation and error handling.
21
+ """
22
+
23
+ async def handle_input(self, user_input: str) -> bool:
24
+ """
25
+ Handle user input asynchronously.
26
+
27
+ Args:
28
+ user_input: The input string from the user
29
+
30
+ Returns:
31
+ bool: True if processing was successful, False if there was an error
32
+
33
+ Note:
34
+ This method should handle its own error reporting to the user.
35
+ The return value indicates success/failure for flow control.
36
+ """
37
+ ...
38
+
39
+
40
+ @runtime_checkable
41
+ class ActionHandler(Protocol):
42
+ """
43
+ Protocol for action handlers in the action system.
44
+
45
+ ActionHandler defines the interface for handling both command-based
46
+ and keyboard shortcut-based actions in a coherent manner.
47
+ """
48
+
49
+ def execute_action(self, action_name: str, context: "ActionContext") -> None:
50
+ """
51
+ Execute an action by name.
52
+
53
+ Args:
54
+ action_name: Name of the action to execute
55
+ context: Action context containing relevant information
56
+
57
+ Raises:
58
+ ActionError: If action execution fails
59
+ """
60
+ ...
61
+
62
+ def handle_command(self, command_string: str, **kwargs) -> None:
63
+ """
64
+ Handle a command string by mapping to appropriate action.
65
+
66
+ Args:
67
+ command_string: Full command string (e.g., '/help arg1 arg2')
68
+ **kwargs: Additional context parameters (e.g., headless_mode, buffer)
69
+
70
+ Note:
71
+ This method parses the command and maps it to the appropriate
72
+ action execution with proper context.
73
+ """
74
+ ...
75
+
76
+ def validate_action(self, action_name: str) -> bool:
77
+ """
78
+ Validate if an action is supported.
79
+
80
+ Args:
81
+ action_name: Action name to validate
82
+
83
+ Returns:
84
+ bool: True if action is supported, False otherwise
85
+ """
86
+ ...
87
+
88
+ def list_actions(self) -> List[str]:
89
+ """
90
+ Return a list of all available action names.
91
+
92
+ Returns:
93
+ List of action names
94
+ """
95
+ ...
96
+
97
+
98
+ @runtime_checkable
99
+ class Completer(Protocol):
100
+ """
101
+ Protocol for auto-completion providers.
102
+
103
+ Completers provide tab-completion suggestions for user input,
104
+ supporting both command completion and context-aware suggestions.
105
+ """
106
+
107
+ def get_completions(self, document, complete_event):
108
+ """
109
+ Get completions for the current input.
110
+
111
+ Args:
112
+ document: Current document state from prompt_toolkit
113
+ complete_event: Completion event from prompt_toolkit
114
+
115
+ Yields:
116
+ Completion: Individual completion suggestions
117
+
118
+ Note:
119
+ This follows the prompt_toolkit Completer interface for
120
+ compatibility with the underlying prompt_toolkit system.
121
+ """
122
+ ...
@@ -0,0 +1,5 @@
1
+ """
2
+ Test suite for repl_toolkit.
3
+
4
+ Comprehensive tests for the action system and REPL functionality.
5
+ """
@@ -0,0 +1,79 @@
1
+ """
2
+ Pytest configuration and fixtures for repl-toolkit tests.
3
+ """
4
+
5
+ import sys
6
+ from unittest.mock import Mock
7
+
8
+ import pytest
9
+ from prompt_toolkit.input import DummyInput
10
+ from prompt_toolkit.output import DummyOutput
11
+
12
+
13
+ @pytest.fixture
14
+ def mock_terminal_for_repl(monkeypatch):
15
+ """
16
+ Mock terminal I/O specifically for REPL tests that need it.
17
+
18
+ This fixture should be explicitly requested by tests that create AsyncREPL or
19
+ similar objects that require terminal access.
20
+
21
+ Usage:
22
+ def test_something(mock_terminal_for_repl):
23
+ # Terminal is now mocked
24
+ repl = AsyncREPL()
25
+ """
26
+
27
+ # Mock prompt_toolkit's create_output to return DummyOutput
28
+ def mock_create_output(*args, **kwargs):
29
+ return DummyOutput()
30
+
31
+ # Mock prompt_toolkit's create_input to return DummyInput
32
+ def mock_create_input(*args, **kwargs):
33
+ return DummyInput()
34
+
35
+ # Patch the output creation functions
36
+ monkeypatch.setattr("prompt_toolkit.output.defaults.create_output", mock_create_output)
37
+
38
+ # Patch the input creation functions
39
+ monkeypatch.setattr("prompt_toolkit.input.defaults.create_input", mock_create_input)
40
+
41
+ # Also patch platform-specific output classes to prevent initialization errors
42
+ if sys.platform == "win32":
43
+ # Mock Windows-specific components
44
+ monkeypatch.setattr(
45
+ "prompt_toolkit.output.windows10.Windows10_Output.__init__",
46
+ lambda self, *args, **kwargs: None,
47
+ )
48
+ monkeypatch.setattr(
49
+ "prompt_toolkit.output.win32.Win32Output.__init__", lambda self, *args, **kwargs: None
50
+ )
51
+
52
+
53
+ @pytest.fixture
54
+ def dummy_input():
55
+ """Provide a DummyInput for tests that need to simulate input."""
56
+ return DummyInput()
57
+
58
+
59
+ @pytest.fixture
60
+ def dummy_output():
61
+ """Provide a DummyOutput for tests that need to capture output."""
62
+ return DummyOutput()
63
+
64
+
65
+ @pytest.fixture
66
+ def mock_asyncio_event_loop():
67
+ """Provide a mock event loop for testing async code."""
68
+ import asyncio
69
+
70
+ loop = asyncio.new_event_loop()
71
+ asyncio.set_event_loop(loop)
72
+
73
+ yield loop
74
+
75
+ # Cleanup
76
+ try:
77
+ loop.close()
78
+ except Exception:
79
+ pass