Skip to main content

django-pyrepl-hacks 🐍

PyPI CI Coverage

A Django shell that uses the new Python REPL, with pyrepl-hacks key bindings.

This package teaches manage.py shell about the new Python REPL (3.13+), adds some additional key bindings to the new REPL, and adds utilities for customizing the REPL.

Supported Python versions 📌

This builds on pyrepl-hacks, which uses Python's internal _pyrepl module. That module is private, so this package supports only the Python versions it has been tested against, and requires-python says which those are. A newer Python will refuse to install it until a release here widens that.

If the new REPL cannot run, the pyrepl interface steps aside and Django moves on to IPython, bpython, or code.interact, exactly as it would if this package were not installed.

Installing 💾

This needs Python 3.13, 3.14, or 3.15 and Django 5.2 or later.

Install with uv:

uv add django-pyrepl-hacks

Or with pip:

python -m pip install django-pyrepl-hacks

Then add it to INSTALLED_APPS:

INSTALLED_APPS = [
    "django_pyrepl_hacks",
    # ...
]

Now manage.py shell will use the new REPL:

$ ./manage.py shell
14 objects imported automatically (use -v 2 for details).

>>>

Default key bindings ⌨️

Key Command What it does
Home home Move to the first character in the input
End end Move to the last character in the input
Alt+M move-to-indentation Move to the first non-space in the line
Shift+Tab dedent Dedent the whole input
Alt+Down move-line-down Swap the current line with the next one
Alt+Up move-line-up Swap the current line with the previous one
Ctrl+Up previous-history Move to the previous history entry
Ctrl+Down next-history Move to the next history entry
Alt+{ previous-paragraph Move to the previous blank line
Alt+} next-paragraph Move to the next blank line

To see what is actually bound in your project, including your own additions:

./manage.py shell --show-bindings

Settings ⚙️

Every setting is optional.

PYREPL_BINDINGS

A dictionary mapping a key to the thing that key should do, layered over the defaults above.

The keys should be human-readable key bindings: "Ctrl+K", "Alt+Up", "Shift+Tab", "F4", "Home", "PageUp", or a sequence like "Ctrl+X Ctrl+R".

The values must be either:

  1. A string representing the name of an installed _pyrepl command
  2. A function, representing a new _pyrepl command
  3. django_pyrepl_hacks.insert("some text") to insert text

Using an existing _pyrepl command

There are about 50 built-in key commands to _pyrepl: show-history, paste-mode, transpose-characters, yank, yank-pop, unix-word-rubout, backward-word, forward-word, and history-search-backward are some of the more useful ones.

pyrepl-hacks adds dedent, move-line-up, move-line-down, move-to-indentation, previous-paragraph, and next-paragraph.

These are the default key bindings (you don't need to set these):

PYREPL_BINDINGS = {
    "Home": "home",
    "End": "end",
    "Alt+M": "move-to-indentation",
    "Shift+Tab": "dedent",
    "Alt+Down": "move-line-down",
    "Alt+Up": "move-line-up",
    "Ctrl+Up": "previous-history",
    "Ctrl+Down": "next-history",
    "Alt+{": "previous-paragraph",
    "Alt+}": "next-paragraph",
}

Note that by default, the home and end keys will move to the first character and the last character in the current code block, which is different from their default behavior in the REPL.

Defining a new command

Setting a PYREPL_BINDINGS value to a function object will register that function as a _pyrepl new command. The new command will be named after the function, with underscores replaced by hyphens, so the sql_of_last_query function below will register as a command named sql-of-last-query.

Functions are called with the pyrepl's reader object, which holds the text you are editing:

# myproject/repl_extensions.py
def sql_of_last_query(reader):
    """Type out the SQL of the most recent query."""
    from django.db import connection

    if connection.queries:
        reader.insert(connection.queries[-1]["sql"])
from myproject.repl_extensions import sql_of_last_query

PYREPL_BINDINGS = {"F9": sql_of_last_query}

A function taking three arguments is called with the event too (just as pyrepl-hacks commands with with_event=True are). This is needed to move the cursor, since movement commands require the event object:

import pyrepl_hacks as repl


def filter_call(reader, event_name, event):
    """Type `User.objects.filter()` and park the cursor inside the parens."""
    reader.insert("User.objects.filter()")
    repl.commands.left(reader, event_name, event)

Lambda functions are not allowed.

Inserting text

Here are two example bindings that insert text:

from django_pyrepl_hacks import insert

PYREPL_BINDINGS = {
    "Ctrl+F": insert("User.objects.filter("),
    "F9": insert("connection.queries[-1]['sql']"),
}

Hitting Ctrl+F will insert User.objects.filter( and hitting F9 will insert connection.queries[-1]['sql'].

Disabling a key binding

Using None as the value to a PYREPL_BINDINGS item will reset that binding back to its _pyrepl default.

If you would prefer the Home and End keys had their default behaviors (moving to the beginning/end of a line instead of the whole block) you could do this:

PYREPL_BINDINGS = {
    "Home": None,
    "End": None,
}

PYREPL_USE_DEFAULT_BINDINGS

Set to False to turn off the default key bindings listed above. The pyrepl-hacks commands they point at are still registered, so you can bind your own keys to them.

PYREPL_THEME

Use this to customize the syntax highlighting colors for the code you type at the REPL.

The keys are the eleven token types that _pyrepl knows about, and the values are colors:

PYREPL_THEME = {
    "string": "green",
    "number": "intense blue",
    "comment": "grey",
    "keyword": "bold magenta",
    "prompt": "bold green",
}

The token types:

Token What it colors
prompt The >>> and ... prompts
keyword def, if, for, import, return
keyword_constant None, True, False
soft_keyword match, case, type
builtin len, print, sorted
comment # like this
string "like this"
number 42, 3.14
op +, -, =, (, ,
definition The name in def name or class Name
reset Everything else, and the default style

The colors are:

Color bold intense background intense background
black yes yes yes yes
blue yes yes yes yes
cyan yes yes yes yes
green yes yes yes yes
grey no no no no
magenta yes yes yes yes
red yes yes yes yes
white yes yes yes yes
yellow yes yes yes yes

Every color works on its own. grey is the exception to the prefixes below: there is no bold grey, and asking for one is an error.

The prefixes are:

Specification Example
plain "red"
bold "bold red"
intense "intense red"
background "background red"
intense background "intense background red"
combined with a comma "background black, bold white"
nothing at all "reset"

Here's a theme that works well with the Solarized Light theme I use in my Terminal:

PYREPL_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",
}

Any value you leave out keeps the REPL's default color.

Named themes (PYREPL_THEME = "solarized-light") are not (yet) implemented.

This setting needs Python 3.14 or later. On an older Python it is ignored and the REPL keeps its own colors, so a project running on both versions can set it once without breaking the shell for anyone. manage.py check warns that it is doing nothing there.

PYREPL_SETUP

This is an escape hatch: a place to put code that should run when the REPL starts and nowhere else.

This is the import path of a function, called once the REPL is configured and just before it starts:

PYREPL_SETUP = "myproject.repl.setup"

An import path rather than the function itself, because settings.py is read by every process you run and this is code only the REPL needs. To do more than one thing, call them from that one function.

Below are some example uses for that function that PYREPL_SETUP points to.

Indicating the environment in your prompt

Here the management shell prompt would become prod>>> and prod... when settings.DEBUG isn't True:

def setup():
    import sys
    from django.conf import settings

    if not settings.DEBUG:
        sys.ps1, sys.ps2 = "prod>>> ", "prod... "

A custom banner

To print something each time the REPL launches:

def setup():
    import django
    from django.db import connections

    database = connections["default"].settings_dict["NAME"]
    print(f"Django {django.get_version()} | {database}")

Checks ✅

manage.py check validates the PYREPL_* settings it can, so a mistake turns up before you are staring at a broken prompt.

Everything it reports is a warning rather than an error.

There are four mistakes it cannot catch:

  1. a command name that does not exist
  2. a key combination that cannot be spelled
  3. a PYREPL_SETUP path that does not import, or does not point at a callable
  4. a color name that does not exist

The first two need a REPL reader, and building one needs a terminal that manage.py check may not have. The third would mean importing your hook into every migrate, which is what naming it as a path exists to avoid.

All four are caught when the shell starts, and reported against the setting they came from:

$ ./manage.py shell
CommandError: Could not set up the REPL: PYREPL_BINDINGS['Ctrl+G'] is 'hom', which is not a command. Did you mean 'home'?

manage.py shell --show-bindings is the easiest way to check your configuration, since it resolves the same settings without starting a REPL.

Contributing 🤝

See CONTRIBUTING.md.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_pyrepl_hacks-0.2.0.tar.gz (20.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_pyrepl_hacks-0.2.0-py3-none-any.whl (17.9 kB view details)

Uploaded Python 3

File details

Details for the file django_pyrepl_hacks-0.2.0.tar.gz.

File metadata

  • Download URL: django_pyrepl_hacks-0.2.0.tar.gz
  • Upload date:
  • Size: 20.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for django_pyrepl_hacks-0.2.0.tar.gz
Algorithm Hash digest
SHA256 1b93e2bf9a8f9666346a5a818a756c1d2e5ec2331b293a9c0314e47f8b1f2a94
MD5 5ffd09da9d217a405727dceab5757af1
BLAKE2b-256 50177f83dcc0a308d234d4d898f5c0b286b73d8b05afbd0bf67a49c823f44dff

See more details on using hashes here.

File details

Details for the file django_pyrepl_hacks-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: django_pyrepl_hacks-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 17.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for django_pyrepl_hacks-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1fa0f1a3d252b257d2cfae967372979012f4c12600247e14f6dd73d675fd373b
MD5 424d867d3d3fd8d71364f85710631e73
BLAKE2b-256 de6e22977adc4e08a6b102a50263076f56ce26d4bbcef69cd86e4dbf092ebab8

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page