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/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
REPL Toolkit - A Python toolkit for building interactive REPL and headless interfaces.
|
|
3
|
+
|
|
4
|
+
This package provides tools for creating interactive command-line interfaces
|
|
5
|
+
with support for both commands and keyboard shortcuts, featuring late backend
|
|
6
|
+
binding for resource context scenarios.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
- Action system with commands and keyboard shortcuts
|
|
10
|
+
- Late backend binding for resource contexts
|
|
11
|
+
- Protocol-based architecture for type safety
|
|
12
|
+
- Comprehensive test coverage
|
|
13
|
+
- Async-native design
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> import asyncio
|
|
17
|
+
>>> from repl_toolkit import run_async_repl, ActionRegistry
|
|
18
|
+
>>>
|
|
19
|
+
>>> class MyBackend:
|
|
20
|
+
... async def handle_input(self, user_input: str) -> bool:
|
|
21
|
+
... print(f"You said: {user_input}")
|
|
22
|
+
... return True
|
|
23
|
+
>>>
|
|
24
|
+
>>> async def main():
|
|
25
|
+
... backend = MyBackend()
|
|
26
|
+
... await run_async_repl(backend=backend)
|
|
27
|
+
>>>
|
|
28
|
+
>>> # asyncio.run(main())
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
__version__ = "1.0.0"
|
|
32
|
+
__author__ = "REPL Toolkit Contributors"
|
|
33
|
+
__license__ = "MIT"
|
|
34
|
+
|
|
35
|
+
# Core exports
|
|
36
|
+
from .async_repl import AsyncREPL, run_async_repl
|
|
37
|
+
from .headless_repl import run_headless_mode, HeadlessREPL
|
|
38
|
+
from .actions import ActionRegistry, Action, ActionContext, ActionError
|
|
39
|
+
from .ptypes import AsyncBackend, ActionHandler, Completer
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# Core classes
|
|
43
|
+
"AsyncREPL",
|
|
44
|
+
"HeadlessREPL",
|
|
45
|
+
"ActionRegistry",
|
|
46
|
+
"Action",
|
|
47
|
+
"ActionContext",
|
|
48
|
+
"ActionError",
|
|
49
|
+
|
|
50
|
+
# Convenience functions
|
|
51
|
+
"run_async_repl",
|
|
52
|
+
"run_headless_mode",
|
|
53
|
+
|
|
54
|
+
# Protocols/Types
|
|
55
|
+
"AsyncBackend",
|
|
56
|
+
"ActionHandler",
|
|
57
|
+
"Completer",
|
|
58
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Action system for repl_toolkit.
|
|
3
|
+
|
|
4
|
+
This module provides the action architecture that combines command
|
|
5
|
+
and keyboard shortcut handling into a single, extensible framework.
|
|
6
|
+
|
|
7
|
+
The action system allows developers to define actions that can be triggered
|
|
8
|
+
by either typed commands (e.g., /help) or keyboard shortcuts (e.g., F1),
|
|
9
|
+
providing a consistent and discoverable interface for users.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .action import Action, ActionContext, ActionError, ActionValidationError, ActionExecutionError
|
|
13
|
+
from .registry import ActionRegistry
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
# Core action types
|
|
17
|
+
"Action",
|
|
18
|
+
"ActionContext",
|
|
19
|
+
"ActionError",
|
|
20
|
+
"ActionValidationError",
|
|
21
|
+
"ActionExecutionError",
|
|
22
|
+
|
|
23
|
+
# Registry
|
|
24
|
+
"ActionRegistry",
|
|
25
|
+
]
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core action definition and context for the action system.
|
|
3
|
+
|
|
4
|
+
This module defines the Action dataclass and ActionContext that form the
|
|
5
|
+
foundation of the command and keyboard shortcut system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Callable, List, Optional, Union, TYPE_CHECKING
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .registry import ActionRegistry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Action:
|
|
18
|
+
"""
|
|
19
|
+
Action definition that can be triggered by commands or keyboard shortcuts.
|
|
20
|
+
|
|
21
|
+
Actions provide a way to define functionality that can be accessed
|
|
22
|
+
through multiple interaction methods (commands, shortcuts) while maintaining
|
|
23
|
+
consistent behavior and documentation.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
# Action with both command and shortcut
|
|
27
|
+
help_action = Action(
|
|
28
|
+
name="show_help",
|
|
29
|
+
description="Show help information",
|
|
30
|
+
category="General",
|
|
31
|
+
handler=show_help_function,
|
|
32
|
+
command="/help",
|
|
33
|
+
command_usage="/help [command] - Show help for all or specific command",
|
|
34
|
+
keys="F1",
|
|
35
|
+
keys_description="Show help"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Command-only action
|
|
39
|
+
history_action = Action(
|
|
40
|
+
name="show_history",
|
|
41
|
+
description="Display conversation history",
|
|
42
|
+
category="Information",
|
|
43
|
+
handler=show_history_function,
|
|
44
|
+
command="/history",
|
|
45
|
+
command_usage="/history - Display message history"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Shortcut-only action
|
|
49
|
+
save_action = Action(
|
|
50
|
+
name="quick_save",
|
|
51
|
+
description="Quick save current state",
|
|
52
|
+
category="File",
|
|
53
|
+
handler=quick_save_function,
|
|
54
|
+
keys="ctrl-s",
|
|
55
|
+
keys_description="Quick save"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Main-loop action (handled externally)
|
|
59
|
+
exit_action = Action(
|
|
60
|
+
name="exit_repl",
|
|
61
|
+
description="Exit the application",
|
|
62
|
+
category="Control",
|
|
63
|
+
handler=None, # Handled by main loop
|
|
64
|
+
command="/exit",
|
|
65
|
+
command_usage="/exit - Exit the application"
|
|
66
|
+
)
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Core action definition
|
|
70
|
+
name: str # Unique action identifier
|
|
71
|
+
description: str # Human-readable description
|
|
72
|
+
category: str # Category for grouping/organization
|
|
73
|
+
handler: Optional[Union[Callable, str]] # Handler function, import path, or None for main-loop
|
|
74
|
+
|
|
75
|
+
# Command binding (optional)
|
|
76
|
+
command: Optional[str] = None # Command string (e.g., "/help")
|
|
77
|
+
command_args_description: Optional[str] = None # Description of command arguments
|
|
78
|
+
command_usage: Optional[str] = None # Full usage description
|
|
79
|
+
|
|
80
|
+
# Keyboard shortcut binding (optional)
|
|
81
|
+
keys: Optional[Union[str, List[str]]] = None # Key combination(s)
|
|
82
|
+
keys_description: Optional[str] = None # Shortcut description for help
|
|
83
|
+
|
|
84
|
+
# Metadata and control
|
|
85
|
+
enabled: bool = True # Whether action is currently enabled
|
|
86
|
+
context: Optional[str] = None # Context where action is available
|
|
87
|
+
requires_backend: bool = False # Whether action needs backend access
|
|
88
|
+
hidden: bool = False # Hide from help listings
|
|
89
|
+
|
|
90
|
+
def __post_init__(self):
|
|
91
|
+
"""Validate action definition after initialization."""
|
|
92
|
+
logger.trace("Action.__post_init__() entry")
|
|
93
|
+
|
|
94
|
+
if not self.name:
|
|
95
|
+
raise ValueError("Action name cannot be empty")
|
|
96
|
+
|
|
97
|
+
if not self.description:
|
|
98
|
+
raise ValueError("Action description cannot be empty")
|
|
99
|
+
|
|
100
|
+
if not self.category:
|
|
101
|
+
raise ValueError("Action category cannot be empty")
|
|
102
|
+
|
|
103
|
+
# Handler can be None for main-loop actions like exit/quit
|
|
104
|
+
# but if provided, it cannot be empty string
|
|
105
|
+
if self.handler == "":
|
|
106
|
+
raise ValueError("Action handler cannot be empty string (use None for main-loop actions)")
|
|
107
|
+
|
|
108
|
+
if not self.command and not self.keys:
|
|
109
|
+
raise ValueError("Action must have either command or keys binding")
|
|
110
|
+
|
|
111
|
+
# Validate command format
|
|
112
|
+
if self.command and not self.command.startswith('/'):
|
|
113
|
+
raise ValueError(f"Command '{self.command}' must start with '/'")
|
|
114
|
+
|
|
115
|
+
# Ensure command usage is provided for commands
|
|
116
|
+
if self.command and not self.command_usage:
|
|
117
|
+
logger.warning(f"Action '{self.name}' has command but no usage description")
|
|
118
|
+
|
|
119
|
+
# Ensure keys description is provided for shortcuts
|
|
120
|
+
if self.keys and not self.keys_description:
|
|
121
|
+
logger.warning(f"Action '{self.name}' has keys but no keys description")
|
|
122
|
+
|
|
123
|
+
logger.trace("Action.__post_init__() exit")
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def has_command(self) -> bool:
|
|
127
|
+
"""Check if action has a command binding."""
|
|
128
|
+
logger.trace("Action.has_command() entry/exit")
|
|
129
|
+
return self.command is not None
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def has_shortcut(self) -> bool:
|
|
133
|
+
"""Check if action has a keyboard shortcut binding."""
|
|
134
|
+
logger.trace("Action.has_shortcut() entry/exit")
|
|
135
|
+
return self.keys is not None
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def is_main_loop_action(self) -> bool:
|
|
139
|
+
"""Check if action is handled by the main loop (handler is None)."""
|
|
140
|
+
logger.trace("Action.is_main_loop_action() entry/exit")
|
|
141
|
+
return self.handler is None
|
|
142
|
+
|
|
143
|
+
def get_keys_list(self) -> List[str]:
|
|
144
|
+
"""Get keys as a list, handling both string and list formats."""
|
|
145
|
+
logger.trace("Action.get_keys_list() entry")
|
|
146
|
+
|
|
147
|
+
if not self.keys:
|
|
148
|
+
logger.trace("Action.get_keys_list() exit - no keys")
|
|
149
|
+
return []
|
|
150
|
+
|
|
151
|
+
result = [self.keys] if isinstance(self.keys, str) else self.keys
|
|
152
|
+
logger.trace("Action.get_keys_list() exit")
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class ActionContext:
|
|
158
|
+
"""
|
|
159
|
+
Context information passed to action handlers.
|
|
160
|
+
|
|
161
|
+
ActionContext provides handlers with access to the registry, backend,
|
|
162
|
+
and context-specific information needed to execute actions properly.
|
|
163
|
+
|
|
164
|
+
The context varies depending on how the action was triggered:
|
|
165
|
+
- Command: args contains parsed command arguments
|
|
166
|
+
- Keyboard: event contains the key press event
|
|
167
|
+
- Programmatic: context can be customized
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
registry: "ActionRegistry" # Reference to action registry
|
|
171
|
+
backend: Optional[Any] = None # Backend instance (if available)
|
|
172
|
+
event: Optional[Any] = None # KeyPress event for shortcuts
|
|
173
|
+
args: List[str] = field(default_factory=list) # Command arguments
|
|
174
|
+
triggered_by: str = "unknown" # How action was triggered
|
|
175
|
+
user_input: Optional[str] = None # Original user input
|
|
176
|
+
headless_mode: bool = False # Whether in headless mode
|
|
177
|
+
buffer: Optional[Any] = None # Reference to input buffer (if applicable)
|
|
178
|
+
|
|
179
|
+
def __post_init__(self):
|
|
180
|
+
"""Set triggered_by based on available context."""
|
|
181
|
+
logger.trace("ActionContext.__post_init__() entry")
|
|
182
|
+
|
|
183
|
+
if self.triggered_by == "unknown":
|
|
184
|
+
if self.event is not None:
|
|
185
|
+
self.triggered_by = "shortcut"
|
|
186
|
+
elif self.args or self.user_input:
|
|
187
|
+
self.triggered_by = "command"
|
|
188
|
+
else:
|
|
189
|
+
self.triggered_by = "programmatic"
|
|
190
|
+
|
|
191
|
+
logger.trace("ActionContext.__post_init__() exit")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ActionError(Exception):
|
|
195
|
+
"""Base exception for action-related errors."""
|
|
196
|
+
|
|
197
|
+
def __init__(self, message: str, action_name: Optional[str] = None):
|
|
198
|
+
"""
|
|
199
|
+
Initialize action error.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
message: Error description
|
|
203
|
+
action_name: Name of action that caused error (optional)
|
|
204
|
+
"""
|
|
205
|
+
logger.trace("ActionError.__init__() entry/exit")
|
|
206
|
+
super().__init__(message)
|
|
207
|
+
self.action_name = action_name
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class ActionValidationError(ActionError):
|
|
211
|
+
"""Exception raised when action validation fails."""
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class ActionExecutionError(ActionError):
|
|
216
|
+
"""Exception raised when action execution fails."""
|
|
217
|
+
pass
|