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.
- repl_toolkit/__init__.py +70 -0
- repl_toolkit/actions/__init__.py +24 -0
- repl_toolkit/actions/action.py +223 -0
- repl_toolkit/actions/registry.py +564 -0
- repl_toolkit/async_repl.py +374 -0
- repl_toolkit/completion/__init__.py +15 -0
- repl_toolkit/completion/prefix.py +109 -0
- repl_toolkit/completion/shell_expansion.py +453 -0
- repl_toolkit/formatting.py +152 -0
- repl_toolkit/headless_repl.py +251 -0
- repl_toolkit/ptypes.py +122 -0
- repl_toolkit/tests/__init__.py +5 -0
- repl_toolkit/tests/conftest.py +79 -0
- repl_toolkit/tests/test_actions.py +578 -0
- repl_toolkit/tests/test_async_repl.py +381 -0
- repl_toolkit/tests/test_completion.py +656 -0
- repl_toolkit/tests/test_formatting.py +232 -0
- repl_toolkit/tests/test_headless.py +677 -0
- repl_toolkit/tests/test_types.py +174 -0
- repl_toolkit-1.2.0.dist-info/METADATA +761 -0
- repl_toolkit-1.2.0.dist-info/RECORD +24 -0
- repl_toolkit-1.2.0.dist-info/WHEEL +5 -0
- repl_toolkit-1.2.0.dist-info/licenses/LICENSE +21 -0
- repl_toolkit-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the action system.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from unittest.mock import Mock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from repl_toolkit.actions import (
|
|
10
|
+
Action,
|
|
11
|
+
ActionContext,
|
|
12
|
+
ActionError,
|
|
13
|
+
ActionExecutionError,
|
|
14
|
+
ActionRegistry,
|
|
15
|
+
ActionValidationError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestAction:
|
|
20
|
+
"""Test Action dataclass functionality."""
|
|
21
|
+
|
|
22
|
+
def test_action_creation_minimal(self):
|
|
23
|
+
"""Test creating action with minimal required parameters."""
|
|
24
|
+
action = Action(
|
|
25
|
+
name="test_action",
|
|
26
|
+
description="Test action",
|
|
27
|
+
category="Test",
|
|
28
|
+
handler=lambda ctx: None,
|
|
29
|
+
command="/test",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assert action.name == "test_action"
|
|
33
|
+
assert action.description == "Test action"
|
|
34
|
+
assert action.category == "Test"
|
|
35
|
+
assert action.command == "/test"
|
|
36
|
+
assert action.has_command
|
|
37
|
+
assert not action.has_shortcut
|
|
38
|
+
|
|
39
|
+
def test_action_creation_full(self):
|
|
40
|
+
"""Test creating action with all parameters."""
|
|
41
|
+
handler = lambda ctx: None
|
|
42
|
+
|
|
43
|
+
action = Action(
|
|
44
|
+
name="full_action",
|
|
45
|
+
description="Full test action",
|
|
46
|
+
category="Test",
|
|
47
|
+
handler=handler,
|
|
48
|
+
command="/full",
|
|
49
|
+
command_usage="/full [args] - Full test command",
|
|
50
|
+
keys="F2",
|
|
51
|
+
keys_description="Full test shortcut",
|
|
52
|
+
enabled=True,
|
|
53
|
+
context="test_context",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert action.name == "full_action"
|
|
57
|
+
assert action.has_command
|
|
58
|
+
assert action.has_shortcut
|
|
59
|
+
assert action.enabled
|
|
60
|
+
assert action.context == "test_context"
|
|
61
|
+
|
|
62
|
+
def test_action_validation_errors(self):
|
|
63
|
+
"""Test action validation failures."""
|
|
64
|
+
# Empty name
|
|
65
|
+
with pytest.raises(ValueError, match="Action name cannot be empty"):
|
|
66
|
+
Action(
|
|
67
|
+
name="",
|
|
68
|
+
description="Test",
|
|
69
|
+
category="Test",
|
|
70
|
+
handler=lambda ctx: None,
|
|
71
|
+
command="/test",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Empty description
|
|
75
|
+
with pytest.raises(ValueError, match="Action description cannot be empty"):
|
|
76
|
+
Action(
|
|
77
|
+
name="test",
|
|
78
|
+
description="",
|
|
79
|
+
category="Test",
|
|
80
|
+
handler=lambda ctx: None,
|
|
81
|
+
command="/test",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# No command or keys
|
|
85
|
+
with pytest.raises(ValueError, match="Action must have either command or keys binding"):
|
|
86
|
+
Action(name="test", description="Test", category="Test", handler=lambda ctx: None)
|
|
87
|
+
|
|
88
|
+
# Invalid command format
|
|
89
|
+
with pytest.raises(ValueError, match="Command 'test' must start with '/'"):
|
|
90
|
+
Action(
|
|
91
|
+
name="test",
|
|
92
|
+
description="Test",
|
|
93
|
+
category="Test",
|
|
94
|
+
handler=lambda ctx: None,
|
|
95
|
+
command="test",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def test_keys_list_handling(self):
|
|
99
|
+
"""Test handling of keys as string vs list."""
|
|
100
|
+
# Single key as string
|
|
101
|
+
action1 = Action(
|
|
102
|
+
name="test1", description="Test", category="Test", handler=lambda ctx: None, keys="F1"
|
|
103
|
+
)
|
|
104
|
+
assert action1.get_keys_list() == ["F1"]
|
|
105
|
+
|
|
106
|
+
# Multiple keys as list
|
|
107
|
+
action2 = Action(
|
|
108
|
+
name="test2",
|
|
109
|
+
description="Test",
|
|
110
|
+
category="Test",
|
|
111
|
+
handler=lambda ctx: None,
|
|
112
|
+
keys=["F1", "ctrl-h"],
|
|
113
|
+
)
|
|
114
|
+
assert action2.get_keys_list() == ["F1", "ctrl-h"]
|
|
115
|
+
|
|
116
|
+
# No keys
|
|
117
|
+
action3 = Action(
|
|
118
|
+
name="test3",
|
|
119
|
+
description="Test",
|
|
120
|
+
category="Test",
|
|
121
|
+
handler=lambda ctx: None,
|
|
122
|
+
command="/test",
|
|
123
|
+
)
|
|
124
|
+
assert action3.get_keys_list() == []
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestActionContext:
|
|
128
|
+
"""Test ActionContext functionality."""
|
|
129
|
+
|
|
130
|
+
def test_context_creation(self):
|
|
131
|
+
"""Test action context creation."""
|
|
132
|
+
registry = Mock()
|
|
133
|
+
backend = Mock()
|
|
134
|
+
|
|
135
|
+
context = ActionContext(
|
|
136
|
+
registry=registry, backend=backend, args=["arg1", "arg2"], triggered_by="command"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
assert context.registry is registry
|
|
140
|
+
assert context.backend is backend
|
|
141
|
+
assert context.args == ["arg1", "arg2"]
|
|
142
|
+
assert context.triggered_by == "command"
|
|
143
|
+
|
|
144
|
+
def test_context_triggered_by_detection(self):
|
|
145
|
+
"""Test automatic triggered_by detection."""
|
|
146
|
+
registry = Mock()
|
|
147
|
+
|
|
148
|
+
# Should detect shortcut from event
|
|
149
|
+
context1 = ActionContext(registry=registry, event=Mock())
|
|
150
|
+
assert context1.triggered_by == "shortcut"
|
|
151
|
+
|
|
152
|
+
# Should detect command from args
|
|
153
|
+
context2 = ActionContext(registry=registry, args=["arg"])
|
|
154
|
+
assert context2.triggered_by == "command"
|
|
155
|
+
|
|
156
|
+
# Should detect command from user_input
|
|
157
|
+
context3 = ActionContext(registry=registry, user_input="/test")
|
|
158
|
+
assert context3.triggered_by == "command"
|
|
159
|
+
|
|
160
|
+
# Should default to programmatic
|
|
161
|
+
context4 = ActionContext(registry=registry)
|
|
162
|
+
assert context4.triggered_by == "programmatic"
|
|
163
|
+
|
|
164
|
+
def test_context_printer_default(self):
|
|
165
|
+
"""Test that printer defaults to print."""
|
|
166
|
+
registry = Mock()
|
|
167
|
+
context = ActionContext(registry=registry)
|
|
168
|
+
assert context.printer == print
|
|
169
|
+
|
|
170
|
+
def test_context_custom_printer(self):
|
|
171
|
+
"""Test that custom printer can be set."""
|
|
172
|
+
registry = Mock()
|
|
173
|
+
custom_printer = Mock()
|
|
174
|
+
context = ActionContext(registry=registry, printer=custom_printer)
|
|
175
|
+
assert context.printer == custom_printer
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestActionRegistry:
|
|
179
|
+
"""Test ActionRegistry functionality."""
|
|
180
|
+
|
|
181
|
+
def setup_method(self):
|
|
182
|
+
"""Set up test registry."""
|
|
183
|
+
self.registry = ActionRegistry()
|
|
184
|
+
|
|
185
|
+
def test_registry_initialization(self):
|
|
186
|
+
"""Test registry initializes with built-in actions."""
|
|
187
|
+
assert len(self.registry.actions) > 0
|
|
188
|
+
assert "show_help" in self.registry.actions
|
|
189
|
+
assert "/help" in self.registry.command_map
|
|
190
|
+
assert "F1" in self.registry.key_map
|
|
191
|
+
|
|
192
|
+
def test_registry_custom_printer(self):
|
|
193
|
+
"""Test registry with custom printer."""
|
|
194
|
+
mock_printer = Mock()
|
|
195
|
+
registry = ActionRegistry(printer=mock_printer)
|
|
196
|
+
assert registry.printer == mock_printer
|
|
197
|
+
|
|
198
|
+
def test_registry_default_printer(self):
|
|
199
|
+
"""Test registry defaults to print."""
|
|
200
|
+
registry = ActionRegistry()
|
|
201
|
+
assert registry.printer == print
|
|
202
|
+
|
|
203
|
+
def test_register_action(self):
|
|
204
|
+
"""Test action registration."""
|
|
205
|
+
action = Action(
|
|
206
|
+
name="test_action",
|
|
207
|
+
description="Test action",
|
|
208
|
+
category="Test",
|
|
209
|
+
handler=lambda ctx: None,
|
|
210
|
+
command="/test",
|
|
211
|
+
keys="F10",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
self.registry.register_action(action)
|
|
215
|
+
|
|
216
|
+
assert "test_action" in self.registry.actions
|
|
217
|
+
assert "/test" in self.registry.command_map
|
|
218
|
+
assert "F10" in self.registry.key_map
|
|
219
|
+
assert self.registry.command_map["/test"] == "test_action"
|
|
220
|
+
assert self.registry.key_map["F10"] == "test_action"
|
|
221
|
+
|
|
222
|
+
def test_register_action_conflicts(self):
|
|
223
|
+
"""Test action registration conflict detection."""
|
|
224
|
+
action1 = Action(
|
|
225
|
+
name="action1",
|
|
226
|
+
description="Test",
|
|
227
|
+
category="Test",
|
|
228
|
+
handler=lambda ctx: None,
|
|
229
|
+
command="/test",
|
|
230
|
+
)
|
|
231
|
+
action2 = Action(
|
|
232
|
+
name="action2",
|
|
233
|
+
description="Test",
|
|
234
|
+
category="Test",
|
|
235
|
+
handler=lambda ctx: None,
|
|
236
|
+
command="/test", # Same command
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
self.registry.register_action(action1)
|
|
240
|
+
|
|
241
|
+
with pytest.raises(ActionValidationError, match="Command '/test' already bound"):
|
|
242
|
+
self.registry.register_action(action2)
|
|
243
|
+
|
|
244
|
+
def test_convenience_registration_methods(self):
|
|
245
|
+
"""Test convenience registration methods."""
|
|
246
|
+
# Test action registration with both command and keys
|
|
247
|
+
self.registry.register_action(
|
|
248
|
+
name="both_test",
|
|
249
|
+
description="Both test",
|
|
250
|
+
category="Test",
|
|
251
|
+
handler=lambda ctx: None,
|
|
252
|
+
command="/both",
|
|
253
|
+
keys="F11",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
assert "both_test" in self.registry.actions
|
|
257
|
+
assert "/both" in self.registry.command_map
|
|
258
|
+
assert "F11" in self.registry.key_map
|
|
259
|
+
|
|
260
|
+
# Test command-only registration
|
|
261
|
+
self.registry.register_action(
|
|
262
|
+
name="cmd_test",
|
|
263
|
+
command="/cmdonly",
|
|
264
|
+
description="Command only",
|
|
265
|
+
category="Test",
|
|
266
|
+
handler=lambda ctx: None,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
action = self.registry.get_action("cmd_test")
|
|
270
|
+
assert action.has_command
|
|
271
|
+
assert not action.has_shortcut
|
|
272
|
+
|
|
273
|
+
# Test shortcut-only registration
|
|
274
|
+
self.registry.register_action(
|
|
275
|
+
name="key_test",
|
|
276
|
+
keys="F12",
|
|
277
|
+
description="Key only",
|
|
278
|
+
category="Test",
|
|
279
|
+
handler=lambda ctx: None,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
action = self.registry.get_action("key_test")
|
|
283
|
+
assert not action.has_command
|
|
284
|
+
assert action.has_shortcut
|
|
285
|
+
|
|
286
|
+
def test_action_lookup_methods(self):
|
|
287
|
+
"""Test action lookup methods."""
|
|
288
|
+
action = Action(
|
|
289
|
+
name="lookup_test",
|
|
290
|
+
description="Lookup test",
|
|
291
|
+
category="Test",
|
|
292
|
+
handler=lambda ctx: None,
|
|
293
|
+
command="/lookup",
|
|
294
|
+
keys="ctrl-l",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
self.registry.register_action(action)
|
|
298
|
+
|
|
299
|
+
# Test lookup by name
|
|
300
|
+
found = self.registry.get_action("lookup_test")
|
|
301
|
+
assert found is action
|
|
302
|
+
|
|
303
|
+
# Test lookup by command
|
|
304
|
+
found = self.registry.get_action_by_command("/lookup")
|
|
305
|
+
assert found is action
|
|
306
|
+
|
|
307
|
+
# Test lookup by keys
|
|
308
|
+
found = self.registry.get_action_by_keys("ctrl-l")
|
|
309
|
+
assert found is action
|
|
310
|
+
|
|
311
|
+
# Test not found cases
|
|
312
|
+
assert self.registry.get_action("nonexistent") is None
|
|
313
|
+
assert self.registry.get_action_by_command("/nonexistent") is None
|
|
314
|
+
assert self.registry.get_action_by_keys("nonexistent") is None
|
|
315
|
+
|
|
316
|
+
def test_execute_action(self):
|
|
317
|
+
"""Test action execution."""
|
|
318
|
+
executed = []
|
|
319
|
+
|
|
320
|
+
def test_handler(context):
|
|
321
|
+
executed.append(context.triggered_by)
|
|
322
|
+
|
|
323
|
+
action = Action(
|
|
324
|
+
name="exec_test",
|
|
325
|
+
description="Execution test",
|
|
326
|
+
category="Test",
|
|
327
|
+
handler=test_handler,
|
|
328
|
+
command="/exec",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
self.registry.register_action(action)
|
|
332
|
+
|
|
333
|
+
context = ActionContext(registry=self.registry, triggered_by="test")
|
|
334
|
+
|
|
335
|
+
self.registry.execute_action("exec_test", context)
|
|
336
|
+
assert executed == ["test"]
|
|
337
|
+
|
|
338
|
+
def test_execute_nonexistent_action(self):
|
|
339
|
+
"""Test executing nonexistent action."""
|
|
340
|
+
context = ActionContext(registry=self.registry)
|
|
341
|
+
|
|
342
|
+
with pytest.raises(ActionError, match="Action 'nonexistent' not found"):
|
|
343
|
+
self.registry.execute_action("nonexistent", context)
|
|
344
|
+
|
|
345
|
+
def test_execute_disabled_action(self):
|
|
346
|
+
"""Test executing disabled action."""
|
|
347
|
+
action = Action(
|
|
348
|
+
name="disabled_test",
|
|
349
|
+
description="Disabled test",
|
|
350
|
+
category="Test",
|
|
351
|
+
handler=lambda ctx: None,
|
|
352
|
+
command="/disabled",
|
|
353
|
+
enabled=False,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
self.registry.register_action(action)
|
|
357
|
+
context = ActionContext(registry=self.registry)
|
|
358
|
+
|
|
359
|
+
# Should not raise error, but should not execute
|
|
360
|
+
self.registry.execute_action("disabled_test", context)
|
|
361
|
+
|
|
362
|
+
def test_handle_command(self):
|
|
363
|
+
"""Test command handling."""
|
|
364
|
+
executed = []
|
|
365
|
+
|
|
366
|
+
def test_handler(context):
|
|
367
|
+
executed.append(context.args)
|
|
368
|
+
|
|
369
|
+
action = Action(
|
|
370
|
+
name="cmd_test",
|
|
371
|
+
description="Command test",
|
|
372
|
+
category="Test",
|
|
373
|
+
handler=test_handler,
|
|
374
|
+
command="/cmdtest",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
self.registry.register_action(action)
|
|
378
|
+
|
|
379
|
+
self.registry.handle_command("/cmdtest arg1 arg2")
|
|
380
|
+
assert executed == [["arg1", "arg2"]]
|
|
381
|
+
|
|
382
|
+
def test_handle_command_with_custom_printer(self):
|
|
383
|
+
"""Test command handling with custom printer."""
|
|
384
|
+
mock_printer = Mock()
|
|
385
|
+
registry = ActionRegistry(printer=mock_printer)
|
|
386
|
+
|
|
387
|
+
# Test unknown command uses custom printer
|
|
388
|
+
registry.handle_command("/unknown")
|
|
389
|
+
|
|
390
|
+
# Should have printed to custom printer
|
|
391
|
+
assert mock_printer.call_count >= 1
|
|
392
|
+
calls = [str(call) for call in mock_printer.call_args_list]
|
|
393
|
+
assert any("Unknown command" in str(call) for call in calls)
|
|
394
|
+
|
|
395
|
+
def test_handle_unknown_command(self):
|
|
396
|
+
"""Test handling unknown command."""
|
|
397
|
+
# Should not raise error, just print message
|
|
398
|
+
self.registry.handle_command("/unknown")
|
|
399
|
+
|
|
400
|
+
def test_handle_shortcut(self):
|
|
401
|
+
"""Test shortcut handling."""
|
|
402
|
+
executed = []
|
|
403
|
+
|
|
404
|
+
def test_handler(context):
|
|
405
|
+
executed.append(context.event)
|
|
406
|
+
|
|
407
|
+
action = Action(
|
|
408
|
+
name="key_test",
|
|
409
|
+
description="Key test",
|
|
410
|
+
category="Test",
|
|
411
|
+
handler=test_handler,
|
|
412
|
+
keys="F5",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
self.registry.register_action(action)
|
|
416
|
+
|
|
417
|
+
mock_event = Mock()
|
|
418
|
+
self.registry.handle_shortcut("F5", mock_event)
|
|
419
|
+
assert executed == [mock_event]
|
|
420
|
+
|
|
421
|
+
def test_handle_unknown_shortcut(self):
|
|
422
|
+
"""Test handling unknown shortcut."""
|
|
423
|
+
# Should not raise error, just log
|
|
424
|
+
self.registry.handle_shortcut("unknown", Mock())
|
|
425
|
+
|
|
426
|
+
def test_printer_propagation_to_context(self):
|
|
427
|
+
"""Test that printer is propagated to ActionContext."""
|
|
428
|
+
mock_printer = Mock()
|
|
429
|
+
registry = ActionRegistry(printer=mock_printer)
|
|
430
|
+
|
|
431
|
+
outputs = []
|
|
432
|
+
|
|
433
|
+
def test_handler(context):
|
|
434
|
+
# Verify context has the custom printer
|
|
435
|
+
assert context.printer == mock_printer
|
|
436
|
+
context.printer("Test output")
|
|
437
|
+
outputs.append("executed")
|
|
438
|
+
|
|
439
|
+
action = Action(
|
|
440
|
+
name="printer_test",
|
|
441
|
+
description="Printer test",
|
|
442
|
+
category="Test",
|
|
443
|
+
handler=test_handler,
|
|
444
|
+
command="/printertest",
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
registry.register_action(action)
|
|
448
|
+
registry.handle_command("/printertest")
|
|
449
|
+
|
|
450
|
+
# Verify handler was executed
|
|
451
|
+
assert outputs == ["executed"]
|
|
452
|
+
|
|
453
|
+
# Verify printer was called
|
|
454
|
+
mock_printer.assert_called_with("Test output")
|
|
455
|
+
|
|
456
|
+
def test_list_methods(self):
|
|
457
|
+
"""Test list methods."""
|
|
458
|
+
actions = self.registry.list_actions()
|
|
459
|
+
assert isinstance(actions, list)
|
|
460
|
+
assert "show_help" in actions
|
|
461
|
+
|
|
462
|
+
commands = self.registry.list_commands()
|
|
463
|
+
assert isinstance(commands, list)
|
|
464
|
+
assert "/help" in commands
|
|
465
|
+
|
|
466
|
+
shortcuts = self.registry.list_shortcuts()
|
|
467
|
+
assert isinstance(shortcuts, list)
|
|
468
|
+
assert "F1" in shortcuts
|
|
469
|
+
|
|
470
|
+
def test_categories(self):
|
|
471
|
+
"""Test category organization."""
|
|
472
|
+
categories = self.registry.get_actions_by_category()
|
|
473
|
+
assert isinstance(categories, dict)
|
|
474
|
+
assert "General" in categories
|
|
475
|
+
assert len(categories["General"]) > 0
|
|
476
|
+
|
|
477
|
+
def test_builtin_help_action(self):
|
|
478
|
+
"""Test built-in help action."""
|
|
479
|
+
# Test general help
|
|
480
|
+
context = ActionContext(registry=self.registry, args=[])
|
|
481
|
+
self.registry.execute_action("show_help", context)
|
|
482
|
+
|
|
483
|
+
# Test specific help
|
|
484
|
+
context = ActionContext(registry=self.registry, args=["show_help"])
|
|
485
|
+
self.registry.execute_action("show_help", context)
|
|
486
|
+
|
|
487
|
+
# Test help for nonexistent action
|
|
488
|
+
context = ActionContext(registry=self.registry, args=["nonexistent"])
|
|
489
|
+
self.registry.execute_action("show_help", context)
|
|
490
|
+
|
|
491
|
+
def test_builtin_help_action_custom_printer(self):
|
|
492
|
+
"""Test built-in help action with custom printer."""
|
|
493
|
+
mock_printer = Mock()
|
|
494
|
+
registry = ActionRegistry(printer=mock_printer)
|
|
495
|
+
|
|
496
|
+
# Execute help command
|
|
497
|
+
registry.handle_command("/help")
|
|
498
|
+
|
|
499
|
+
# Verify custom printer was called
|
|
500
|
+
assert mock_printer.call_count > 0
|
|
501
|
+
|
|
502
|
+
# Check that help output was sent to custom printer
|
|
503
|
+
calls = [str(call) for call in mock_printer.call_args_list]
|
|
504
|
+
assert any("Available Actions" in str(call) for call in calls)
|
|
505
|
+
|
|
506
|
+
def test_builtin_shortcuts_action(self):
|
|
507
|
+
"""Test built-in shortcuts listing action."""
|
|
508
|
+
context = ActionContext(registry=self.registry, args=[])
|
|
509
|
+
self.registry.execute_action("list_shortcuts", context)
|
|
510
|
+
|
|
511
|
+
def test_builtin_shortcuts_action_custom_printer(self):
|
|
512
|
+
"""Test built-in shortcuts action with custom printer."""
|
|
513
|
+
mock_printer = Mock()
|
|
514
|
+
registry = ActionRegistry(printer=mock_printer)
|
|
515
|
+
|
|
516
|
+
# Execute shortcuts command
|
|
517
|
+
registry.handle_command("/shortcuts")
|
|
518
|
+
|
|
519
|
+
# Verify custom printer was called
|
|
520
|
+
assert mock_printer.call_count > 0
|
|
521
|
+
|
|
522
|
+
# Check that shortcuts output was sent to custom printer
|
|
523
|
+
calls = [str(call) for call in mock_printer.call_args_list]
|
|
524
|
+
assert any("Keyboard Shortcuts" in str(call) for call in calls)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class TestActionHandlerProtocol:
|
|
528
|
+
"""Test ActionHandler protocol compliance."""
|
|
529
|
+
|
|
530
|
+
def test_protocol_compliance(self):
|
|
531
|
+
"""Test that ActionRegistry implements ActionHandler protocol."""
|
|
532
|
+
from repl_toolkit.ptypes import ActionHandler
|
|
533
|
+
|
|
534
|
+
registry = ActionRegistry()
|
|
535
|
+
assert isinstance(registry, ActionHandler)
|
|
536
|
+
|
|
537
|
+
# Test protocol methods
|
|
538
|
+
assert hasattr(registry, "execute_action")
|
|
539
|
+
assert hasattr(registry, "handle_command")
|
|
540
|
+
assert hasattr(registry, "validate_action")
|
|
541
|
+
assert hasattr(registry, "list_actions")
|
|
542
|
+
|
|
543
|
+
def test_validate_action(self):
|
|
544
|
+
"""Test action validation."""
|
|
545
|
+
registry = ActionRegistry()
|
|
546
|
+
|
|
547
|
+
assert registry.validate_action("show_help") # Built-in action
|
|
548
|
+
assert not registry.validate_action("nonexistent")
|
|
549
|
+
|
|
550
|
+
def test_list_actions(self):
|
|
551
|
+
"""Test action listing."""
|
|
552
|
+
registry = ActionRegistry()
|
|
553
|
+
actions = registry.list_actions()
|
|
554
|
+
|
|
555
|
+
assert isinstance(actions, list)
|
|
556
|
+
assert len(actions) > 0
|
|
557
|
+
assert "show_help" in actions
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class TestActionValidationExtended:
|
|
561
|
+
"""Additional validation tests for Action."""
|
|
562
|
+
|
|
563
|
+
def test_empty_category_validation(self):
|
|
564
|
+
"""Test that empty category raises ValueError."""
|
|
565
|
+
with pytest.raises(ValueError, match="category cannot be empty"):
|
|
566
|
+
Action(
|
|
567
|
+
name="test",
|
|
568
|
+
description="Test",
|
|
569
|
+
category="", # Empty category
|
|
570
|
+
handler=lambda ctx: None,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
def test_empty_string_handler_validation(self):
|
|
574
|
+
"""Test that empty string handler raises ValueError."""
|
|
575
|
+
with pytest.raises(ValueError, match="handler cannot be empty string"):
|
|
576
|
+
Action(
|
|
577
|
+
name="test", description="Test", category="Test", handler="" # Empty string handler
|
|
578
|
+
)
|