pyrepl-hacks


Namepyrepl-hacks JSON
Version 0.3.0 PyPI version JSON
download
home_pageNone
SummaryHacky extensions and helper functions for the new Python REPL.
upload_time2025-11-07 22:22:20
maintainerNone
docs_urlNone
authorTrey Hunner
requires_python<3.15,>=3.13
licenseNone
keywords repl python terminal keyboard shortcuts commands
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # pyrepl-hacks 🙀

Hacky extensions and helper functions for the new Python REPL.

```python
import pyrepl_hacks as repl

repl.bind("Alt+M", "move-to-indentation")   # Move to first non-space in current line
repl.bind("Shift+Tab", "dedent")            # Dedent the whole input
repl.bind("Alt+Down", "move-line-down")     # Swap current line with next in block
repl.bind("Alt+Up", "move-line-up")         # Swap current line with previous in block
repl.bind("Shift+Home", "home")             # Move to first character in the input
repl.bind("Shift+End", "end")               # Move to last character in the input
repl.bind("Ctrl+Up", "previous-paragraph")  # Move to previous code block
repl.bind("Ctrl+Down", "next-paragraph")    # Move to next code block

# Make Ctrl+N insert a specific list of numbers
repl.bind_to_insert("Ctrl+N", "[2, 1, 3, 4, 7, 11, 18, 29]")


@repl.bind(r"Ctrl+X Ctrl+R", with_event=True)
def subprocess_run(reader, event_name, event):
    """Ctrl+X followed by Ctrl+R will insert a subprocess.run command."""
    reader.insert("import subprocess\n")
    code = 'subprocess.run("", shell=True)'
    reader.insert(code)
    for _ in range(len(code) - code.index('""') - 1):
        repl.commands.left(reader, event_name, event)
```


## ⚠ïļ WARNING: here be dragons 🐉

This library relies on Python implementation details which may change in future Python versions.

This library uses the `_pyrepl` module (and optionally `_colorize`).
As the `_` prefix implies, these modules are not designed for public use.

That means that when you upgrade to a newer Python (for example Python 3.15) this code may break.
For that reason, the Python versions this package claims to work with are pinned to only known-to-be-working Python versions.


## Installing ðŸ’ū

This package is meant to be installed in its own directory that will then be added to `sys.path` within [your `PYTHONSTARTUP` file][PYTHONSTARTUP].

For example, you could install pyrepl-hacks into a `~/.pyhacks` directory:

```console
mkdir -p ~/.pyhacks
python -m pip install pyrepl-hacks --target ~/.pyhacks
```

Then you can use it in a `~/.pythonrc` file:

```python
def _main():
    from pathlib import Path
    import sys
    sys.path.append(str(Path.home() / ".pyhacks"))
    try:
        import pyrepl_hacks as repl
    except ImportError:
        pass  # We must be on Python 3.12 or earlier
    else:
        repl.bind("Alt+M", "move-to-indentation")
        repl.bind("Shift+Tab", "dedent")
        repl.bind("Alt+Down", "move-line-down")
        repl.bind("Alt+Up", "move-line-up")
        repl.bind("Shift+Home", "home")
        repl.bind("Shift+End", "end")
        repl.bind("Ctrl+Up", "previous-paragraph")
        repl.bind("Ctrl+Down", "next-paragraph")

        repl.bind_to_insert("Ctrl+N", "[2, 1, 3, 4, 7, 11, 18, 29]")

_main()
del _main  # Don't pollute the global namespace in our REPL
```

And put this something like this in your shell configuration file (`~/.bashrc`, `~/.zshrc`, etc.):

```bash
export PYTHONSTARTUP="$HOME/.pythonrc"
```

This will modify the REPL in every Python environment you start.

See [my blog post on pyrepl-hacks](https://treyhunner.com/2025/10/handy-python-repl-modifications/) for more explanation on `PYTHONSTARTUP` files and that `sys.path.append` call above.


## Command Registering and Key Binding âŒĻïļ

This library includes features for easily registering and binding new REPL commands.

### Binding to existing commands

You can bind a key to an existing command:

```python
import pyrepl_hacks as repl

repl.bind("Shift+Home", "home")
```

### Inserting text with a binding

You can use the `bind_to_insert` helper to bind a key to insert specific text:

```python
import pyrepl_hacks as repl

repl.bind_to_insert("Ctrl+P", "Python?!")
```

### Registering new commands

Need something fancy that doesn't exist yet?

You can register a new command:

```python
import pyrepl_hacks as repl

@repl.register_command
def exit(reader):
    """Exits Python immediately."""
    import sys
    sys.exit(0)
```

The `register_command` decorator will turn the `under_score` separated name into a `kebab-case` name by default.

The `register_command` can optionally accept a command name and, if the command needs access to the event name and event object, a `with_event=True` argument can be provided:

```python
import pyrepl_hacks as repl

@repl.register_command("delete-line", with_event=True)
def delete_whole_line(reader, event_name, event):
    """Move to beginning of line and delete all text."""
    reader.pos = reader.bol()
    repl.commands.kill_line(reader, event_name, event)
```

After commands have been registered, they can be used with the `bind` function to bind them to specific keys:

```python
import pyrepl_hacks as repl

repl.bind("F4", "exit")
repl.bind("Ctrl+X Ctrl+D", "delete-line")
```

### Binding keys while registering

The `bind` function can also be used as a decorator to register a command and bind it to a specific key combination at the same time:

```python
import pyrepl_hacks as repl

@repl.bind("F4")
def exit(reader):
    """Exits Python immediately."""
    import sys
    sys.exit(0)
```

Since there's not much point in making a new command *without* binding it, you'll usually want to use `bind` instead of `register_command`.

Just like `register_command`, `bind` decorator can also accept a `with_event=True` argument to pass the event name and event object into the command function.


## Available Commands 📑

Here are some of the interesting commands provided by Python (in `_pyrepl.commands`):

- `clear-screen`: Clear screen (`Ctrl+L`)
- `previous-history`: Show previous block (`Ctrl+P`)
- `next-history`: Show next block (`Ctrl+N`)
- `accept`: Run current code block (`Alt+Enter`)
- `beginning-of-line`: Move cursor to the first character of the current line (`Ctrl+A` or `Home`)
- `end-of-line`: Move cursor to the last character of the current line (`Ctrl+E` or `End`)
- `home`: Move cursor the first character in the code block
- `end`: Move cursor the last character in the code block
- `kill-line`: Delete to end of line (`Ctrl+K`)
- `unix-line-discard`: Delete to beginning of line (`Ctrl+U`)
- `backward-word`: Move cursor back one word (`Ctrl+Left`)
- `forward-word`: Move cursor forward one word (`Ctrl+Right`)
- `backward-kill-word`:  Delete to beginning of word (`Alt+Backspace`)
- `kill-word`:  Delete to end of word (`Alt+D`)

[See here for which keyboard shortcuts these are bound to by default](https://www.pythonmorsels.com/repl-features/#keyboard-shortcuts).

This `pyrepl-hacks` project provides some additional commands as well:

- `move-to-indentation`: Move to first non-space in current line
- `dedent`: Dedent the whole code block
- `move-line-down`: Swap current line with next one in the block
- `move-line-up`: Swap current line with previous one in the block
- `previous-paragraph`: Move to the start of the previous paragraph (blank line separated block)
- `next-paragraph`: Move to the start of the next paragraph (blank line separated block)

These 6 additional commands have no key bindings by default.

I recommend binding these commands as well as the `home` and `end` commands (provided by `_pyrepl.commands`) which are also unbound by default:

```python
repl.bind("Alt+M", "move-to-indentation")   # Move to first non-space in current line
repl.bind("Shift+Tab", "dedent")            # Dedent the whole input
repl.bind("Alt+Down", "move-line-down")     # Swap current line with next in block
repl.bind("Alt+Up", "move-line-up")         # Swap current line with previous in block
repl.bind("Shift+Home", "home")             # Move to first character in the input
repl.bind("Shift+End", "end")               # Move to last character in the input
repl.bind("Ctrl+Up", "previous-paragraph")  # Move to previous code block
repl.bind("Ctrl+Down", "next-paragraph")    # Move to next code block
```

Note that these custom REPL commands and all existing commands provided by `_pyrepl.commands` include wrapper functions in the `commands` submodule.
These functions are named the same as their command name, except `-` must be replaced by `_`:

```python
from pyrepl_hacks.commands import move_to_indentation, clear_screen
```


## Customizing Your Syntax Theme ðŸŽĻ

Python 3.14 includes syntax highlighting in the REPL.

You can use the `update_theme` command from pyrepl-hacks to customize the colors your REPL uses:

```python
try:
    import pyrepl_hacks as repl

    repl.update_theme(
        keyword="green",
        builtin="blue",
        comment="intense blue",
        string="cyan",
        number="cyan",
        definition="blue",
        soft_keyword="bold green",
        op="intense green",
        reset="reset, intense green",
    )
except ImportError:
    pass  # We're on Python 3.13 or below
```

These are the supported colors:

- `black`
- `blue`
- `cyan`
- `green`
- `grey`
- `magenta`
- `red`
- `white`
- `yellow`

Each supports the modifiers `bold`, `intense`, `background` and `intense background`.

Also the "color" of `reset` will reset all modifiers.


## The Future is Obsolescence? ðŸĶĪ

This project came out of the things I learned while [hacking on my own REPL shortcuts](https://treyhunner.com/2024/10/adding-keyboard-shortcuts-to-the-python-repl/) and [customizing my REPL's syntax highlighting](https://treyhunner.com/2025/09/customizing-your-python-repl-color-scheme/).

My hope is that this package will be obsolete one day.

I hope that Python will eventually include an official interface for creating new REPL commands and binding keys to commands.

I also hope that some (or all?) of the 4 new commands this module includes will eventually be included with Python by default.


[PYTHONSTARTUP]: https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "pyrepl-hacks",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<3.15,>=3.13",
    "maintainer_email": null,
    "keywords": "repl, python, terminal, keyboard, shortcuts, commands",
    "author": "Trey Hunner",
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/54/2f/0efdfb44f4d946b27fa20332e9f67e799c7f61b95e578ba387a216f064ab/pyrepl_hacks-0.3.0.tar.gz",
    "platform": null,
    "description": "# pyrepl-hacks \ud83d\ude40\n\nHacky extensions and helper functions for the new Python REPL.\n\n```python\nimport pyrepl_hacks as repl\n\nrepl.bind(\"Alt+M\", \"move-to-indentation\")   # Move to first non-space in current line\nrepl.bind(\"Shift+Tab\", \"dedent\")            # Dedent the whole input\nrepl.bind(\"Alt+Down\", \"move-line-down\")     # Swap current line with next in block\nrepl.bind(\"Alt+Up\", \"move-line-up\")         # Swap current line with previous in block\nrepl.bind(\"Shift+Home\", \"home\")             # Move to first character in the input\nrepl.bind(\"Shift+End\", \"end\")               # Move to last character in the input\nrepl.bind(\"Ctrl+Up\", \"previous-paragraph\")  # Move to previous code block\nrepl.bind(\"Ctrl+Down\", \"next-paragraph\")    # Move to next code block\n\n# Make Ctrl+N insert a specific list of numbers\nrepl.bind_to_insert(\"Ctrl+N\", \"[2, 1, 3, 4, 7, 11, 18, 29]\")\n\n\n@repl.bind(r\"Ctrl+X Ctrl+R\", with_event=True)\ndef subprocess_run(reader, event_name, event):\n    \"\"\"Ctrl+X followed by Ctrl+R will insert a subprocess.run command.\"\"\"\n    reader.insert(\"import subprocess\\n\")\n    code = 'subprocess.run(\"\", shell=True)'\n    reader.insert(code)\n    for _ in range(len(code) - code.index('\"\"') - 1):\n        repl.commands.left(reader, event_name, event)\n```\n\n\n## \u26a0\ufe0f WARNING: here be dragons \ud83d\udc09\n\nThis library relies on Python implementation details which may change in future Python versions.\n\nThis library uses the `_pyrepl` module (and optionally `_colorize`).\nAs the `_` prefix implies, these modules are not designed for public use.\n\nThat means that when you upgrade to a newer Python (for example Python 3.15) this code may break.\nFor that reason, the Python versions this package claims to work with are pinned to only known-to-be-working Python versions.\n\n\n## Installing \ud83d\udcbe\n\nThis package is meant to be installed in its own directory that will then be added to `sys.path` within [your `PYTHONSTARTUP` file][PYTHONSTARTUP].\n\nFor example, you could install pyrepl-hacks into a `~/.pyhacks` directory:\n\n```console\nmkdir -p ~/.pyhacks\npython -m pip install pyrepl-hacks --target ~/.pyhacks\n```\n\nThen you can use it in a `~/.pythonrc` file:\n\n```python\ndef _main():\n    from pathlib import Path\n    import sys\n    sys.path.append(str(Path.home() / \".pyhacks\"))\n    try:\n        import pyrepl_hacks as repl\n    except ImportError:\n        pass  # We must be on Python 3.12 or earlier\n    else:\n        repl.bind(\"Alt+M\", \"move-to-indentation\")\n        repl.bind(\"Shift+Tab\", \"dedent\")\n        repl.bind(\"Alt+Down\", \"move-line-down\")\n        repl.bind(\"Alt+Up\", \"move-line-up\")\n        repl.bind(\"Shift+Home\", \"home\")\n        repl.bind(\"Shift+End\", \"end\")\n        repl.bind(\"Ctrl+Up\", \"previous-paragraph\")\n        repl.bind(\"Ctrl+Down\", \"next-paragraph\")\n\n        repl.bind_to_insert(\"Ctrl+N\", \"[2, 1, 3, 4, 7, 11, 18, 29]\")\n\n_main()\ndel _main  # Don't pollute the global namespace in our REPL\n```\n\nAnd put this something like this in your shell configuration file (`~/.bashrc`, `~/.zshrc`, etc.):\n\n```bash\nexport PYTHONSTARTUP=\"$HOME/.pythonrc\"\n```\n\nThis will modify the REPL in every Python environment you start.\n\nSee [my blog post on pyrepl-hacks](https://treyhunner.com/2025/10/handy-python-repl-modifications/) for more explanation on `PYTHONSTARTUP` files and that `sys.path.append` call above.\n\n\n## Command Registering and Key Binding \u2328\ufe0f\n\nThis library includes features for easily registering and binding new REPL commands.\n\n### Binding to existing commands\n\nYou can bind a key to an existing command:\n\n```python\nimport pyrepl_hacks as repl\n\nrepl.bind(\"Shift+Home\", \"home\")\n```\n\n### Inserting text with a binding\n\nYou can use the `bind_to_insert` helper to bind a key to insert specific text:\n\n```python\nimport pyrepl_hacks as repl\n\nrepl.bind_to_insert(\"Ctrl+P\", \"Python?!\")\n```\n\n### Registering new commands\n\nNeed something fancy that doesn't exist yet?\n\nYou can register a new command:\n\n```python\nimport pyrepl_hacks as repl\n\n@repl.register_command\ndef exit(reader):\n    \"\"\"Exits Python immediately.\"\"\"\n    import sys\n    sys.exit(0)\n```\n\nThe `register_command` decorator will turn the `under_score` separated name into a `kebab-case` name by default.\n\nThe `register_command` can optionally accept a command name and, if the command needs access to the event name and event object, a `with_event=True` argument can be provided:\n\n```python\nimport pyrepl_hacks as repl\n\n@repl.register_command(\"delete-line\", with_event=True)\ndef delete_whole_line(reader, event_name, event):\n    \"\"\"Move to beginning of line and delete all text.\"\"\"\n    reader.pos = reader.bol()\n    repl.commands.kill_line(reader, event_name, event)\n```\n\nAfter commands have been registered, they can be used with the `bind` function to bind them to specific keys:\n\n```python\nimport pyrepl_hacks as repl\n\nrepl.bind(\"F4\", \"exit\")\nrepl.bind(\"Ctrl+X Ctrl+D\", \"delete-line\")\n```\n\n### Binding keys while registering\n\nThe `bind` function can also be used as a decorator to register a command and bind it to a specific key combination at the same time:\n\n```python\nimport pyrepl_hacks as repl\n\n@repl.bind(\"F4\")\ndef exit(reader):\n    \"\"\"Exits Python immediately.\"\"\"\n    import sys\n    sys.exit(0)\n```\n\nSince there's not much point in making a new command *without* binding it, you'll usually want to use `bind` instead of `register_command`.\n\nJust like `register_command`, `bind` decorator can also accept a `with_event=True` argument to pass the event name and event object into the command function.\n\n\n## Available Commands \ud83d\udcd1\n\nHere are some of the interesting commands provided by Python (in `_pyrepl.commands`):\n\n- `clear-screen`: Clear screen (`Ctrl+L`)\n- `previous-history`: Show previous block (`Ctrl+P`)\n- `next-history`: Show next block (`Ctrl+N`)\n- `accept`: Run current code block (`Alt+Enter`)\n- `beginning-of-line`: Move cursor to the first character of the current line (`Ctrl+A` or `Home`)\n- `end-of-line`: Move cursor to the last character of the current line (`Ctrl+E` or `End`)\n- `home`: Move cursor the first character in the code block\n- `end`: Move cursor the last character in the code block\n- `kill-line`: Delete to end of line (`Ctrl+K`)\n- `unix-line-discard`: Delete to beginning of line (`Ctrl+U`)\n- `backward-word`: Move cursor back one word (`Ctrl+Left`)\n- `forward-word`: Move cursor forward one word (`Ctrl+Right`)\n- `backward-kill-word`:  Delete to beginning of word (`Alt+Backspace`)\n- `kill-word`:  Delete to end of word (`Alt+D`)\n\n[See here for which keyboard shortcuts these are bound to by default](https://www.pythonmorsels.com/repl-features/#keyboard-shortcuts).\n\nThis `pyrepl-hacks` project provides some additional commands as well:\n\n- `move-to-indentation`: Move to first non-space in current line\n- `dedent`: Dedent the whole code block\n- `move-line-down`: Swap current line with next one in the block\n- `move-line-up`: Swap current line with previous one in the block\n- `previous-paragraph`: Move to the start of the previous paragraph (blank line separated block)\n- `next-paragraph`: Move to the start of the next paragraph (blank line separated block)\n\nThese 6 additional commands have no key bindings by default.\n\nI recommend binding these commands as well as the `home` and `end` commands (provided by `_pyrepl.commands`) which are also unbound by default:\n\n```python\nrepl.bind(\"Alt+M\", \"move-to-indentation\")   # Move to first non-space in current line\nrepl.bind(\"Shift+Tab\", \"dedent\")            # Dedent the whole input\nrepl.bind(\"Alt+Down\", \"move-line-down\")     # Swap current line with next in block\nrepl.bind(\"Alt+Up\", \"move-line-up\")         # Swap current line with previous in block\nrepl.bind(\"Shift+Home\", \"home\")             # Move to first character in the input\nrepl.bind(\"Shift+End\", \"end\")               # Move to last character in the input\nrepl.bind(\"Ctrl+Up\", \"previous-paragraph\")  # Move to previous code block\nrepl.bind(\"Ctrl+Down\", \"next-paragraph\")    # Move to next code block\n```\n\nNote that these custom REPL commands and all existing commands provided by `_pyrepl.commands` include wrapper functions in the `commands` submodule.\nThese functions are named the same as their command name, except `-` must be replaced by `_`:\n\n```python\nfrom pyrepl_hacks.commands import move_to_indentation, clear_screen\n```\n\n\n## Customizing Your Syntax Theme \ud83c\udfa8\n\nPython 3.14 includes syntax highlighting in the REPL.\n\nYou can use the `update_theme` command from pyrepl-hacks to customize the colors your REPL uses:\n\n```python\ntry:\n    import pyrepl_hacks as repl\n\n    repl.update_theme(\n        keyword=\"green\",\n        builtin=\"blue\",\n        comment=\"intense blue\",\n        string=\"cyan\",\n        number=\"cyan\",\n        definition=\"blue\",\n        soft_keyword=\"bold green\",\n        op=\"intense green\",\n        reset=\"reset, intense green\",\n    )\nexcept ImportError:\n    pass  # We're on Python 3.13 or below\n```\n\nThese are the supported colors:\n\n- `black`\n- `blue`\n- `cyan`\n- `green`\n- `grey`\n- `magenta`\n- `red`\n- `white`\n- `yellow`\n\nEach supports the modifiers `bold`, `intense`, `background` and `intense background`.\n\nAlso the \"color\" of `reset` will reset all modifiers.\n\n\n## The Future is Obsolescence? \ud83e\udda4\n\nThis project came out of the things I learned while [hacking on my own REPL shortcuts](https://treyhunner.com/2024/10/adding-keyboard-shortcuts-to-the-python-repl/) and [customizing my REPL's syntax highlighting](https://treyhunner.com/2025/09/customizing-your-python-repl-color-scheme/).\n\nMy hope is that this package will be obsolete one day.\n\nI hope that Python will eventually include an official interface for creating new REPL commands and binding keys to commands.\n\nI also hope that some (or all?) of the 4 new commands this module includes will eventually be included with Python by default.\n\n\n[PYTHONSTARTUP]: https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Hacky extensions and helper functions for the new Python REPL.",
    "version": "0.3.0",
    "project_urls": {
        "Documentation": "https://github.com/treyhunner/pyrepl-hacks#readme",
        "Issues": "https://github.com/treyhunner/pyrepl-hacks/issues",
        "Source": "https://github.com/treyhunner/pyrepl-hacks"
    },
    "split_keywords": [
        "repl",
        " python",
        " terminal",
        " keyboard",
        " shortcuts",
        " commands"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "266d859f709a1aeb314d98bb2761c43c09287ae5e7e040c355a9aeb7530904a1",
                "md5": "cf20f6b6e155e917b92d3978666f11aa",
                "sha256": "4ca85200b46db0f640e44d1bc3887a6a3d3c3f2519f2915065847341aca19777"
            },
            "downloads": -1,
            "filename": "pyrepl_hacks-0.3.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "cf20f6b6e155e917b92d3978666f11aa",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<3.15,>=3.13",
            "size": 14274,
            "upload_time": "2025-11-07T22:22:19",
            "upload_time_iso_8601": "2025-11-07T22:22:19.803586Z",
            "url": "https://files.pythonhosted.org/packages/26/6d/859f709a1aeb314d98bb2761c43c09287ae5e7e040c355a9aeb7530904a1/pyrepl_hacks-0.3.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "542f0efdfb44f4d946b27fa20332e9f67e799c7f61b95e578ba387a216f064ab",
                "md5": "84a1bcd4acd23634ff8794f55ed27ef0",
                "sha256": "c45b8b1c4595d194e8b93898d2c36ca5f08f51792d21a366f9ac4066db31c49a"
            },
            "downloads": -1,
            "filename": "pyrepl_hacks-0.3.0.tar.gz",
            "has_sig": false,
            "md5_digest": "84a1bcd4acd23634ff8794f55ed27ef0",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<3.15,>=3.13",
            "size": 21549,
            "upload_time": "2025-11-07T22:22:20",
            "upload_time_iso_8601": "2025-11-07T22:22:20.986105Z",
            "url": "https://files.pythonhosted.org/packages/54/2f/0efdfb44f4d946b27fa20332e9f67e799c7f61b95e578ba387a216f064ab/pyrepl_hacks-0.3.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-11-07 22:22:20",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "treyhunner",
    "github_project": "pyrepl-hacks#readme",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "pyrepl-hacks"
}
        
Elapsed time: 4.96529s