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.

@@ -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