repl-toolkit 1.0.0__tar.gz

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,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,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
+ [![PyPI version](https://badge.fury.io/py/repl-toolkit.svg)](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