django-pyrepl-hacks 🐍
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:
- A string representing the name of an installed
_pyreplcommand - A function, representing a new
_pyreplcommand 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:
- a command name that does not exist
- a key combination that cannot be spelled
- a
PYREPL_SETUPpath that does not import, or does not point at a callable - 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b93e2bf9a8f9666346a5a818a756c1d2e5ec2331b293a9c0314e47f8b1f2a94
|
|
| MD5 |
5ffd09da9d217a405727dceab5757af1
|
|
| BLAKE2b-256 |
50177f83dcc0a308d234d4d898f5c0b286b73d8b05afbd0bf67a49c823f44dff
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1fa0f1a3d252b257d2cfae967372979012f4c12600247e14f6dd73d675fd373b
|
|
| MD5 |
424d867d3d3fd8d71364f85710631e73
|
|
| BLAKE2b-256 |
de6e22977adc4e08a6b102a50263076f56ce26d4bbcef69cd86e4dbf092ebab8
|