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,641 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: repl-toolkit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python toolkit for building interactive REPL and headless interfaces with action support
|
|
5
|
+
Author-email: REPL Toolkit Contributors <martin.j.bartlett@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bassmanitram/repl-toolkit
|
|
8
|
+
Project-URL: Documentation, https://repl-toolkit.readthedocs.io/
|
|
9
|
+
Project-URL: Repository, https://github.com/bassmanitram/repl-toolkit.git
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/bassmanitram/repl-toolkit/issues
|
|
11
|
+
Keywords: repl,cli,interactive,chat,toolkit,actions,keyboard,shortcuts
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
24
|
+
Classifier: Topic :: System :: Shells
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
29
|
+
Requires-Dist: loguru>=0.5.0
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest>=6.0; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.18.0; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-cov>=3.0.0; extra == "test"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: flake8>=4.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=0.991; extra == "dev"
|
|
39
|
+
Requires-Dist: build>=0.8.0; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# REPL Toolkit
|
|
43
|
+
|
|
44
|
+
[](https://badge.fury.io/py/repl-toolkit)
|
|
45
|
+
|
|
46
|
+
A Python toolkit for building interactive REPL and headless interfaces with support for both commands and keyboard shortcuts, featuring late backend binding for resource context scenarios.
|
|
47
|
+
|
|
48
|
+
## Key Features
|
|
49
|
+
|
|
50
|
+
### Action System
|
|
51
|
+
- **Single Definition**: One action, multiple triggers (command + shortcut)
|
|
52
|
+
- **Flexible Binding**: Command-only, shortcut-only, or both
|
|
53
|
+
- **Context Aware**: Actions know how they were triggered
|
|
54
|
+
- **Dynamic Registration**: Add actions at runtime
|
|
55
|
+
- **Category Organization**: Organize actions for better help systems
|
|
56
|
+
|
|
57
|
+
### Developer Experience
|
|
58
|
+
- **Protocol-Based**: Type-safe interfaces with runtime checking
|
|
59
|
+
- **Easy Extension**: Simple inheritance and registration patterns
|
|
60
|
+
- **Rich Help System**: Automatic help generation with usage examples
|
|
61
|
+
- **Error Handling**: Comprehensive error handling and user feedback
|
|
62
|
+
- **Async Native**: Built for modern async Python applications
|
|
63
|
+
- **Late Backend Binding**: Initialize REPL before backend is available
|
|
64
|
+
|
|
65
|
+
### Production Ready
|
|
66
|
+
- **Comprehensive Tests**: Full test coverage with pytest
|
|
67
|
+
- **Documentation**: Complete API documentation and examples
|
|
68
|
+
- **Performance**: Efficient action lookup and execution
|
|
69
|
+
- **Logging**: Structured logging with loguru integration
|
|
70
|
+
- **Headless Support**: Non-interactive mode for automation and testing
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install repl-toolkit
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Dependencies:**
|
|
79
|
+
- Python 3.8+
|
|
80
|
+
- prompt-toolkit >= 3.0.0
|
|
81
|
+
- loguru >= 0.5.0
|
|
82
|
+
|
|
83
|
+
## Quick Start
|
|
84
|
+
|
|
85
|
+
### Basic Usage
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import asyncio
|
|
89
|
+
from repl_toolkit import run_async_repl, ActionRegistry, Action
|
|
90
|
+
|
|
91
|
+
# Your backend that processes user input
|
|
92
|
+
class MyBackend:
|
|
93
|
+
async def handle_input(self, user_input: str) -> bool:
|
|
94
|
+
print(f"You said: {user_input}")
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
# Create action registry with custom actions
|
|
98
|
+
class MyActions(ActionRegistry):
|
|
99
|
+
def __init__(self):
|
|
100
|
+
super().__init__()
|
|
101
|
+
|
|
102
|
+
# Add action with both command and shortcut
|
|
103
|
+
self.register_action(
|
|
104
|
+
name="save_data",
|
|
105
|
+
description="Save current data",
|
|
106
|
+
category="File",
|
|
107
|
+
handler=self._save_data,
|
|
108
|
+
command="/save",
|
|
109
|
+
command_usage="/save [filename] - Save data to file",
|
|
110
|
+
keys="ctrl-s",
|
|
111
|
+
keys_description="Quick save"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _save_data(self, context):
|
|
115
|
+
# Access backend through context
|
|
116
|
+
backend = context.backend
|
|
117
|
+
filename = context.args[0] if context.args else "data.txt"
|
|
118
|
+
print(f"Saving to {filename}")
|
|
119
|
+
if context.triggered_by == "shortcut":
|
|
120
|
+
print(" (Triggered by Ctrl+S)")
|
|
121
|
+
|
|
122
|
+
# Run the REPL with late backend binding
|
|
123
|
+
async def main():
|
|
124
|
+
actions = MyActions()
|
|
125
|
+
backend = MyBackend()
|
|
126
|
+
|
|
127
|
+
await run_async_repl(
|
|
128
|
+
backend=backend,
|
|
129
|
+
action_registry=actions,
|
|
130
|
+
prompt_string="My App: "
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
asyncio.run(main())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Resource Context Pattern
|
|
138
|
+
|
|
139
|
+
The late backend binding pattern is useful when your backend requires resources that are only available within a specific context:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import asyncio
|
|
143
|
+
from repl_toolkit import AsyncREPL, ActionRegistry
|
|
144
|
+
|
|
145
|
+
class DatabaseBackend:
|
|
146
|
+
def __init__(self, db_connection):
|
|
147
|
+
self.db = db_connection
|
|
148
|
+
|
|
149
|
+
async def handle_input(self, user_input: str) -> bool:
|
|
150
|
+
# Use database connection
|
|
151
|
+
result = await self.db.query(user_input)
|
|
152
|
+
print(f"Query result: {result}")
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
async def main():
|
|
156
|
+
# Create REPL without backend (backend not available yet)
|
|
157
|
+
actions = ActionRegistry()
|
|
158
|
+
repl = AsyncREPL(action_registry=actions)
|
|
159
|
+
|
|
160
|
+
# Backend only available within resource context
|
|
161
|
+
async with get_database_connection() as db:
|
|
162
|
+
backend = DatabaseBackend(db)
|
|
163
|
+
# Now run REPL with backend
|
|
164
|
+
await repl.run(backend, "Database connected!")
|
|
165
|
+
|
|
166
|
+
asyncio.run(main())
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Users can now:
|
|
170
|
+
- Type `/save myfile.txt` OR press `Ctrl+S`
|
|
171
|
+
- Type `/help` OR press `F1` for help
|
|
172
|
+
- All actions work seamlessly both ways
|
|
173
|
+
|
|
174
|
+
## Core Concepts
|
|
175
|
+
|
|
176
|
+
### Actions
|
|
177
|
+
|
|
178
|
+
Actions are the heart of the extension system. Each action can be triggered by:
|
|
179
|
+
- **Commands**: Typed commands like `/help` or `/save filename`
|
|
180
|
+
- **Keyboard Shortcuts**: Key combinations like `F1` or `Ctrl+S`
|
|
181
|
+
- **Programmatic**: Direct execution in code
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from repl_toolkit import Action
|
|
185
|
+
|
|
186
|
+
# Both command and shortcut
|
|
187
|
+
action = Action(
|
|
188
|
+
name="my_action",
|
|
189
|
+
description="Does something useful",
|
|
190
|
+
category="Utilities",
|
|
191
|
+
handler=my_handler_function,
|
|
192
|
+
command="/myaction",
|
|
193
|
+
command_usage="/myaction [args] - Does something useful",
|
|
194
|
+
keys="F5",
|
|
195
|
+
keys_description="Quick action trigger"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Command-only action
|
|
199
|
+
cmd_action = Action(
|
|
200
|
+
name="command_only",
|
|
201
|
+
description="Command-only functionality",
|
|
202
|
+
category="Commands",
|
|
203
|
+
handler=cmd_handler,
|
|
204
|
+
command="/cmdonly"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Shortcut-only action
|
|
208
|
+
key_action = Action(
|
|
209
|
+
name="shortcut_only",
|
|
210
|
+
description="Keyboard shortcut",
|
|
211
|
+
category="Shortcuts",
|
|
212
|
+
handler=key_handler,
|
|
213
|
+
keys="ctrl-k",
|
|
214
|
+
keys_description="Special shortcut"
|
|
215
|
+
)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Action Registry
|
|
219
|
+
|
|
220
|
+
The `ActionRegistry` manages all actions and provides the interface between the REPL and your application logic:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from repl_toolkit import ActionRegistry
|
|
224
|
+
|
|
225
|
+
class MyRegistry(ActionRegistry):
|
|
226
|
+
def __init__(self):
|
|
227
|
+
super().__init__()
|
|
228
|
+
self._register_my_actions()
|
|
229
|
+
|
|
230
|
+
def _register_my_actions(self):
|
|
231
|
+
# Command + shortcut
|
|
232
|
+
self.register_action(
|
|
233
|
+
name="action_name",
|
|
234
|
+
description="What it does",
|
|
235
|
+
category="Category",
|
|
236
|
+
handler=self._handler_method,
|
|
237
|
+
command="/cmd",
|
|
238
|
+
keys="F2"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _handler_method(self, context):
|
|
242
|
+
# Access backend through context
|
|
243
|
+
backend = context.backend
|
|
244
|
+
if backend:
|
|
245
|
+
# Use backend
|
|
246
|
+
pass
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Action Context
|
|
250
|
+
|
|
251
|
+
Action handlers receive rich context about how they were invoked:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
def my_handler(context: ActionContext):
|
|
255
|
+
# Access the registry and backend
|
|
256
|
+
registry = context.registry
|
|
257
|
+
backend = context.backend # Available after run() is called
|
|
258
|
+
|
|
259
|
+
# Different context based on trigger method
|
|
260
|
+
if context.triggered_by == "command":
|
|
261
|
+
args = context.args # Command arguments
|
|
262
|
+
print(f"Command args: {args}")
|
|
263
|
+
|
|
264
|
+
elif context.triggered_by == "shortcut":
|
|
265
|
+
event = context.event # Keyboard event
|
|
266
|
+
print("Triggered by keyboard shortcut")
|
|
267
|
+
|
|
268
|
+
# Original user input (for commands)
|
|
269
|
+
if context.user_input:
|
|
270
|
+
print(f"Full input: {context.user_input}")
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Built-in Actions
|
|
274
|
+
|
|
275
|
+
Every registry comes with built-in actions:
|
|
276
|
+
|
|
277
|
+
| Action | Command | Shortcut | Description |
|
|
278
|
+
|--------|---------|----------|-------------|
|
|
279
|
+
| **Help** | `/help [action]` | `F1` | Show help for all actions or specific action |
|
|
280
|
+
| **Shortcuts** | `/shortcuts` | - | List all keyboard shortcuts |
|
|
281
|
+
| **Shell** | `/shell [cmd]` | - | Drop to interactive shell or run command |
|
|
282
|
+
| **Exit** | `/exit` | - | Exit the application |
|
|
283
|
+
| **Quit** | `/quit` | - | Quit the application |
|
|
284
|
+
|
|
285
|
+
## Keyboard Shortcuts
|
|
286
|
+
|
|
287
|
+
The system supports rich keyboard shortcut definitions:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
# Function keys
|
|
291
|
+
keys="F1" # F1
|
|
292
|
+
keys="F12" # F12
|
|
293
|
+
|
|
294
|
+
# Modifier combinations
|
|
295
|
+
keys="ctrl-s" # Ctrl+S
|
|
296
|
+
keys="alt-h" # Alt+H
|
|
297
|
+
keys="shift-tab" # Shift+Tab
|
|
298
|
+
|
|
299
|
+
# Complex combinations
|
|
300
|
+
keys="ctrl-alt-d" # Ctrl+Alt+D
|
|
301
|
+
|
|
302
|
+
# Multiple shortcuts for same action
|
|
303
|
+
keys=["F5", "ctrl-r"] # Either F5 OR Ctrl+R
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Headless Mode
|
|
307
|
+
|
|
308
|
+
For automation, testing, and batch processing:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
import asyncio
|
|
312
|
+
from repl_toolkit import run_headless_mode
|
|
313
|
+
|
|
314
|
+
class BatchBackend:
|
|
315
|
+
async def handle_input(self, user_input: str) -> bool:
|
|
316
|
+
# Process input without user interaction
|
|
317
|
+
result = await process_batch_input(user_input)
|
|
318
|
+
return result
|
|
319
|
+
|
|
320
|
+
async def main():
|
|
321
|
+
backend = BatchBackend()
|
|
322
|
+
|
|
323
|
+
# Process initial message, then read from stdin
|
|
324
|
+
success = await run_headless_mode(
|
|
325
|
+
backend=backend,
|
|
326
|
+
initial_message="Starting batch processing"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
return 0 if success else 1
|
|
330
|
+
|
|
331
|
+
# Usage:
|
|
332
|
+
# echo -e "Line 1\nLine 2\n/send\nLine 3" | python script.py
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Headless Features
|
|
336
|
+
|
|
337
|
+
- **stdin Processing**: Reads input line by line from stdin
|
|
338
|
+
- **Buffer Accumulation**: Content lines accumulate until `/send` command
|
|
339
|
+
- **Multiple Send Cycles**: Support for multiple `/send` operations
|
|
340
|
+
- **Command Processing**: Full action system support in headless mode
|
|
341
|
+
- **EOF Handling**: Automatically sends remaining buffer on EOF
|
|
342
|
+
|
|
343
|
+
## Architecture
|
|
344
|
+
|
|
345
|
+
### Late Backend Binding
|
|
346
|
+
|
|
347
|
+
The architecture supports late backend binding, allowing you to initialize the REPL before the backend is available:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
351
|
+
│ AsyncREPL │───▶│ ActionRegistry │ │ Your Backend │
|
|
352
|
+
│ (Interface) │ │ (Action System) │ │ (Available Later)│
|
|
353
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
354
|
+
│ │ │
|
|
355
|
+
▼ ▼ ▼
|
|
356
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
357
|
+
│ prompt_toolkit │ │ Actions │ │ Resource Context│
|
|
358
|
+
│ (Terminal) │ │ (Commands+Keys) │ │ (DB, API, etc.)│
|
|
359
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Protocol-Based Design
|
|
363
|
+
|
|
364
|
+
The toolkit uses Python protocols for type safety and flexibility:
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
from repl_toolkit.ptypes import AsyncBackend, ActionHandler
|
|
368
|
+
|
|
369
|
+
# Your backend must implement AsyncBackend
|
|
370
|
+
class MyBackend(AsyncBackend):
|
|
371
|
+
async def handle_input(self, user_input: str) -> bool:
|
|
372
|
+
# Process input, return success/failure
|
|
373
|
+
return True
|
|
374
|
+
|
|
375
|
+
# Action registries implement ActionHandler
|
|
376
|
+
class MyActions(ActionHandler):
|
|
377
|
+
def execute_action(self, action_name: str, context: ActionContext):
|
|
378
|
+
# Execute action by name
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
def handle_command(self, command_string: str):
|
|
382
|
+
# Handle command input
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
def validate_action(self, action_name: str) -> bool:
|
|
386
|
+
# Check if action exists
|
|
387
|
+
return action_name in self.actions
|
|
388
|
+
|
|
389
|
+
def list_actions(self) -> List[str]:
|
|
390
|
+
# Return available actions
|
|
391
|
+
return list(self.actions.keys())
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Examples
|
|
395
|
+
|
|
396
|
+
### Basic Example
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
# examples/basic_usage.py - Complete working example
|
|
400
|
+
import asyncio
|
|
401
|
+
from repl_toolkit import run_async_repl, ActionRegistry, Action
|
|
402
|
+
|
|
403
|
+
class EchoBackend:
|
|
404
|
+
async def handle_input(self, input: str) -> bool:
|
|
405
|
+
print(f"Echo: {input}")
|
|
406
|
+
return True
|
|
407
|
+
|
|
408
|
+
async def main():
|
|
409
|
+
backend = EchoBackend()
|
|
410
|
+
await run_async_repl(backend=backend)
|
|
411
|
+
|
|
412
|
+
asyncio.run(main())
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Advanced Example
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
# examples/advanced_usage.py - Full-featured example
|
|
419
|
+
import asyncio
|
|
420
|
+
from repl_toolkit import AsyncREPL, ActionRegistry, Action, ActionContext
|
|
421
|
+
|
|
422
|
+
class AdvancedBackend:
|
|
423
|
+
def __init__(self):
|
|
424
|
+
self.data = []
|
|
425
|
+
|
|
426
|
+
async def handle_input(self, input: str) -> bool:
|
|
427
|
+
self.data.append(input)
|
|
428
|
+
print(f"Stored: {input} (Total: {len(self.data)})")
|
|
429
|
+
return True
|
|
430
|
+
|
|
431
|
+
class AdvancedActions(ActionRegistry):
|
|
432
|
+
def __init__(self):
|
|
433
|
+
super().__init__()
|
|
434
|
+
|
|
435
|
+
# Statistics with both command and shortcut
|
|
436
|
+
self.register_action(
|
|
437
|
+
name="show_stats",
|
|
438
|
+
description="Show data statistics",
|
|
439
|
+
category="Info",
|
|
440
|
+
handler=self._show_stats,
|
|
441
|
+
command="/stats",
|
|
442
|
+
keys="F3"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def _show_stats(self, context):
|
|
446
|
+
backend = context.backend
|
|
447
|
+
count = len(backend.data) if backend else 0
|
|
448
|
+
print(f"Statistics: {count} items stored")
|
|
449
|
+
|
|
450
|
+
async def main():
|
|
451
|
+
actions = AdvancedActions()
|
|
452
|
+
backend = AdvancedBackend()
|
|
453
|
+
|
|
454
|
+
repl = AsyncREPL(action_registry=actions, prompt_string="Advanced: ")
|
|
455
|
+
await repl.run(backend)
|
|
456
|
+
|
|
457
|
+
asyncio.run(main())
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Development
|
|
461
|
+
|
|
462
|
+
### Setup Development Environment
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
git clone https://github.com/bassmanitram/repl-toolkit.git
|
|
466
|
+
cd repl-toolkit
|
|
467
|
+
pip install -e ".[dev,test]"
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Run Tests
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
pytest
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Run Tests with Coverage
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
pytest --cov=repl_toolkit --cov-report=html
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Code Formatting
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
black repl_toolkit/
|
|
486
|
+
isort repl_toolkit/
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Type Checking
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
mypy repl_toolkit/
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Testing
|
|
496
|
+
|
|
497
|
+
Run the comprehensive test suite:
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
# Install test dependencies
|
|
501
|
+
pip install pytest pytest-asyncio
|
|
502
|
+
|
|
503
|
+
# Run all tests
|
|
504
|
+
pytest
|
|
505
|
+
|
|
506
|
+
# Run with coverage
|
|
507
|
+
pytest --cov=repl_toolkit --cov-report=html
|
|
508
|
+
|
|
509
|
+
# Run specific test categories
|
|
510
|
+
pytest repl_toolkit/tests/test_actions.py # Action system tests
|
|
511
|
+
pytest repl_toolkit/tests/test_async_repl.py # REPL interface tests
|
|
512
|
+
pytest repl_toolkit/tests/test_headless.py # Headless mode tests
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Writing Tests
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
import pytest
|
|
519
|
+
from repl_toolkit import ActionRegistry, Action, ActionContext
|
|
520
|
+
|
|
521
|
+
def test_my_action():
|
|
522
|
+
# Test action execution
|
|
523
|
+
registry = ActionRegistry()
|
|
524
|
+
|
|
525
|
+
executed = []
|
|
526
|
+
def test_handler(context):
|
|
527
|
+
executed.append(context.triggered_by)
|
|
528
|
+
|
|
529
|
+
action = Action(
|
|
530
|
+
name="test",
|
|
531
|
+
description="Test action",
|
|
532
|
+
category="Test",
|
|
533
|
+
handler=test_handler,
|
|
534
|
+
command="/test"
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
registry.register_action(action)
|
|
538
|
+
|
|
539
|
+
context = ActionContext(registry=registry)
|
|
540
|
+
registry.execute_action("test", context)
|
|
541
|
+
|
|
542
|
+
assert executed == ["programmatic"]
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
## API Reference
|
|
546
|
+
|
|
547
|
+
### Core Classes
|
|
548
|
+
|
|
549
|
+
#### `AsyncREPL`
|
|
550
|
+
```python
|
|
551
|
+
class AsyncREPL:
|
|
552
|
+
def __init__(
|
|
553
|
+
self,
|
|
554
|
+
action_registry: Optional[ActionHandler] = None,
|
|
555
|
+
completer: Optional[Completer] = None,
|
|
556
|
+
prompt_string: Optional[str] = None,
|
|
557
|
+
history_path: Optional[Path] = None
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
async def run(self, backend: AsyncBackend, initial_message: Optional[str] = None)
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
#### `ActionRegistry`
|
|
564
|
+
```python
|
|
565
|
+
class ActionRegistry(ActionHandler):
|
|
566
|
+
def register_action(self, action: Action) -> None
|
|
567
|
+
def register_action(self, name, description, category, handler, command=None, keys=None, **kwargs) -> None
|
|
568
|
+
|
|
569
|
+
def execute_action(self, action_name: str, context: ActionContext) -> None
|
|
570
|
+
def handle_command(self, command_string: str, **kwargs) -> None
|
|
571
|
+
def handle_shortcut(self, key_combo: str, event: Any) -> None
|
|
572
|
+
|
|
573
|
+
def validate_action(self, action_name: str) -> bool
|
|
574
|
+
def list_actions(self) -> List[str]
|
|
575
|
+
def get_actions_by_category(self) -> Dict[str, List[Action]]
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### Convenience Functions
|
|
579
|
+
|
|
580
|
+
#### `run_async_repl()`
|
|
581
|
+
```python
|
|
582
|
+
async def run_async_repl(
|
|
583
|
+
backend: AsyncBackend,
|
|
584
|
+
action_registry: Optional[ActionHandler] = None,
|
|
585
|
+
completer: Optional[Completer] = None,
|
|
586
|
+
initial_message: Optional[str] = None,
|
|
587
|
+
prompt_string: Optional[str] = None,
|
|
588
|
+
history_path: Optional[Path] = None,
|
|
589
|
+
)
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
#### `run_headless_mode()`
|
|
593
|
+
```python
|
|
594
|
+
async def run_headless_mode(
|
|
595
|
+
backend: AsyncBackend,
|
|
596
|
+
action_registry: Optional[ActionHandler] = None,
|
|
597
|
+
initial_message: Optional[str] = None,
|
|
598
|
+
) -> bool
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Protocols
|
|
602
|
+
|
|
603
|
+
#### `AsyncBackend`
|
|
604
|
+
```python
|
|
605
|
+
class AsyncBackend(Protocol):
|
|
606
|
+
async def handle_input(self, user_input: str) -> bool: ...
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
#### `ActionHandler`
|
|
610
|
+
```python
|
|
611
|
+
class ActionHandler(Protocol):
|
|
612
|
+
def execute_action(self, action_name: str, context: ActionContext) -> None: ...
|
|
613
|
+
def handle_command(self, command_string: str, **kwargs) -> None: ...
|
|
614
|
+
def validate_action(self, action_name: str) -> bool: ...
|
|
615
|
+
def list_actions(self) -> List[str]: ...
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
## License
|
|
619
|
+
|
|
620
|
+
MIT License. See LICENSE file for details.
|
|
621
|
+
|
|
622
|
+
## Contributing
|
|
623
|
+
|
|
624
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and submit pull requests to the [main repository](https://github.com/bassmanitram/repl-toolkit).
|
|
625
|
+
|
|
626
|
+
## Links
|
|
627
|
+
|
|
628
|
+
- **GitHub Repository**: https://github.com/bassmanitram/repl-toolkit
|
|
629
|
+
- **PyPI Package**: https://pypi.org/project/repl-toolkit/
|
|
630
|
+
- **Documentation**: https://repl-toolkit.readthedocs.io/
|
|
631
|
+
- **Issue Tracker**: https://github.com/bassmanitram/repl-toolkit/issues
|
|
632
|
+
|
|
633
|
+
## Changelog
|
|
634
|
+
|
|
635
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history and changes.
|
|
636
|
+
|
|
637
|
+
## Acknowledgments
|
|
638
|
+
|
|
639
|
+
- Built on [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) for terminal handling
|
|
640
|
+
- Logging by [loguru](https://github.com/Delgan/loguru) for structured logs
|
|
641
|
+
- Inspired by modern CLI tools and REPL interfaces
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
repl_toolkit/__init__.py,sha256=BfER_QiV9WWMtJqC4h0Eu8OQDEtgKBQkwQMpA1dGfAM,1587
|
|
2
|
+
repl_toolkit/async_repl.py,sha256=qKMwq5cxC9JXXMtnRDlE_HMKwL9GEGFcas2fGMzXXPk,13489
|
|
3
|
+
repl_toolkit/headless_repl.py,sha256=nhxsdI6toYafNVI4K5vHD8yXd5hXu2Y6pOX4MCj-8fM,8664
|
|
4
|
+
repl_toolkit/ptypes.py,sha256=IyNyIYzpZcwX9LUTnt1-YC7YLqCXZOgFi20Jy3Qvf8A,3459
|
|
5
|
+
repl_toolkit/actions/__init__.py,sha256=rvlGQpKXOEHsl6uIq8C7doHUTGYygrRk7GoWX20xuoI,717
|
|
6
|
+
repl_toolkit/actions/action.py,sha256=Uh7K03y7eXC7IDfgqo9OeM5bAbtQnhAEG_um076POKc,8317
|
|
7
|
+
repl_toolkit/actions/registry.py,sha256=EkLzvBWknyRpUVN_4BJ7kBfazQhhz5-JFMPbWVAme_8,21428
|
|
8
|
+
repl_toolkit/actions/shell.py,sha256=urSbF9rvGd3xVWKMjqORKVbmKK4bBqAJZoEXUMf8_vk,2369
|
|
9
|
+
repl_toolkit/tests/__init__.py,sha256=nvR8_rcZnDFLDkw4BtwsqlFlDH1S36dkYggkXXoAA_E,103
|
|
10
|
+
repl_toolkit/tests/test_actions.py,sha256=98u723hStCRTfgP9tlJoglKbUump-moFDsy0pPnw_5s,15608
|
|
11
|
+
repl_toolkit/tests/test_async_repl.py,sha256=hV1a5jxLpU_vCprB2w2v2eAGIqK_cXeTKC_GrNp5Kmk,13938
|
|
12
|
+
repl_toolkit/tests/test_headless.py,sha256=k-tQGW0pq527SN-JHQmy9iyqT4VCg62XjAhW3JMMVsg,24415
|
|
13
|
+
repl_toolkit/tests/test_types.py,sha256=km8kL2oXIA80pGH4LHRh_o6K5fXnuTbJGZ4PHX-2u0A,6267
|
|
14
|
+
repl_toolkit-1.0.0.dist-info/licenses/LICENSE,sha256=5-7_uLibOdypJs6fRX975iZGGXah9x4TayLXryzlwiI,1081
|
|
15
|
+
repl_toolkit-1.0.0.dist-info/METADATA,sha256=1_xkGYRJsQpMO8tTbCIR_WxXZSNmlPYnMUslZRPcP0Q,18823
|
|
16
|
+
repl_toolkit-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
repl_toolkit-1.0.0.dist-info/top_level.txt,sha256=PTINMGO71j1yZwDk1ZwjGkyY1Ayh-fpnmtKFljlNxPM,13
|
|
18
|
+
repl_toolkit-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 REPL Toolkit Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
repl_toolkit
|