repl-toolkit 1.0.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.
Potentially problematic release.
This version of repl-toolkit might be problematic. Click here for more details.
- repl_toolkit/__init__.py +58 -0
- repl_toolkit/actions/__init__.py +25 -0
- repl_toolkit/actions/action.py +217 -0
- repl_toolkit/actions/registry.py +538 -0
- repl_toolkit/actions/shell.py +62 -0
- repl_toolkit/async_repl.py +367 -0
- repl_toolkit/headless_repl.py +247 -0
- repl_toolkit/ptypes.py +119 -0
- repl_toolkit/tests/__init__.py +5 -0
- repl_toolkit/tests/test_actions.py +453 -0
- repl_toolkit/tests/test_async_repl.py +376 -0
- repl_toolkit/tests/test_headless.py +682 -0
- repl_toolkit/tests/test_types.py +173 -0
- repl_toolkit-1.0.0.dist-info/METADATA +641 -0
- repl_toolkit-1.0.0.dist-info/RECORD +18 -0
- repl_toolkit-1.0.0.dist-info/WHEEL +5 -0
- repl_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
- repl_toolkit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for AsyncREPL with action support and late backend binding.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
7
|
+
import asyncio
|
|
8
|
+
from unittest.mock import Mock, AsyncMock, patch
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from repl_toolkit import AsyncREPL, run_async_repl
|
|
12
|
+
from repl_toolkit.actions import ActionRegistry, Action, ActionContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MockBackend:
|
|
16
|
+
"""Mock backend for testing."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.inputs_received = []
|
|
20
|
+
self.should_succeed = True
|
|
21
|
+
|
|
22
|
+
async def handle_input(self, user_input: str) -> bool:
|
|
23
|
+
self.inputs_received.append(user_input)
|
|
24
|
+
await asyncio.sleep(0.01) # Simulate processing
|
|
25
|
+
return self.should_succeed
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestAsyncREPL:
|
|
29
|
+
"""Test AsyncREPL functionality with late backend binding."""
|
|
30
|
+
|
|
31
|
+
def setup_method(self):
|
|
32
|
+
"""Set up test fixtures."""
|
|
33
|
+
self.backend = MockBackend()
|
|
34
|
+
self.action_registry = ActionRegistry()
|
|
35
|
+
|
|
36
|
+
def test_repl_initialization(self):
|
|
37
|
+
"""Test REPL initialization without backend."""
|
|
38
|
+
repl = AsyncREPL()
|
|
39
|
+
|
|
40
|
+
# REPL should initialize without backend
|
|
41
|
+
assert repl.action_registry is not None
|
|
42
|
+
assert hasattr(repl.action_registry, 'backend')
|
|
43
|
+
# Backend not set until run() is called
|
|
44
|
+
assert repl.action_registry.backend is None
|
|
45
|
+
|
|
46
|
+
def test_repl_with_custom_registry(self):
|
|
47
|
+
"""Test REPL with custom action registry."""
|
|
48
|
+
custom_registry = ActionRegistry()
|
|
49
|
+
repl = AsyncREPL(action_registry=custom_registry)
|
|
50
|
+
|
|
51
|
+
assert repl.action_registry is custom_registry
|
|
52
|
+
|
|
53
|
+
def test_repl_with_history(self):
|
|
54
|
+
"""Test REPL with history file."""
|
|
55
|
+
history_path = Path("/tmp/test_history.txt")
|
|
56
|
+
repl = AsyncREPL(history_path=history_path)
|
|
57
|
+
|
|
58
|
+
# Should not raise error during initialization
|
|
59
|
+
assert repl.session.history is not None
|
|
60
|
+
|
|
61
|
+
def test_key_parsing(self):
|
|
62
|
+
"""Test key combination parsing."""
|
|
63
|
+
repl = AsyncREPL()
|
|
64
|
+
|
|
65
|
+
# Test function keys
|
|
66
|
+
assert repl._parse_key_combination("F1") == ("f1",)
|
|
67
|
+
assert repl._parse_key_combination("F12") == ("f12",)
|
|
68
|
+
|
|
69
|
+
# Test modifier combinations
|
|
70
|
+
assert repl._parse_key_combination("ctrl-s") == ("c-s",)
|
|
71
|
+
assert repl._parse_key_combination("alt-h") == ("escape", "h")
|
|
72
|
+
|
|
73
|
+
# Test single keys
|
|
74
|
+
assert repl._parse_key_combination("enter") == ("enter",)
|
|
75
|
+
|
|
76
|
+
def test_should_exit(self):
|
|
77
|
+
"""Test exit condition detection."""
|
|
78
|
+
repl = AsyncREPL()
|
|
79
|
+
|
|
80
|
+
assert repl._should_exit("/exit")
|
|
81
|
+
assert repl._should_exit("/quit")
|
|
82
|
+
assert repl._should_exit(" /EXIT ")
|
|
83
|
+
assert not repl._should_exit("/help")
|
|
84
|
+
assert not repl._should_exit("regular input")
|
|
85
|
+
|
|
86
|
+
def test_backend_injection_during_run(self):
|
|
87
|
+
"""Test backend injection into action registry during run."""
|
|
88
|
+
repl = AsyncREPL()
|
|
89
|
+
|
|
90
|
+
# Initially no backend
|
|
91
|
+
assert repl.action_registry.backend is None
|
|
92
|
+
|
|
93
|
+
# Backend should be injected when run() is called
|
|
94
|
+
# We can't easily test the full run() method, but we can test the injection logic
|
|
95
|
+
repl.action_registry.backend = self.backend
|
|
96
|
+
assert repl.action_registry.backend is self.backend
|
|
97
|
+
|
|
98
|
+
@patch('repl_toolkit.async_repl.PromptSession')
|
|
99
|
+
def test_key_bindings_registration(self, mock_session):
|
|
100
|
+
"""Test that key bindings are properly registered."""
|
|
101
|
+
# Add a test action with shortcut
|
|
102
|
+
test_action = Action(
|
|
103
|
+
name="test_shortcut",
|
|
104
|
+
description="Test shortcut",
|
|
105
|
+
category="Test",
|
|
106
|
+
handler=lambda ctx: None,
|
|
107
|
+
keys="F5"
|
|
108
|
+
)
|
|
109
|
+
self.action_registry.register_action(test_action)
|
|
110
|
+
|
|
111
|
+
repl = AsyncREPL(action_registry=self.action_registry)
|
|
112
|
+
|
|
113
|
+
# Verify PromptSession was called with key_bindings
|
|
114
|
+
mock_session.assert_called_once()
|
|
115
|
+
call_kwargs = mock_session.call_args[1]
|
|
116
|
+
assert 'key_bindings' in call_kwargs
|
|
117
|
+
|
|
118
|
+
key_bindings = call_kwargs['key_bindings']
|
|
119
|
+
assert key_bindings is not None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestRunAsyncREPL:
|
|
123
|
+
"""Test run_async_repl convenience function."""
|
|
124
|
+
|
|
125
|
+
def setup_method(self):
|
|
126
|
+
"""Set up test fixtures."""
|
|
127
|
+
self.backend = MockBackend()
|
|
128
|
+
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
async def test_run_async_repl_basic(self):
|
|
131
|
+
"""Test basic run_async_repl functionality."""
|
|
132
|
+
with patch('repl_toolkit.async_repl.AsyncREPL') as mock_repl_class:
|
|
133
|
+
mock_repl = Mock()
|
|
134
|
+
mock_repl.run = AsyncMock()
|
|
135
|
+
mock_repl_class.return_value = mock_repl
|
|
136
|
+
|
|
137
|
+
await run_async_repl(
|
|
138
|
+
backend=self.backend,
|
|
139
|
+
initial_message="test message"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Verify REPL was created and run with correct parameters
|
|
143
|
+
mock_repl_class.assert_called_once()
|
|
144
|
+
mock_repl.run.assert_called_once_with(self.backend, "test message")
|
|
145
|
+
|
|
146
|
+
@pytest.mark.asyncio
|
|
147
|
+
async def test_run_async_repl_with_registry(self):
|
|
148
|
+
"""Test run_async_repl with custom action registry."""
|
|
149
|
+
custom_registry = ActionRegistry()
|
|
150
|
+
|
|
151
|
+
with patch('repl_toolkit.async_repl.AsyncREPL') as mock_repl_class:
|
|
152
|
+
mock_repl = Mock()
|
|
153
|
+
mock_repl.run = AsyncMock()
|
|
154
|
+
mock_repl_class.return_value = mock_repl
|
|
155
|
+
|
|
156
|
+
await run_async_repl(
|
|
157
|
+
backend=self.backend,
|
|
158
|
+
action_registry=custom_registry,
|
|
159
|
+
prompt_string="Custom: ",
|
|
160
|
+
history_path=Path("/tmp/custom_history.txt")
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Verify parameters were passed correctly to AsyncREPL constructor
|
|
164
|
+
call_args = mock_repl_class.call_args[0]
|
|
165
|
+
call_kwargs = mock_repl_class.call_args[1] if mock_repl_class.call_args[1] else {}
|
|
166
|
+
|
|
167
|
+
# Check that action_registry was passed (could be positional or keyword)
|
|
168
|
+
assert (len(call_args) > 0 and call_args[0] is custom_registry) or \
|
|
169
|
+
call_kwargs.get('action_registry') is custom_registry
|
|
170
|
+
|
|
171
|
+
# Verify run was called with backend and initial_message
|
|
172
|
+
mock_repl.run.assert_called_once_with(self.backend, None)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestREPLActionIntegration:
|
|
176
|
+
"""Test integration between REPL and action system with late backend binding."""
|
|
177
|
+
|
|
178
|
+
def setup_method(self):
|
|
179
|
+
"""Set up test fixtures."""
|
|
180
|
+
self.backend = MockBackend()
|
|
181
|
+
# Create action registry (backend will be injected later)
|
|
182
|
+
self.action_registry = ActionRegistry()
|
|
183
|
+
self.executed_actions = []
|
|
184
|
+
|
|
185
|
+
# Add test action
|
|
186
|
+
def test_handler(context):
|
|
187
|
+
self.executed_actions.append((context.triggered_by, context.args))
|
|
188
|
+
|
|
189
|
+
test_action = Action(
|
|
190
|
+
name="test_action",
|
|
191
|
+
description="Test action",
|
|
192
|
+
category="Test",
|
|
193
|
+
handler=test_handler,
|
|
194
|
+
command="/test",
|
|
195
|
+
keys="F9"
|
|
196
|
+
)
|
|
197
|
+
self.action_registry.register_action(test_action)
|
|
198
|
+
|
|
199
|
+
def test_action_registry_integration(self):
|
|
200
|
+
"""Test that REPL properly integrates with action registry."""
|
|
201
|
+
repl = AsyncREPL(action_registry=self.action_registry)
|
|
202
|
+
|
|
203
|
+
# Registry should be the one we provided
|
|
204
|
+
assert repl.action_registry is self.action_registry
|
|
205
|
+
|
|
206
|
+
# Test action should be available
|
|
207
|
+
assert repl.action_registry.validate_action("test_action")
|
|
208
|
+
assert "/test" in repl.action_registry.command_map
|
|
209
|
+
assert "F9" in repl.action_registry.key_map
|
|
210
|
+
|
|
211
|
+
# Backend not set until run() is called
|
|
212
|
+
assert repl.action_registry.backend is None
|
|
213
|
+
|
|
214
|
+
def test_action_registry_without_backend(self):
|
|
215
|
+
"""Test REPL with action registry that has no backend initially."""
|
|
216
|
+
registry_without_backend = ActionRegistry() # No backend
|
|
217
|
+
repl = AsyncREPL(action_registry=registry_without_backend)
|
|
218
|
+
|
|
219
|
+
# Registry should not have backend initially (as designed)
|
|
220
|
+
assert repl.action_registry.backend is None
|
|
221
|
+
assert repl.action_registry is registry_without_backend
|
|
222
|
+
|
|
223
|
+
def test_backend_injection_during_execution(self):
|
|
224
|
+
"""Test that backend gets injected when needed."""
|
|
225
|
+
repl = AsyncREPL(action_registry=self.action_registry)
|
|
226
|
+
|
|
227
|
+
# Simulate what happens during run() - backend gets injected
|
|
228
|
+
repl.action_registry.backend = self.backend
|
|
229
|
+
|
|
230
|
+
# Now test command execution
|
|
231
|
+
repl.action_registry.handle_command("/test arg1 arg2")
|
|
232
|
+
|
|
233
|
+
# Verify action was executed
|
|
234
|
+
assert len(self.executed_actions) == 1
|
|
235
|
+
triggered_by, args = self.executed_actions[0]
|
|
236
|
+
assert triggered_by == "command"
|
|
237
|
+
assert args == ["arg1", "arg2"]
|
|
238
|
+
|
|
239
|
+
def test_shortcut_execution_integration(self):
|
|
240
|
+
"""Test shortcut execution through REPL."""
|
|
241
|
+
repl = AsyncREPL(action_registry=self.action_registry)
|
|
242
|
+
|
|
243
|
+
# Inject backend (simulating what run() does)
|
|
244
|
+
repl.action_registry.backend = self.backend
|
|
245
|
+
|
|
246
|
+
# Simulate shortcut execution
|
|
247
|
+
mock_event = Mock()
|
|
248
|
+
repl.action_registry.handle_shortcut("F9", mock_event)
|
|
249
|
+
|
|
250
|
+
# Verify action was executed
|
|
251
|
+
assert len(self.executed_actions) == 1
|
|
252
|
+
triggered_by, _ = self.executed_actions[0]
|
|
253
|
+
assert triggered_by == "shortcut"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class TestErrorHandling:
|
|
257
|
+
"""Test error handling in AsyncREPL with late backend binding."""
|
|
258
|
+
|
|
259
|
+
def setup_method(self):
|
|
260
|
+
"""Set up test fixtures."""
|
|
261
|
+
self.backend = MockBackend()
|
|
262
|
+
|
|
263
|
+
def test_backend_error_handling(self):
|
|
264
|
+
"""Test handling of backend errors."""
|
|
265
|
+
self.backend.should_succeed = False
|
|
266
|
+
repl = AsyncREPL()
|
|
267
|
+
|
|
268
|
+
# Should not raise error during initialization (no backend yet)
|
|
269
|
+
assert repl.action_registry is not None
|
|
270
|
+
|
|
271
|
+
def test_invalid_key_combination(self):
|
|
272
|
+
"""Test handling of invalid key combinations."""
|
|
273
|
+
repl = AsyncREPL()
|
|
274
|
+
|
|
275
|
+
# Should handle invalid key combinations gracefully
|
|
276
|
+
result = repl._parse_key_combination("invalid-key-combo")
|
|
277
|
+
assert isinstance(result, tuple)
|
|
278
|
+
|
|
279
|
+
@patch('repl_toolkit.async_repl.logger')
|
|
280
|
+
def test_shortcut_registration_error(self, mock_logger):
|
|
281
|
+
"""Test handling of shortcut registration errors."""
|
|
282
|
+
action_registry = ActionRegistry()
|
|
283
|
+
|
|
284
|
+
# Add action with potentially problematic key combo
|
|
285
|
+
action_registry.register_action(Action(
|
|
286
|
+
name="problematic",
|
|
287
|
+
description="Problematic action",
|
|
288
|
+
category="Test",
|
|
289
|
+
handler=lambda ctx: None,
|
|
290
|
+
keys="invalid-combo"
|
|
291
|
+
))
|
|
292
|
+
|
|
293
|
+
# Should not raise error during REPL creation
|
|
294
|
+
repl = AsyncREPL(action_registry=action_registry)
|
|
295
|
+
assert repl is not None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class TestLateBackendBinding:
|
|
299
|
+
"""Test late backend binding specific functionality."""
|
|
300
|
+
|
|
301
|
+
def setup_method(self):
|
|
302
|
+
"""Set up test fixtures."""
|
|
303
|
+
self.backend = MockBackend()
|
|
304
|
+
|
|
305
|
+
def test_repl_creation_without_backend(self):
|
|
306
|
+
"""Test that REPL can be created without backend."""
|
|
307
|
+
repl = AsyncREPL()
|
|
308
|
+
|
|
309
|
+
# Should create successfully
|
|
310
|
+
assert repl is not None
|
|
311
|
+
assert repl.action_registry is not None
|
|
312
|
+
assert repl.action_registry.backend is None
|
|
313
|
+
|
|
314
|
+
def test_backend_injection_pattern(self):
|
|
315
|
+
"""Test the backend injection pattern."""
|
|
316
|
+
# Create REPL without backend
|
|
317
|
+
action_registry = ActionRegistry()
|
|
318
|
+
repl = AsyncREPL(action_registry=action_registry)
|
|
319
|
+
|
|
320
|
+
# Initially no backend
|
|
321
|
+
assert action_registry.backend is None
|
|
322
|
+
|
|
323
|
+
# Simulate backend becoming available (like in resource context)
|
|
324
|
+
action_registry.backend = self.backend
|
|
325
|
+
|
|
326
|
+
# Now backend is available
|
|
327
|
+
assert action_registry.backend is self.backend
|
|
328
|
+
|
|
329
|
+
def test_action_execution_with_late_backend(self):
|
|
330
|
+
"""Test that actions work with late backend binding."""
|
|
331
|
+
executed = []
|
|
332
|
+
|
|
333
|
+
def test_handler(context):
|
|
334
|
+
# Handler can access backend through context
|
|
335
|
+
backend = context.backend
|
|
336
|
+
executed.append(backend is not None)
|
|
337
|
+
|
|
338
|
+
action_registry = ActionRegistry()
|
|
339
|
+
action_registry.register_action(Action(
|
|
340
|
+
name="test",
|
|
341
|
+
description="Test",
|
|
342
|
+
category="Test",
|
|
343
|
+
handler=test_handler,
|
|
344
|
+
command="/test"
|
|
345
|
+
))
|
|
346
|
+
|
|
347
|
+
# Execute without backend
|
|
348
|
+
context = ActionContext(registry=action_registry, triggered_by="test")
|
|
349
|
+
action_registry.execute_action("test", context)
|
|
350
|
+
assert executed == [False] # No backend available
|
|
351
|
+
|
|
352
|
+
# Inject backend and execute again
|
|
353
|
+
executed.clear()
|
|
354
|
+
action_registry.backend = self.backend
|
|
355
|
+
context = ActionContext(registry=action_registry, backend=self.backend, triggered_by="test")
|
|
356
|
+
action_registry.execute_action("test", context)
|
|
357
|
+
assert executed == [True] # Backend available
|
|
358
|
+
|
|
359
|
+
class TestAsyncREPLEdgeCases:
|
|
360
|
+
"""Test edge cases in AsyncREPL."""
|
|
361
|
+
|
|
362
|
+
def test_register_shortcuts_without_key_map(self):
|
|
363
|
+
"""Test registering shortcuts when action_registry has no key_map."""
|
|
364
|
+
# Create a mock registry without key_map attribute
|
|
365
|
+
class MockRegistry:
|
|
366
|
+
pass
|
|
367
|
+
|
|
368
|
+
mock_registry = MockRegistry()
|
|
369
|
+
repl = AsyncREPL(action_registry=mock_registry)
|
|
370
|
+
|
|
371
|
+
# This should not raise - it should handle missing key_map gracefully
|
|
372
|
+
bindings = KeyBindings()
|
|
373
|
+
repl._register_action_shortcuts(bindings)
|
|
374
|
+
|
|
375
|
+
# Should have no bindings registered
|
|
376
|
+
assert len(bindings.bindings) == 0
|