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
repl_toolkit/ptypes.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
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 Protocol, runtime_checkable, Optional, List
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class AsyncBackend(Protocol):
|
|
14
|
+
"""
|
|
15
|
+
Protocol for async backends that process user input.
|
|
16
|
+
|
|
17
|
+
Backends are responsible for handling user input and generating responses
|
|
18
|
+
in an asynchronous manner, supporting cancellation and error handling.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
async def handle_input(self, user_input: str) -> bool:
|
|
22
|
+
"""
|
|
23
|
+
Handle user input asynchronously.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
user_input: The input string from the user
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
bool: True if processing was successful, False if there was an error
|
|
30
|
+
|
|
31
|
+
Note:
|
|
32
|
+
This method should handle its own error reporting to the user.
|
|
33
|
+
The return value indicates success/failure for flow control.
|
|
34
|
+
"""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
@runtime_checkable
|
|
38
|
+
class ActionHandler(Protocol):
|
|
39
|
+
"""
|
|
40
|
+
Protocol for action handlers in the action system.
|
|
41
|
+
|
|
42
|
+
ActionHandler defines the interface for handling both command-based
|
|
43
|
+
and keyboard shortcut-based actions in a coherent manner.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def execute_action(self, action_name: str, context: "ActionContext") -> None:
|
|
47
|
+
"""
|
|
48
|
+
Execute an action by name.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
action_name: Name of the action to execute
|
|
52
|
+
context: Action context containing relevant information
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ActionError: If action execution fails
|
|
56
|
+
"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def handle_command(self, command_string: str, **kwargs) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Handle a command string by mapping to appropriate action.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
command_string: Full command string (e.g., '/help arg1 arg2')
|
|
65
|
+
**kwargs: Additional context parameters (e.g., headless_mode, buffer)
|
|
66
|
+
|
|
67
|
+
Note:
|
|
68
|
+
This method parses the command and maps it to the appropriate
|
|
69
|
+
action execution with proper context.
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def validate_action(self, action_name: str) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
Validate if an action is supported.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
action_name: Action name to validate
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
bool: True if action is supported, False otherwise
|
|
82
|
+
"""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
def list_actions(self) -> List[str]:
|
|
86
|
+
"""
|
|
87
|
+
Return a list of all available action names.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of action names
|
|
91
|
+
"""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@runtime_checkable
|
|
96
|
+
class Completer(Protocol):
|
|
97
|
+
"""
|
|
98
|
+
Protocol for auto-completion providers.
|
|
99
|
+
|
|
100
|
+
Completers provide tab-completion suggestions for user input,
|
|
101
|
+
supporting both command completion and context-aware suggestions.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def get_completions(self, document, complete_event):
|
|
105
|
+
"""
|
|
106
|
+
Get completions for the current input.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
document: Current document state from prompt_toolkit
|
|
110
|
+
complete_event: Completion event from prompt_toolkit
|
|
111
|
+
|
|
112
|
+
Yields:
|
|
113
|
+
Completion: Individual completion suggestions
|
|
114
|
+
|
|
115
|
+
Note:
|
|
116
|
+
This follows the prompt_toolkit Completer interface for
|
|
117
|
+
compatibility with the underlying prompt_toolkit system.
|
|
118
|
+
"""
|
|
119
|
+
...
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the action system.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from unittest.mock import Mock
|
|
7
|
+
|
|
8
|
+
from repl_toolkit.actions import Action, ActionContext, ActionRegistry
|
|
9
|
+
from repl_toolkit.actions import ActionError, ActionValidationError, ActionExecutionError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestAction:
|
|
13
|
+
"""Test Action dataclass functionality."""
|
|
14
|
+
|
|
15
|
+
def test_action_creation_minimal(self):
|
|
16
|
+
"""Test creating action with minimal required parameters."""
|
|
17
|
+
action = Action(
|
|
18
|
+
name="test_action",
|
|
19
|
+
description="Test action",
|
|
20
|
+
category="Test",
|
|
21
|
+
handler=lambda ctx: None,
|
|
22
|
+
command="/test"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
assert action.name == "test_action"
|
|
26
|
+
assert action.description == "Test action"
|
|
27
|
+
assert action.category == "Test"
|
|
28
|
+
assert action.command == "/test"
|
|
29
|
+
assert action.has_command
|
|
30
|
+
assert not action.has_shortcut
|
|
31
|
+
|
|
32
|
+
def test_action_creation_full(self):
|
|
33
|
+
"""Test creating action with all parameters."""
|
|
34
|
+
handler = lambda ctx: None
|
|
35
|
+
|
|
36
|
+
action = Action(
|
|
37
|
+
name="full_action",
|
|
38
|
+
description="Full test action",
|
|
39
|
+
category="Test",
|
|
40
|
+
handler=handler,
|
|
41
|
+
command="/full",
|
|
42
|
+
command_usage="/full [args] - Full test command",
|
|
43
|
+
keys="F2",
|
|
44
|
+
keys_description="Full test shortcut",
|
|
45
|
+
enabled=True,
|
|
46
|
+
context="test_context"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
assert action.name == "full_action"
|
|
50
|
+
assert action.has_command
|
|
51
|
+
assert action.has_shortcut
|
|
52
|
+
assert action.enabled
|
|
53
|
+
assert action.context == "test_context"
|
|
54
|
+
|
|
55
|
+
def test_action_validation_errors(self):
|
|
56
|
+
"""Test action validation failures."""
|
|
57
|
+
# Empty name
|
|
58
|
+
with pytest.raises(ValueError, match="Action name cannot be empty"):
|
|
59
|
+
Action(name="", description="Test", category="Test", handler=lambda ctx: None, command="/test")
|
|
60
|
+
|
|
61
|
+
# Empty description
|
|
62
|
+
with pytest.raises(ValueError, match="Action description cannot be empty"):
|
|
63
|
+
Action(name="test", description="", category="Test", handler=lambda ctx: None, command="/test")
|
|
64
|
+
|
|
65
|
+
# No command or keys
|
|
66
|
+
with pytest.raises(ValueError, match="Action must have either command or keys binding"):
|
|
67
|
+
Action(name="test", description="Test", category="Test", handler=lambda ctx: None)
|
|
68
|
+
|
|
69
|
+
# Invalid command format
|
|
70
|
+
with pytest.raises(ValueError, match="Command 'test' must start with '/'"):
|
|
71
|
+
Action(name="test", description="Test", category="Test", handler=lambda ctx: None, command="test")
|
|
72
|
+
|
|
73
|
+
def test_keys_list_handling(self):
|
|
74
|
+
"""Test handling of keys as string vs list."""
|
|
75
|
+
# Single key as string
|
|
76
|
+
action1 = Action(
|
|
77
|
+
name="test1", description="Test", category="Test",
|
|
78
|
+
handler=lambda ctx: None, keys="F1"
|
|
79
|
+
)
|
|
80
|
+
assert action1.get_keys_list() == ["F1"]
|
|
81
|
+
|
|
82
|
+
# Multiple keys as list
|
|
83
|
+
action2 = Action(
|
|
84
|
+
name="test2", description="Test", category="Test",
|
|
85
|
+
handler=lambda ctx: None, keys=["F1", "ctrl-h"]
|
|
86
|
+
)
|
|
87
|
+
assert action2.get_keys_list() == ["F1", "ctrl-h"]
|
|
88
|
+
|
|
89
|
+
# No keys
|
|
90
|
+
action3 = Action(
|
|
91
|
+
name="test3", description="Test", category="Test",
|
|
92
|
+
handler=lambda ctx: None, command="/test"
|
|
93
|
+
)
|
|
94
|
+
assert action3.get_keys_list() == []
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestActionContext:
|
|
98
|
+
"""Test ActionContext functionality."""
|
|
99
|
+
|
|
100
|
+
def test_context_creation(self):
|
|
101
|
+
"""Test action context creation."""
|
|
102
|
+
registry = Mock()
|
|
103
|
+
backend = Mock()
|
|
104
|
+
|
|
105
|
+
context = ActionContext(
|
|
106
|
+
registry=registry,
|
|
107
|
+
backend=backend,
|
|
108
|
+
args=["arg1", "arg2"],
|
|
109
|
+
triggered_by="command"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
assert context.registry is registry
|
|
113
|
+
assert context.backend is backend
|
|
114
|
+
assert context.args == ["arg1", "arg2"]
|
|
115
|
+
assert context.triggered_by == "command"
|
|
116
|
+
|
|
117
|
+
def test_context_triggered_by_detection(self):
|
|
118
|
+
"""Test automatic triggered_by detection."""
|
|
119
|
+
registry = Mock()
|
|
120
|
+
|
|
121
|
+
# Should detect shortcut from event
|
|
122
|
+
context1 = ActionContext(registry=registry, event=Mock())
|
|
123
|
+
assert context1.triggered_by == "shortcut"
|
|
124
|
+
|
|
125
|
+
# Should detect command from args
|
|
126
|
+
context2 = ActionContext(registry=registry, args=["arg"])
|
|
127
|
+
assert context2.triggered_by == "command"
|
|
128
|
+
|
|
129
|
+
# Should detect command from user_input
|
|
130
|
+
context3 = ActionContext(registry=registry, user_input="/test")
|
|
131
|
+
assert context3.triggered_by == "command"
|
|
132
|
+
|
|
133
|
+
# Should default to programmatic
|
|
134
|
+
context4 = ActionContext(registry=registry)
|
|
135
|
+
assert context4.triggered_by == "programmatic"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestActionRegistry:
|
|
139
|
+
"""Test ActionRegistry functionality."""
|
|
140
|
+
|
|
141
|
+
def setup_method(self):
|
|
142
|
+
"""Set up test registry."""
|
|
143
|
+
self.registry = ActionRegistry()
|
|
144
|
+
|
|
145
|
+
def test_registry_initialization(self):
|
|
146
|
+
"""Test registry initializes with built-in actions."""
|
|
147
|
+
assert len(self.registry.actions) > 0
|
|
148
|
+
assert "show_help" in self.registry.actions
|
|
149
|
+
assert "/help" in self.registry.command_map
|
|
150
|
+
assert "F1" in self.registry.key_map
|
|
151
|
+
|
|
152
|
+
def test_register_action(self):
|
|
153
|
+
"""Test action registration."""
|
|
154
|
+
action = Action(
|
|
155
|
+
name="test_action",
|
|
156
|
+
description="Test action",
|
|
157
|
+
category="Test",
|
|
158
|
+
handler=lambda ctx: None,
|
|
159
|
+
command="/test",
|
|
160
|
+
keys="F10"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self.registry.register_action(action)
|
|
164
|
+
|
|
165
|
+
assert "test_action" in self.registry.actions
|
|
166
|
+
assert "/test" in self.registry.command_map
|
|
167
|
+
assert "F10" in self.registry.key_map
|
|
168
|
+
assert self.registry.command_map["/test"] == "test_action"
|
|
169
|
+
assert self.registry.key_map["F10"] == "test_action"
|
|
170
|
+
|
|
171
|
+
def test_register_action_conflicts(self):
|
|
172
|
+
"""Test action registration conflict detection."""
|
|
173
|
+
action1 = Action(
|
|
174
|
+
name="action1", description="Test", category="Test",
|
|
175
|
+
handler=lambda ctx: None, command="/test"
|
|
176
|
+
)
|
|
177
|
+
action2 = Action(
|
|
178
|
+
name="action2", description="Test", category="Test",
|
|
179
|
+
handler=lambda ctx: None, command="/test" # Same command
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
self.registry.register_action(action1)
|
|
183
|
+
|
|
184
|
+
with pytest.raises(ActionValidationError, match="Command '/test' already bound"):
|
|
185
|
+
self.registry.register_action(action2)
|
|
186
|
+
|
|
187
|
+
def test_convenience_registration_methods(self):
|
|
188
|
+
"""Test convenience registration methods."""
|
|
189
|
+
# Test action registration with both command and keys
|
|
190
|
+
self.registry.register_action(
|
|
191
|
+
name="both_test",
|
|
192
|
+
description="Both test",
|
|
193
|
+
category="Test",
|
|
194
|
+
handler=lambda ctx: None,
|
|
195
|
+
command="/both",
|
|
196
|
+
keys="F11"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
assert "both_test" in self.registry.actions
|
|
200
|
+
assert "/both" in self.registry.command_map
|
|
201
|
+
assert "F11" in self.registry.key_map
|
|
202
|
+
|
|
203
|
+
# Test command-only registration
|
|
204
|
+
self.registry.register_action(
|
|
205
|
+
name="cmd_test",
|
|
206
|
+
command="/cmdonly",
|
|
207
|
+
description="Command only",
|
|
208
|
+
category="Test",
|
|
209
|
+
handler=lambda ctx: None
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
action = self.registry.get_action("cmd_test")
|
|
213
|
+
assert action.has_command
|
|
214
|
+
assert not action.has_shortcut
|
|
215
|
+
|
|
216
|
+
# Test shortcut-only registration
|
|
217
|
+
self.registry.register_action(
|
|
218
|
+
name="key_test",
|
|
219
|
+
keys="F12",
|
|
220
|
+
description="Key only",
|
|
221
|
+
category="Test",
|
|
222
|
+
handler=lambda ctx: None
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
action = self.registry.get_action("key_test")
|
|
226
|
+
assert not action.has_command
|
|
227
|
+
assert action.has_shortcut
|
|
228
|
+
|
|
229
|
+
def test_action_lookup_methods(self):
|
|
230
|
+
"""Test action lookup methods."""
|
|
231
|
+
action = Action(
|
|
232
|
+
name="lookup_test",
|
|
233
|
+
description="Lookup test",
|
|
234
|
+
category="Test",
|
|
235
|
+
handler=lambda ctx: None,
|
|
236
|
+
command="/lookup",
|
|
237
|
+
keys="ctrl-l"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
self.registry.register_action(action)
|
|
241
|
+
|
|
242
|
+
# Test lookup by name
|
|
243
|
+
found = self.registry.get_action("lookup_test")
|
|
244
|
+
assert found is action
|
|
245
|
+
|
|
246
|
+
# Test lookup by command
|
|
247
|
+
found = self.registry.get_action_by_command("/lookup")
|
|
248
|
+
assert found is action
|
|
249
|
+
|
|
250
|
+
# Test lookup by keys
|
|
251
|
+
found = self.registry.get_action_by_keys("ctrl-l")
|
|
252
|
+
assert found is action
|
|
253
|
+
|
|
254
|
+
# Test not found cases
|
|
255
|
+
assert self.registry.get_action("nonexistent") is None
|
|
256
|
+
assert self.registry.get_action_by_command("/nonexistent") is None
|
|
257
|
+
assert self.registry.get_action_by_keys("nonexistent") is None
|
|
258
|
+
|
|
259
|
+
def test_execute_action(self):
|
|
260
|
+
"""Test action execution."""
|
|
261
|
+
executed = []
|
|
262
|
+
|
|
263
|
+
def test_handler(context):
|
|
264
|
+
executed.append(context.triggered_by)
|
|
265
|
+
|
|
266
|
+
action = Action(
|
|
267
|
+
name="exec_test",
|
|
268
|
+
description="Execution test",
|
|
269
|
+
category="Test",
|
|
270
|
+
handler=test_handler,
|
|
271
|
+
command="/exec"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
self.registry.register_action(action)
|
|
275
|
+
|
|
276
|
+
context = ActionContext(
|
|
277
|
+
registry=self.registry,
|
|
278
|
+
triggered_by="test"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
self.registry.execute_action("exec_test", context)
|
|
282
|
+
assert executed == ["test"]
|
|
283
|
+
|
|
284
|
+
def test_execute_nonexistent_action(self):
|
|
285
|
+
"""Test executing nonexistent action."""
|
|
286
|
+
context = ActionContext(registry=self.registry)
|
|
287
|
+
|
|
288
|
+
with pytest.raises(ActionError, match="Action 'nonexistent' not found"):
|
|
289
|
+
self.registry.execute_action("nonexistent", context)
|
|
290
|
+
|
|
291
|
+
def test_execute_disabled_action(self):
|
|
292
|
+
"""Test executing disabled action."""
|
|
293
|
+
action = Action(
|
|
294
|
+
name="disabled_test",
|
|
295
|
+
description="Disabled test",
|
|
296
|
+
category="Test",
|
|
297
|
+
handler=lambda ctx: None,
|
|
298
|
+
command="/disabled",
|
|
299
|
+
enabled=False
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
self.registry.register_action(action)
|
|
303
|
+
context = ActionContext(registry=self.registry)
|
|
304
|
+
|
|
305
|
+
# Should not raise error, but should not execute
|
|
306
|
+
self.registry.execute_action("disabled_test", context)
|
|
307
|
+
|
|
308
|
+
def test_handle_command(self):
|
|
309
|
+
"""Test command handling."""
|
|
310
|
+
executed = []
|
|
311
|
+
|
|
312
|
+
def test_handler(context):
|
|
313
|
+
executed.append(context.args)
|
|
314
|
+
|
|
315
|
+
action = Action(
|
|
316
|
+
name="cmd_test",
|
|
317
|
+
description="Command test",
|
|
318
|
+
category="Test",
|
|
319
|
+
handler=test_handler,
|
|
320
|
+
command="/cmdtest"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
self.registry.register_action(action)
|
|
324
|
+
|
|
325
|
+
self.registry.handle_command("/cmdtest arg1 arg2")
|
|
326
|
+
assert executed == [["arg1", "arg2"]]
|
|
327
|
+
|
|
328
|
+
def test_handle_unknown_command(self):
|
|
329
|
+
"""Test handling unknown command."""
|
|
330
|
+
# Should not raise error, just print message
|
|
331
|
+
self.registry.handle_command("/unknown")
|
|
332
|
+
|
|
333
|
+
def test_handle_shortcut(self):
|
|
334
|
+
"""Test shortcut handling."""
|
|
335
|
+
executed = []
|
|
336
|
+
|
|
337
|
+
def test_handler(context):
|
|
338
|
+
executed.append(context.event)
|
|
339
|
+
|
|
340
|
+
action = Action(
|
|
341
|
+
name="key_test",
|
|
342
|
+
description="Key test",
|
|
343
|
+
category="Test",
|
|
344
|
+
handler=test_handler,
|
|
345
|
+
keys="F5"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
self.registry.register_action(action)
|
|
349
|
+
|
|
350
|
+
mock_event = Mock()
|
|
351
|
+
self.registry.handle_shortcut("F5", mock_event)
|
|
352
|
+
assert executed == [mock_event]
|
|
353
|
+
|
|
354
|
+
def test_handle_unknown_shortcut(self):
|
|
355
|
+
"""Test handling unknown shortcut."""
|
|
356
|
+
# Should not raise error, just log
|
|
357
|
+
self.registry.handle_shortcut("unknown", Mock())
|
|
358
|
+
|
|
359
|
+
def test_list_methods(self):
|
|
360
|
+
"""Test list methods."""
|
|
361
|
+
actions = self.registry.list_actions()
|
|
362
|
+
assert isinstance(actions, list)
|
|
363
|
+
assert "show_help" in actions
|
|
364
|
+
|
|
365
|
+
commands = self.registry.list_commands()
|
|
366
|
+
assert isinstance(commands, list)
|
|
367
|
+
assert "/help" in commands
|
|
368
|
+
|
|
369
|
+
shortcuts = self.registry.list_shortcuts()
|
|
370
|
+
assert isinstance(shortcuts, list)
|
|
371
|
+
assert "F1" in shortcuts
|
|
372
|
+
|
|
373
|
+
def test_categories(self):
|
|
374
|
+
"""Test category organization."""
|
|
375
|
+
categories = self.registry.get_actions_by_category()
|
|
376
|
+
assert isinstance(categories, dict)
|
|
377
|
+
assert "General" in categories
|
|
378
|
+
assert len(categories["General"]) > 0
|
|
379
|
+
|
|
380
|
+
def test_builtin_help_action(self):
|
|
381
|
+
"""Test built-in help action."""
|
|
382
|
+
# Test general help
|
|
383
|
+
context = ActionContext(registry=self.registry, args=[])
|
|
384
|
+
self.registry.execute_action("show_help", context)
|
|
385
|
+
|
|
386
|
+
# Test specific help
|
|
387
|
+
context = ActionContext(registry=self.registry, args=["show_help"])
|
|
388
|
+
self.registry.execute_action("show_help", context)
|
|
389
|
+
|
|
390
|
+
# Test help for nonexistent action
|
|
391
|
+
context = ActionContext(registry=self.registry, args=["nonexistent"])
|
|
392
|
+
self.registry.execute_action("show_help", context)
|
|
393
|
+
|
|
394
|
+
def test_builtin_shortcuts_action(self):
|
|
395
|
+
"""Test built-in shortcuts listing action."""
|
|
396
|
+
context = ActionContext(registry=self.registry, args=[])
|
|
397
|
+
self.registry.execute_action("list_shortcuts", context)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class TestActionHandlerProtocol:
|
|
401
|
+
"""Test ActionHandler protocol compliance."""
|
|
402
|
+
|
|
403
|
+
def test_protocol_compliance(self):
|
|
404
|
+
"""Test that ActionRegistry implements ActionHandler protocol."""
|
|
405
|
+
from repl_toolkit.ptypes import ActionHandler
|
|
406
|
+
|
|
407
|
+
registry = ActionRegistry()
|
|
408
|
+
assert isinstance(registry, ActionHandler)
|
|
409
|
+
|
|
410
|
+
# Test protocol methods
|
|
411
|
+
assert hasattr(registry, 'execute_action')
|
|
412
|
+
assert hasattr(registry, 'handle_command')
|
|
413
|
+
assert hasattr(registry, 'validate_action')
|
|
414
|
+
assert hasattr(registry, 'list_actions')
|
|
415
|
+
|
|
416
|
+
def test_validate_action(self):
|
|
417
|
+
"""Test action validation."""
|
|
418
|
+
registry = ActionRegistry()
|
|
419
|
+
|
|
420
|
+
assert registry.validate_action("show_help") # Built-in action
|
|
421
|
+
assert not registry.validate_action("nonexistent")
|
|
422
|
+
|
|
423
|
+
def test_list_actions(self):
|
|
424
|
+
"""Test action listing."""
|
|
425
|
+
registry = ActionRegistry()
|
|
426
|
+
actions = registry.list_actions()
|
|
427
|
+
|
|
428
|
+
assert isinstance(actions, list)
|
|
429
|
+
assert len(actions) > 0
|
|
430
|
+
assert "show_help" in actions
|
|
431
|
+
|
|
432
|
+
class TestActionValidationExtended:
|
|
433
|
+
"""Additional validation tests for Action."""
|
|
434
|
+
|
|
435
|
+
def test_empty_category_validation(self):
|
|
436
|
+
"""Test that empty category raises ValueError."""
|
|
437
|
+
with pytest.raises(ValueError, match="category cannot be empty"):
|
|
438
|
+
Action(
|
|
439
|
+
name="test",
|
|
440
|
+
description="Test",
|
|
441
|
+
category="", # Empty category
|
|
442
|
+
handler=lambda ctx: None
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def test_empty_string_handler_validation(self):
|
|
446
|
+
"""Test that empty string handler raises ValueError."""
|
|
447
|
+
with pytest.raises(ValueError, match="handler cannot be empty string"):
|
|
448
|
+
Action(
|
|
449
|
+
name="test",
|
|
450
|
+
description="Test",
|
|
451
|
+
category="Test",
|
|
452
|
+
handler="" # Empty string handler
|
|
453
|
+
)
|