repl-toolkit


Namerepl-toolkit JSON
Version 1.0.0 PyPI version JSON
download
home_pageNone
SummaryA Python toolkit for building interactive REPL and headless interfaces with action support
upload_time2025-10-27 15:08:25
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseMIT
keywords repl cli interactive chat toolkit actions keyboard shortcuts
VCS
bugtrack_url
requirements prompt-toolkit loguru pytest pytest-asyncio pytest-cov black isort flake8 mypy
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # REPL Toolkit

[![PyPI version](https://badge.fury.io/py/repl-toolkit.svg)](https://badge.fury.io/py/repl-toolkit)

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.

## Key Features

### Action System
- **Single Definition**: One action, multiple triggers (command + shortcut)
- **Flexible Binding**: Command-only, shortcut-only, or both
- **Context Aware**: Actions know how they were triggered
- **Dynamic Registration**: Add actions at runtime
- **Category Organization**: Organize actions for better help systems

### Developer Experience
- **Protocol-Based**: Type-safe interfaces with runtime checking
- **Easy Extension**: Simple inheritance and registration patterns
- **Rich Help System**: Automatic help generation with usage examples
- **Error Handling**: Comprehensive error handling and user feedback
- **Async Native**: Built for modern async Python applications
- **Late Backend Binding**: Initialize REPL before backend is available

### Production Ready
- **Comprehensive Tests**: Full test coverage with pytest
- **Documentation**: Complete API documentation and examples
- **Performance**: Efficient action lookup and execution
- **Logging**: Structured logging with loguru integration
- **Headless Support**: Non-interactive mode for automation and testing

## Installation

```bash
pip install repl-toolkit
```

**Dependencies:**
- Python 3.8+
- prompt-toolkit >= 3.0.0
- loguru >= 0.5.0

## Quick Start

### Basic Usage

```python
import asyncio
from repl_toolkit import run_async_repl, ActionRegistry, Action

# Your backend that processes user input
class MyBackend:
    async def handle_input(self, user_input: str) -> bool:
        print(f"You said: {user_input}")
        return True

# Create action registry with custom actions
class MyActions(ActionRegistry):
    def __init__(self):
        super().__init__()
        
        # Add action with both command and shortcut
        self.register_action(
            name="save_data",
            description="Save current data",
            category="File",
            handler=self._save_data,
            command="/save",
            command_usage="/save [filename] - Save data to file",
            keys="ctrl-s",
            keys_description="Quick save"
        )
    
    def _save_data(self, context):
        # Access backend through context
        backend = context.backend
        filename = context.args[0] if context.args else "data.txt"
        print(f"Saving to {filename}")
        if context.triggered_by == "shortcut":
            print("   (Triggered by Ctrl+S)")

# Run the REPL with late backend binding
async def main():
    actions = MyActions()
    backend = MyBackend()
    
    await run_async_repl(
        backend=backend,
        action_registry=actions,
        prompt_string="My App: "
    )

if __name__ == "__main__":
    asyncio.run(main())
```

### Resource Context Pattern

The late backend binding pattern is useful when your backend requires resources that are only available within a specific context:

```python
import asyncio
from repl_toolkit import AsyncREPL, ActionRegistry

class DatabaseBackend:
    def __init__(self, db_connection):
        self.db = db_connection
    
    async def handle_input(self, user_input: str) -> bool:
        # Use database connection
        result = await self.db.query(user_input)
        print(f"Query result: {result}")
        return True

async def main():
    # Create REPL without backend (backend not available yet)
    actions = ActionRegistry()
    repl = AsyncREPL(action_registry=actions)
    
    # Backend only available within resource context
    async with get_database_connection() as db:
        backend = DatabaseBackend(db)
        # Now run REPL with backend
        await repl.run(backend, "Database connected!")

asyncio.run(main())
```

Users can now:
- Type `/save myfile.txt` OR press `Ctrl+S`
- Type `/help` OR press `F1` for help
- All actions work seamlessly both ways

## Core Concepts

### Actions

Actions are the heart of the extension system. Each action can be triggered by:
- **Commands**: Typed commands like `/help` or `/save filename`
- **Keyboard Shortcuts**: Key combinations like `F1` or `Ctrl+S`  
- **Programmatic**: Direct execution in code

```python
from repl_toolkit import Action

# Both command and shortcut
action = Action(
    name="my_action",
    description="Does something useful",
    category="Utilities",
    handler=my_handler_function,
    command="/myaction",
    command_usage="/myaction [args] - Does something useful",
    keys="F5",
    keys_description="Quick action trigger"
)

# Command-only action
cmd_action = Action(
    name="command_only",
    description="Command-only functionality",  
    category="Commands",
    handler=cmd_handler,
    command="/cmdonly"
)

# Shortcut-only action
key_action = Action(
    name="shortcut_only",
    description="Keyboard shortcut",
    category="Shortcuts", 
    handler=key_handler,
    keys="ctrl-k",
    keys_description="Special shortcut"
)
```

### Action Registry

The `ActionRegistry` manages all actions and provides the interface between the REPL and your application logic:

```python
from repl_toolkit import ActionRegistry

class MyRegistry(ActionRegistry):
    def __init__(self):
        super().__init__()
        self._register_my_actions()
    
    def _register_my_actions(self):
        # Command + shortcut
        self.register_action(
            name="action_name",
            description="What it does", 
            category="Category",
            handler=self._handler_method,
            command="/cmd",
            keys="F2"
        )
    
    def _handler_method(self, context):
        # Access backend through context
        backend = context.backend
        if backend:
            # Use backend
            pass
```

### Action Context

Action handlers receive rich context about how they were invoked:

```python
def my_handler(context: ActionContext):
    # Access the registry and backend
    registry = context.registry
    backend = context.backend  # Available after run() is called
    
    # Different context based on trigger method
    if context.triggered_by == "command":
        args = context.args  # Command arguments
        print(f"Command args: {args}")
        
    elif context.triggered_by == "shortcut":
        event = context.event  # Keyboard event
        print("Triggered by keyboard shortcut")
        
    # Original user input (for commands)
    if context.user_input:
        print(f"Full input: {context.user_input}")
```

## Built-in Actions

Every registry comes with built-in actions:

| Action | Command | Shortcut | Description |
|--------|---------|----------|-------------|
| **Help** | `/help [action]` | `F1` | Show help for all actions or specific action |
| **Shortcuts** | `/shortcuts` | - | List all keyboard shortcuts |  
| **Shell** | `/shell [cmd]` | - | Drop to interactive shell or run command |
| **Exit** | `/exit` | - | Exit the application |
| **Quit** | `/quit` | - | Quit the application |

## Keyboard Shortcuts

The system supports rich keyboard shortcut definitions:

```python
# Function keys
keys="F1"          # F1
keys="F12"         # F12

# Modifier combinations  
keys="ctrl-s"      # Ctrl+S
keys="alt-h"       # Alt+H
keys="shift-tab"   # Shift+Tab

# Complex combinations
keys="ctrl-alt-d"  # Ctrl+Alt+D

# Multiple shortcuts for same action
keys=["F5", "ctrl-r"]  # Either F5 OR Ctrl+R
```

## Headless Mode

For automation, testing, and batch processing:

```python
import asyncio
from repl_toolkit import run_headless_mode

class BatchBackend:
    async def handle_input(self, user_input: str) -> bool:
        # Process input without user interaction
        result = await process_batch_input(user_input)
        return result

async def main():
    backend = BatchBackend()
    
    # Process initial message, then read from stdin
    success = await run_headless_mode(
        backend=backend,
        initial_message="Starting batch processing"
    )
    
    return 0 if success else 1

# Usage:
# echo -e "Line 1\nLine 2\n/send\nLine 3" | python script.py
```

### Headless Features

- **stdin Processing**: Reads input line by line from stdin
- **Buffer Accumulation**: Content lines accumulate until `/send` command
- **Multiple Send Cycles**: Support for multiple `/send` operations
- **Command Processing**: Full action system support in headless mode
- **EOF Handling**: Automatically sends remaining buffer on EOF

## Architecture

### Late Backend Binding

The architecture supports late backend binding, allowing you to initialize the REPL before the backend is available:

```
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   AsyncREPL     │───▶│ ActionRegistry   │    │   Your Backend  │
│   (Interface)   │    │ (Action System)  │    │  (Available Later)│
└─────────────────┘    └──────────────────┘    └─────────────────┘
         │                       │                       │
         ▼                       ▼                       ▼
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│  prompt_toolkit │    │     Actions      │    │  Resource Context│
│   (Terminal)    │    │  (Commands+Keys) │    │   (DB, API, etc.)│
└─────────────────┘    └──────────────────┘    └─────────────────┘
```

### Protocol-Based Design

The toolkit uses Python protocols for type safety and flexibility:

```python
from repl_toolkit.ptypes import AsyncBackend, ActionHandler

# Your backend must implement AsyncBackend
class MyBackend(AsyncBackend):
    async def handle_input(self, user_input: str) -> bool:
        # Process input, return success/failure
        return True

# Action registries implement ActionHandler  
class MyActions(ActionHandler):
    def execute_action(self, action_name: str, context: ActionContext):
        # Execute action by name
        pass
    
    def handle_command(self, command_string: str):
        # Handle command input
        pass
    
    def validate_action(self, action_name: str) -> bool:
        # Check if action exists
        return action_name in self.actions
    
    def list_actions(self) -> List[str]:
        # Return available actions
        return list(self.actions.keys())
```

## Examples

### Basic Example

```python
# examples/basic_usage.py - Complete working example
import asyncio
from repl_toolkit import run_async_repl, ActionRegistry, Action

class EchoBackend:
    async def handle_input(self, input: str) -> bool:
        print(f"Echo: {input}")
        return True

async def main():
    backend = EchoBackend()
    await run_async_repl(backend=backend)

asyncio.run(main())
```

### Advanced Example

```python
# examples/advanced_usage.py - Full-featured example
import asyncio
from repl_toolkit import AsyncREPL, ActionRegistry, Action, ActionContext

class AdvancedBackend:
    def __init__(self):
        self.data = []
    
    async def handle_input(self, input: str) -> bool:
        self.data.append(input)
        print(f"Stored: {input} (Total: {len(self.data)})")
        return True

class AdvancedActions(ActionRegistry):
    def __init__(self):
        super().__init__()
        
        # Statistics with both command and shortcut
        self.register_action(
            name="show_stats",
            description="Show data statistics",
            category="Info", 
            handler=self._show_stats,
            command="/stats",
            keys="F3"
        )
    
    def _show_stats(self, context):
        backend = context.backend
        count = len(backend.data) if backend else 0
        print(f"Statistics: {count} items stored")

async def main():
    actions = AdvancedActions()
    backend = AdvancedBackend()
    
    repl = AsyncREPL(action_registry=actions, prompt_string="Advanced: ")
    await repl.run(backend)

asyncio.run(main())
```

## Development

### Setup Development Environment

```bash
git clone https://github.com/bassmanitram/repl-toolkit.git
cd repl-toolkit
pip install -e ".[dev,test]"
```

### Run Tests

```bash
pytest
```

### Run Tests with Coverage

```bash
pytest --cov=repl_toolkit --cov-report=html
```

### Code Formatting

```bash
black repl_toolkit/
isort repl_toolkit/
```

### Type Checking

```bash
mypy repl_toolkit/
```

## Testing

Run the comprehensive test suite:

```bash
# Install test dependencies
pip install pytest pytest-asyncio

# Run all tests
pytest

# Run with coverage
pytest --cov=repl_toolkit --cov-report=html

# Run specific test categories
pytest repl_toolkit/tests/test_actions.py     # Action system tests
pytest repl_toolkit/tests/test_async_repl.py  # REPL interface tests
pytest repl_toolkit/tests/test_headless.py    # Headless mode tests
```

### Writing Tests

```python
import pytest
from repl_toolkit import ActionRegistry, Action, ActionContext

def test_my_action():
    # Test action execution
    registry = ActionRegistry()
    
    executed = []
    def test_handler(context):
        executed.append(context.triggered_by)
    
    action = Action(
        name="test",
        description="Test action",
        category="Test",
        handler=test_handler,
        command="/test"
    )
    
    registry.register_action(action)
    
    context = ActionContext(registry=registry)
    registry.execute_action("test", context)
    
    assert executed == ["programmatic"]
```

## API Reference

### Core Classes

#### `AsyncREPL`
```python
class AsyncREPL:
    def __init__(
        self,
        action_registry: Optional[ActionHandler] = None,
        completer: Optional[Completer] = None,
        prompt_string: Optional[str] = None,
        history_path: Optional[Path] = None
    )
    
    async def run(self, backend: AsyncBackend, initial_message: Optional[str] = None)
```

#### `ActionRegistry`
```python
class ActionRegistry(ActionHandler):
    def register_action(self, action: Action) -> None
    def register_action(self, name, description, category, handler, command=None, keys=None, **kwargs) -> None
    
    def execute_action(self, action_name: str, context: ActionContext) -> None
    def handle_command(self, command_string: str, **kwargs) -> None
    def handle_shortcut(self, key_combo: str, event: Any) -> None
    
    def validate_action(self, action_name: str) -> bool
    def list_actions(self) -> List[str]
    def get_actions_by_category(self) -> Dict[str, List[Action]]
```

### Convenience Functions

#### `run_async_repl()`
```python
async def run_async_repl(
    backend: AsyncBackend,
    action_registry: Optional[ActionHandler] = None,
    completer: Optional[Completer] = None,
    initial_message: Optional[str] = None,
    prompt_string: Optional[str] = None,
    history_path: Optional[Path] = None,
)
```

#### `run_headless_mode()`
```python
async def run_headless_mode(
    backend: AsyncBackend,
    action_registry: Optional[ActionHandler] = None,
    initial_message: Optional[str] = None,
) -> bool
```

### Protocols

#### `AsyncBackend`
```python
class AsyncBackend(Protocol):
    async def handle_input(self, user_input: str) -> bool: ...
```

#### `ActionHandler`
```python
class ActionHandler(Protocol):
    def execute_action(self, action_name: str, context: ActionContext) -> None: ...
    def handle_command(self, command_string: str, **kwargs) -> None: ...
    def validate_action(self, action_name: str) -> bool: ...
    def list_actions(self) -> List[str]: ...
```

## License

MIT License. See LICENSE file for details.

## Contributing

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).

## Links

- **GitHub Repository**: https://github.com/bassmanitram/repl-toolkit
- **PyPI Package**: https://pypi.org/project/repl-toolkit/
- **Documentation**: https://repl-toolkit.readthedocs.io/
- **Issue Tracker**: https://github.com/bassmanitram/repl-toolkit/issues

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for version history and changes.

## Acknowledgments

- Built on [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) for terminal handling
- Logging by [loguru](https://github.com/Delgan/loguru) for structured logs
- Inspired by modern CLI tools and REPL interfaces

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "repl-toolkit",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "repl, cli, interactive, chat, toolkit, actions, keyboard, shortcuts",
    "author": null,
    "author_email": "REPL Toolkit Contributors <martin.j.bartlett@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/7c/6d/359b1e01b973aeed818c5c0e33dbb86e79de22d9086df4114ac209d95b11/repl_toolkit-1.0.0.tar.gz",
    "platform": null,
    "description": "# REPL Toolkit\n\n[![PyPI version](https://badge.fury.io/py/repl-toolkit.svg)](https://badge.fury.io/py/repl-toolkit)\n\nA 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.\n\n## Key Features\n\n### Action System\n- **Single Definition**: One action, multiple triggers (command + shortcut)\n- **Flexible Binding**: Command-only, shortcut-only, or both\n- **Context Aware**: Actions know how they were triggered\n- **Dynamic Registration**: Add actions at runtime\n- **Category Organization**: Organize actions for better help systems\n\n### Developer Experience\n- **Protocol-Based**: Type-safe interfaces with runtime checking\n- **Easy Extension**: Simple inheritance and registration patterns\n- **Rich Help System**: Automatic help generation with usage examples\n- **Error Handling**: Comprehensive error handling and user feedback\n- **Async Native**: Built for modern async Python applications\n- **Late Backend Binding**: Initialize REPL before backend is available\n\n### Production Ready\n- **Comprehensive Tests**: Full test coverage with pytest\n- **Documentation**: Complete API documentation and examples\n- **Performance**: Efficient action lookup and execution\n- **Logging**: Structured logging with loguru integration\n- **Headless Support**: Non-interactive mode for automation and testing\n\n## Installation\n\n```bash\npip install repl-toolkit\n```\n\n**Dependencies:**\n- Python 3.8+\n- prompt-toolkit >= 3.0.0\n- loguru >= 0.5.0\n\n## Quick Start\n\n### Basic Usage\n\n```python\nimport asyncio\nfrom repl_toolkit import run_async_repl, ActionRegistry, Action\n\n# Your backend that processes user input\nclass MyBackend:\n    async def handle_input(self, user_input: str) -> bool:\n        print(f\"You said: {user_input}\")\n        return True\n\n# Create action registry with custom actions\nclass MyActions(ActionRegistry):\n    def __init__(self):\n        super().__init__()\n        \n        # Add action with both command and shortcut\n        self.register_action(\n            name=\"save_data\",\n            description=\"Save current data\",\n            category=\"File\",\n            handler=self._save_data,\n            command=\"/save\",\n            command_usage=\"/save [filename] - Save data to file\",\n            keys=\"ctrl-s\",\n            keys_description=\"Quick save\"\n        )\n    \n    def _save_data(self, context):\n        # Access backend through context\n        backend = context.backend\n        filename = context.args[0] if context.args else \"data.txt\"\n        print(f\"Saving to {filename}\")\n        if context.triggered_by == \"shortcut\":\n            print(\"   (Triggered by Ctrl+S)\")\n\n# Run the REPL with late backend binding\nasync def main():\n    actions = MyActions()\n    backend = MyBackend()\n    \n    await run_async_repl(\n        backend=backend,\n        action_registry=actions,\n        prompt_string=\"My App: \"\n    )\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Resource Context Pattern\n\nThe late backend binding pattern is useful when your backend requires resources that are only available within a specific context:\n\n```python\nimport asyncio\nfrom repl_toolkit import AsyncREPL, ActionRegistry\n\nclass DatabaseBackend:\n    def __init__(self, db_connection):\n        self.db = db_connection\n    \n    async def handle_input(self, user_input: str) -> bool:\n        # Use database connection\n        result = await self.db.query(user_input)\n        print(f\"Query result: {result}\")\n        return True\n\nasync def main():\n    # Create REPL without backend (backend not available yet)\n    actions = ActionRegistry()\n    repl = AsyncREPL(action_registry=actions)\n    \n    # Backend only available within resource context\n    async with get_database_connection() as db:\n        backend = DatabaseBackend(db)\n        # Now run REPL with backend\n        await repl.run(backend, \"Database connected!\")\n\nasyncio.run(main())\n```\n\nUsers can now:\n- Type `/save myfile.txt` OR press `Ctrl+S`\n- Type `/help` OR press `F1` for help\n- All actions work seamlessly both ways\n\n## Core Concepts\n\n### Actions\n\nActions are the heart of the extension system. Each action can be triggered by:\n- **Commands**: Typed commands like `/help` or `/save filename`\n- **Keyboard Shortcuts**: Key combinations like `F1` or `Ctrl+S`  \n- **Programmatic**: Direct execution in code\n\n```python\nfrom repl_toolkit import Action\n\n# Both command and shortcut\naction = Action(\n    name=\"my_action\",\n    description=\"Does something useful\",\n    category=\"Utilities\",\n    handler=my_handler_function,\n    command=\"/myaction\",\n    command_usage=\"/myaction [args] - Does something useful\",\n    keys=\"F5\",\n    keys_description=\"Quick action trigger\"\n)\n\n# Command-only action\ncmd_action = Action(\n    name=\"command_only\",\n    description=\"Command-only functionality\",  \n    category=\"Commands\",\n    handler=cmd_handler,\n    command=\"/cmdonly\"\n)\n\n# Shortcut-only action\nkey_action = Action(\n    name=\"shortcut_only\",\n    description=\"Keyboard shortcut\",\n    category=\"Shortcuts\", \n    handler=key_handler,\n    keys=\"ctrl-k\",\n    keys_description=\"Special shortcut\"\n)\n```\n\n### Action Registry\n\nThe `ActionRegistry` manages all actions and provides the interface between the REPL and your application logic:\n\n```python\nfrom repl_toolkit import ActionRegistry\n\nclass MyRegistry(ActionRegistry):\n    def __init__(self):\n        super().__init__()\n        self._register_my_actions()\n    \n    def _register_my_actions(self):\n        # Command + shortcut\n        self.register_action(\n            name=\"action_name\",\n            description=\"What it does\", \n            category=\"Category\",\n            handler=self._handler_method,\n            command=\"/cmd\",\n            keys=\"F2\"\n        )\n    \n    def _handler_method(self, context):\n        # Access backend through context\n        backend = context.backend\n        if backend:\n            # Use backend\n            pass\n```\n\n### Action Context\n\nAction handlers receive rich context about how they were invoked:\n\n```python\ndef my_handler(context: ActionContext):\n    # Access the registry and backend\n    registry = context.registry\n    backend = context.backend  # Available after run() is called\n    \n    # Different context based on trigger method\n    if context.triggered_by == \"command\":\n        args = context.args  # Command arguments\n        print(f\"Command args: {args}\")\n        \n    elif context.triggered_by == \"shortcut\":\n        event = context.event  # Keyboard event\n        print(\"Triggered by keyboard shortcut\")\n        \n    # Original user input (for commands)\n    if context.user_input:\n        print(f\"Full input: {context.user_input}\")\n```\n\n## Built-in Actions\n\nEvery registry comes with built-in actions:\n\n| Action | Command | Shortcut | Description |\n|--------|---------|----------|-------------|\n| **Help** | `/help [action]` | `F1` | Show help for all actions or specific action |\n| **Shortcuts** | `/shortcuts` | - | List all keyboard shortcuts |  \n| **Shell** | `/shell [cmd]` | - | Drop to interactive shell or run command |\n| **Exit** | `/exit` | - | Exit the application |\n| **Quit** | `/quit` | - | Quit the application |\n\n## Keyboard Shortcuts\n\nThe system supports rich keyboard shortcut definitions:\n\n```python\n# Function keys\nkeys=\"F1\"          # F1\nkeys=\"F12\"         # F12\n\n# Modifier combinations  \nkeys=\"ctrl-s\"      # Ctrl+S\nkeys=\"alt-h\"       # Alt+H\nkeys=\"shift-tab\"   # Shift+Tab\n\n# Complex combinations\nkeys=\"ctrl-alt-d\"  # Ctrl+Alt+D\n\n# Multiple shortcuts for same action\nkeys=[\"F5\", \"ctrl-r\"]  # Either F5 OR Ctrl+R\n```\n\n## Headless Mode\n\nFor automation, testing, and batch processing:\n\n```python\nimport asyncio\nfrom repl_toolkit import run_headless_mode\n\nclass BatchBackend:\n    async def handle_input(self, user_input: str) -> bool:\n        # Process input without user interaction\n        result = await process_batch_input(user_input)\n        return result\n\nasync def main():\n    backend = BatchBackend()\n    \n    # Process initial message, then read from stdin\n    success = await run_headless_mode(\n        backend=backend,\n        initial_message=\"Starting batch processing\"\n    )\n    \n    return 0 if success else 1\n\n# Usage:\n# echo -e \"Line 1\\nLine 2\\n/send\\nLine 3\" | python script.py\n```\n\n### Headless Features\n\n- **stdin Processing**: Reads input line by line from stdin\n- **Buffer Accumulation**: Content lines accumulate until `/send` command\n- **Multiple Send Cycles**: Support for multiple `/send` operations\n- **Command Processing**: Full action system support in headless mode\n- **EOF Handling**: Automatically sends remaining buffer on EOF\n\n## Architecture\n\n### Late Backend Binding\n\nThe architecture supports late backend binding, allowing you to initialize the REPL before the backend is available:\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502   AsyncREPL     \u2502\u2500\u2500\u2500\u25b6\u2502 ActionRegistry   \u2502    \u2502   Your Backend  \u2502\n\u2502   (Interface)   \u2502    \u2502 (Action System)  \u2502    \u2502  (Available Later)\u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n         \u2502                       \u2502                       \u2502\n         \u25bc                       \u25bc                       \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  prompt_toolkit \u2502    \u2502     Actions      \u2502    \u2502  Resource Context\u2502\n\u2502   (Terminal)    \u2502    \u2502  (Commands+Keys) \u2502    \u2502   (DB, API, etc.)\u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n### Protocol-Based Design\n\nThe toolkit uses Python protocols for type safety and flexibility:\n\n```python\nfrom repl_toolkit.ptypes import AsyncBackend, ActionHandler\n\n# Your backend must implement AsyncBackend\nclass MyBackend(AsyncBackend):\n    async def handle_input(self, user_input: str) -> bool:\n        # Process input, return success/failure\n        return True\n\n# Action registries implement ActionHandler  \nclass MyActions(ActionHandler):\n    def execute_action(self, action_name: str, context: ActionContext):\n        # Execute action by name\n        pass\n    \n    def handle_command(self, command_string: str):\n        # Handle command input\n        pass\n    \n    def validate_action(self, action_name: str) -> bool:\n        # Check if action exists\n        return action_name in self.actions\n    \n    def list_actions(self) -> List[str]:\n        # Return available actions\n        return list(self.actions.keys())\n```\n\n## Examples\n\n### Basic Example\n\n```python\n# examples/basic_usage.py - Complete working example\nimport asyncio\nfrom repl_toolkit import run_async_repl, ActionRegistry, Action\n\nclass EchoBackend:\n    async def handle_input(self, input: str) -> bool:\n        print(f\"Echo: {input}\")\n        return True\n\nasync def main():\n    backend = EchoBackend()\n    await run_async_repl(backend=backend)\n\nasyncio.run(main())\n```\n\n### Advanced Example\n\n```python\n# examples/advanced_usage.py - Full-featured example\nimport asyncio\nfrom repl_toolkit import AsyncREPL, ActionRegistry, Action, ActionContext\n\nclass AdvancedBackend:\n    def __init__(self):\n        self.data = []\n    \n    async def handle_input(self, input: str) -> bool:\n        self.data.append(input)\n        print(f\"Stored: {input} (Total: {len(self.data)})\")\n        return True\n\nclass AdvancedActions(ActionRegistry):\n    def __init__(self):\n        super().__init__()\n        \n        # Statistics with both command and shortcut\n        self.register_action(\n            name=\"show_stats\",\n            description=\"Show data statistics\",\n            category=\"Info\", \n            handler=self._show_stats,\n            command=\"/stats\",\n            keys=\"F3\"\n        )\n    \n    def _show_stats(self, context):\n        backend = context.backend\n        count = len(backend.data) if backend else 0\n        print(f\"Statistics: {count} items stored\")\n\nasync def main():\n    actions = AdvancedActions()\n    backend = AdvancedBackend()\n    \n    repl = AsyncREPL(action_registry=actions, prompt_string=\"Advanced: \")\n    await repl.run(backend)\n\nasyncio.run(main())\n```\n\n## Development\n\n### Setup Development Environment\n\n```bash\ngit clone https://github.com/bassmanitram/repl-toolkit.git\ncd repl-toolkit\npip install -e \".[dev,test]\"\n```\n\n### Run Tests\n\n```bash\npytest\n```\n\n### Run Tests with Coverage\n\n```bash\npytest --cov=repl_toolkit --cov-report=html\n```\n\n### Code Formatting\n\n```bash\nblack repl_toolkit/\nisort repl_toolkit/\n```\n\n### Type Checking\n\n```bash\nmypy repl_toolkit/\n```\n\n## Testing\n\nRun the comprehensive test suite:\n\n```bash\n# Install test dependencies\npip install pytest pytest-asyncio\n\n# Run all tests\npytest\n\n# Run with coverage\npytest --cov=repl_toolkit --cov-report=html\n\n# Run specific test categories\npytest repl_toolkit/tests/test_actions.py     # Action system tests\npytest repl_toolkit/tests/test_async_repl.py  # REPL interface tests\npytest repl_toolkit/tests/test_headless.py    # Headless mode tests\n```\n\n### Writing Tests\n\n```python\nimport pytest\nfrom repl_toolkit import ActionRegistry, Action, ActionContext\n\ndef test_my_action():\n    # Test action execution\n    registry = ActionRegistry()\n    \n    executed = []\n    def test_handler(context):\n        executed.append(context.triggered_by)\n    \n    action = Action(\n        name=\"test\",\n        description=\"Test action\",\n        category=\"Test\",\n        handler=test_handler,\n        command=\"/test\"\n    )\n    \n    registry.register_action(action)\n    \n    context = ActionContext(registry=registry)\n    registry.execute_action(\"test\", context)\n    \n    assert executed == [\"programmatic\"]\n```\n\n## API Reference\n\n### Core Classes\n\n#### `AsyncREPL`\n```python\nclass AsyncREPL:\n    def __init__(\n        self,\n        action_registry: Optional[ActionHandler] = None,\n        completer: Optional[Completer] = None,\n        prompt_string: Optional[str] = None,\n        history_path: Optional[Path] = None\n    )\n    \n    async def run(self, backend: AsyncBackend, initial_message: Optional[str] = None)\n```\n\n#### `ActionRegistry`\n```python\nclass ActionRegistry(ActionHandler):\n    def register_action(self, action: Action) -> None\n    def register_action(self, name, description, category, handler, command=None, keys=None, **kwargs) -> None\n    \n    def execute_action(self, action_name: str, context: ActionContext) -> None\n    def handle_command(self, command_string: str, **kwargs) -> None\n    def handle_shortcut(self, key_combo: str, event: Any) -> None\n    \n    def validate_action(self, action_name: str) -> bool\n    def list_actions(self) -> List[str]\n    def get_actions_by_category(self) -> Dict[str, List[Action]]\n```\n\n### Convenience Functions\n\n#### `run_async_repl()`\n```python\nasync def run_async_repl(\n    backend: AsyncBackend,\n    action_registry: Optional[ActionHandler] = None,\n    completer: Optional[Completer] = None,\n    initial_message: Optional[str] = None,\n    prompt_string: Optional[str] = None,\n    history_path: Optional[Path] = None,\n)\n```\n\n#### `run_headless_mode()`\n```python\nasync def run_headless_mode(\n    backend: AsyncBackend,\n    action_registry: Optional[ActionHandler] = None,\n    initial_message: Optional[str] = None,\n) -> bool\n```\n\n### Protocols\n\n#### `AsyncBackend`\n```python\nclass AsyncBackend(Protocol):\n    async def handle_input(self, user_input: str) -> bool: ...\n```\n\n#### `ActionHandler`\n```python\nclass ActionHandler(Protocol):\n    def execute_action(self, action_name: str, context: ActionContext) -> None: ...\n    def handle_command(self, command_string: str, **kwargs) -> None: ...\n    def validate_action(self, action_name: str) -> bool: ...\n    def list_actions(self) -> List[str]: ...\n```\n\n## License\n\nMIT License. See LICENSE file for details.\n\n## Contributing\n\nContributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and submit pull requests to the [main repository](https://github.com/bassmanitram/repl-toolkit).\n\n## Links\n\n- **GitHub Repository**: https://github.com/bassmanitram/repl-toolkit\n- **PyPI Package**: https://pypi.org/project/repl-toolkit/\n- **Documentation**: https://repl-toolkit.readthedocs.io/\n- **Issue Tracker**: https://github.com/bassmanitram/repl-toolkit/issues\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md) for version history and changes.\n\n## Acknowledgments\n\n- Built on [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) for terminal handling\n- Logging by [loguru](https://github.com/Delgan/loguru) for structured logs\n- Inspired by modern CLI tools and REPL interfaces\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "A Python toolkit for building interactive REPL and headless interfaces with action support",
    "version": "1.0.0",
    "project_urls": {
        "Bug Tracker": "https://github.com/bassmanitram/repl-toolkit/issues",
        "Documentation": "https://repl-toolkit.readthedocs.io/",
        "Homepage": "https://github.com/bassmanitram/repl-toolkit",
        "Repository": "https://github.com/bassmanitram/repl-toolkit.git"
    },
    "split_keywords": [
        "repl",
        " cli",
        " interactive",
        " chat",
        " toolkit",
        " actions",
        " keyboard",
        " shortcuts"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "b523e244e1a88d65df90c05a3d0dce3b28e59e0eb160abb10fa97ce2661b8c34",
                "md5": "d3a35ca4d7f0f4652b84797f522bfe37",
                "sha256": "d3fca06e2c694215ab530a3851f06e8789c3f94dc2acc1e402c2a98f73f41d00"
            },
            "downloads": -1,
            "filename": "repl_toolkit-1.0.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d3a35ca4d7f0f4652b84797f522bfe37",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 35520,
            "upload_time": "2025-10-27T15:08:23",
            "upload_time_iso_8601": "2025-10-27T15:08:23.571123Z",
            "url": "https://files.pythonhosted.org/packages/b5/23/e244e1a88d65df90c05a3d0dce3b28e59e0eb160abb10fa97ce2661b8c34/repl_toolkit-1.0.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "7c6d359b1e01b973aeed818c5c0e33dbb86e79de22d9086df4114ac209d95b11",
                "md5": "f66b5be21a44c84894d8fc130e42bd2c",
                "sha256": "a9c25b0c471cb563c9fcf455f0757eee75802a75abf4aa87745b83e61b87c353"
            },
            "downloads": -1,
            "filename": "repl_toolkit-1.0.0.tar.gz",
            "has_sig": false,
            "md5_digest": "f66b5be21a44c84894d8fc130e42bd2c",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 34917,
            "upload_time": "2025-10-27T15:08:25",
            "upload_time_iso_8601": "2025-10-27T15:08:25.256536Z",
            "url": "https://files.pythonhosted.org/packages/7c/6d/359b1e01b973aeed818c5c0e33dbb86e79de22d9086df4114ac209d95b11/repl_toolkit-1.0.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-27 15:08:25",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "bassmanitram",
    "github_project": "repl-toolkit",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [
        {
            "name": "prompt-toolkit",
            "specs": [
                [
                    ">=",
                    "3.0.0"
                ]
            ]
        },
        {
            "name": "loguru",
            "specs": [
                [
                    ">=",
                    "0.5.0"
                ]
            ]
        },
        {
            "name": "pytest",
            "specs": [
                [
                    ">=",
                    "6.0"
                ]
            ]
        },
        {
            "name": "pytest-asyncio",
            "specs": [
                [
                    ">=",
                    "0.18.0"
                ]
            ]
        },
        {
            "name": "pytest-cov",
            "specs": [
                [
                    ">=",
                    "3.0.0"
                ]
            ]
        },
        {
            "name": "black",
            "specs": [
                [
                    ">=",
                    "22.0.0"
                ]
            ]
        },
        {
            "name": "isort",
            "specs": [
                [
                    ">=",
                    "5.0.0"
                ]
            ]
        },
        {
            "name": "flake8",
            "specs": [
                [
                    ">=",
                    "4.0.0"
                ]
            ]
        },
        {
            "name": "mypy",
            "specs": [
                [
                    ">=",
                    "0.991"
                ]
            ]
        }
    ],
    "lcname": "repl-toolkit"
}
        
Elapsed time: 1.47934s