django-pyrepl-hacks 0.1.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.
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+
4
+ ## 0.1.0 (unreleased)
5
+
6
+ Initial release.
7
+
8
+ - A `shell` management command that prefers the new Python REPL, with Django's auto-imported models and [pyrepl-hacks][] key bindings
9
+ - `PYREPL_BINDINGS` and `PYREPL_USE_DEFAULT_BINDINGS` for key bindings
10
+ - `PYREPL_THEME` for syntax highlighting colors (Python 3.14 and later)
11
+ - `PYREPL_SETUP` for anything else, including custom prompts and banners
12
+ - `manage.py shell --show-bindings` to see what is bound
13
+ - System checks for every setting
14
+
15
+ [pyrepl-hacks]: https://github.com/treyhunner/pyrepl-hacks
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present Trey Hunner
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,409 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-pyrepl-hacks
3
+ Version: 0.1.0
4
+ Summary: A Django shell that uses the new Python REPL, with pyrepl-hacks key bindings.
5
+ Keywords: django,repl,shell,pyrepl,keyboard,shortcuts
6
+ Author: Trey Hunner
7
+ Author-email: Trey Hunner <trey@treyhunner.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE.txt
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Django :: 5.2
15
+ Classifier: Framework :: Django :: 6.0
16
+ Classifier: Framework :: Django :: 6.1
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: System :: Shells
25
+ Classifier: Topic :: Terminals
26
+ Classifier: Typing :: Typed
27
+ Requires-Dist: django>=5.2
28
+ Requires-Dist: pyrepl-hacks>=0.5.0
29
+ Requires-Python: >=3.13, <3.15
30
+ Project-URL: Documentation, https://github.com/treyhunner/django-pyrepl-hacks#readme
31
+ Project-URL: Changelog, https://github.com/treyhunner/django-pyrepl-hacks/blob/main/CHANGELOG.md
32
+ Project-URL: Issues, https://github.com/treyhunner/django-pyrepl-hacks/issues
33
+ Project-URL: Source, https://github.com/treyhunner/django-pyrepl-hacks
34
+ Description-Content-Type: text/markdown
35
+
36
+ # django-pyrepl-hacks 🐍
37
+
38
+ [![PyPI][pypi-badge]][pypi]
39
+ [![CI][ci-badge]][ci]
40
+ [![Coverage][coverage-badge]][coverage]
41
+
42
+ A Django `shell` that uses the new Python REPL, with [pyrepl-hacks][] key bindings.
43
+
44
+ Django's shell tries IPython, then bpython, then `code.interact`.
45
+ None of those is the REPL that ships with Python 3.13 and later, so `manage.py shell` gives you a worse REPL than `python` does.
46
+ This package adds a `pyrepl` interface and puts it first.
47
+
48
+ You get syntax highlighting, multi-line editing, and history search, plus Django's auto-imported models and a handful of extra key bindings.
49
+
50
+
51
+ ## ⚠️ WARNING: here be dragons 🐉
52
+
53
+ This builds on [pyrepl-hacks][], which relies on the `_pyrepl` module.
54
+ As the `_` prefix implies, that module is not designed for public use, and a new Python release may break it.
55
+
56
+ So this package pins its supported Python versions to the ones known to work.
57
+
58
+
59
+ ## Installing 💾
60
+
61
+ This needs Python 3.13 or 3.14 and Django 5.2 or later.
62
+
63
+ ```console
64
+ uv add django-pyrepl-hacks
65
+ ```
66
+
67
+ Or with pip:
68
+
69
+ ```console
70
+ python -m pip install django-pyrepl-hacks
71
+ ```
72
+
73
+ Then add it to `INSTALLED_APPS`:
74
+
75
+ ```python
76
+ INSTALLED_APPS = [
77
+ "django_pyrepl_hacks",
78
+ # ...
79
+ ]
80
+ ```
81
+
82
+ That's the whole setup.
83
+ `manage.py shell` will now use the new REPL:
84
+
85
+ ```console
86
+ $ ./manage.py shell
87
+ 14 objects imported automatically (use -v 2 for details).
88
+
89
+ >>>
90
+ ```
91
+
92
+ The other interfaces are still there, so `manage.py shell -i ipython` works as before.
93
+ So does everything else the shell command does: `-c`, piped stdin, `--no-imports`, and `--no-startup`.
94
+
95
+ ### A note on dependency groups
96
+
97
+ `INSTALLED_APPS` is read wherever Django starts, so this belongs with your regular dependencies rather than in a dev-only group.
98
+ A dev-only install plus an unconditional `INSTALLED_APPS` entry means production cannot start at all.
99
+
100
+ If you would rather keep it out of production, gate the entry too:
101
+
102
+ ```python
103
+ if DEBUG:
104
+ INSTALLED_APPS += ["django_pyrepl_hacks"]
105
+ ```
106
+
107
+ Production then falls back to Django's own shell, which means a production shell loses the auto-imported models as well.
108
+ Shipping it is usually the smaller cost: the package is pure Python and depends on nothing but Django and [pyrepl-hacks][].
109
+
110
+
111
+ ## Default key bindings ⌨️
112
+
113
+ | Key | Command | What it does |
114
+ | ----------- | --------------------- | ----------------------------------------- |
115
+ | `Home` | `home` | Move to the first character in the input |
116
+ | `End` | `end` | Move to the last character in the input |
117
+ | `Alt+M` | `move-to-indentation` | Move to the first non-space in the line |
118
+ | `Shift+Tab` | `dedent` | Dedent the whole input |
119
+ | `Alt+Down` | `move-line-down` | Swap the current line with the next one |
120
+ | `Alt+Up` | `move-line-up` | Swap the current line with the previous one |
121
+ | `Ctrl+Up` | `previous-history` | Move to the previous history entry |
122
+ | `Ctrl+Down` | `next-history` | Move to the next history entry |
123
+ | `Alt+{` | `previous-paragraph` | Move to the previous blank line |
124
+ | `Alt+}` | `next-paragraph` | Move to the next blank line |
125
+
126
+ To see what is actually bound in your project, including your own additions:
127
+
128
+ ```console
129
+ ./manage.py shell --show-bindings
130
+ ```
131
+
132
+
133
+ ## Settings ⚙️
134
+
135
+ Every setting is optional.
136
+
137
+
138
+ ### `PYREPL_BINDINGS`
139
+
140
+ A dictionary mapping a key to the thing that key should do, layered over the defaults above.
141
+
142
+ The keys are human-readable: `"Ctrl+K"`, `"Alt+Up"`, `"Shift+Tab"`, `"F4"`, `"Home"`, `"PageUp"`, or a sequence like `"Ctrl+X Ctrl+R"`.
143
+
144
+ The values come in four flavors.
145
+
146
+ **1. A string is the name of a REPL command that already exists.**
147
+
148
+ ```python
149
+ PYREPL_BINDINGS = {
150
+ "Ctrl+K": "kill-line", # Delete from the cursor to end of line
151
+ "Alt+D": "kill-word", # Delete the word after the cursor
152
+ "Ctrl+L": "clear-screen",
153
+ "Alt+<": "first-history", # Jump to the oldest history entry
154
+ "Alt+>": "last-history", # Jump to the newest
155
+ "Ctrl+O": "operate-and-get-next", # Run this line, then offer the next one
156
+ }
157
+ ```
158
+
159
+ There are about 50 of these built in.
160
+ `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.
161
+ [pyrepl-hacks][] adds `dedent`, `move-line-up`, `move-line-down`, `move-to-indentation`, `previous-paragraph`, and `next-paragraph`.
162
+
163
+ **2. `insert(text)` types some text for you.**
164
+
165
+ ```python
166
+ from django_pyrepl_hacks import insert
167
+
168
+ PYREPL_BINDINGS = {
169
+ "Ctrl+N": insert("User.objects.filter("),
170
+ "F5": insert("from django.test import Client\nc = Client()\n"),
171
+ "F9": insert("connection.queries[-1]['sql']"),
172
+ }
173
+ ```
174
+
175
+ **3. A function becomes a new REPL command.**
176
+
177
+ It is registered under its own name, with underscores turned into hyphens, so `sql_of_last_query` becomes the command `sql-of-last-query`.
178
+ It is called with the reader, which is the object holding the text you are editing:
179
+
180
+ ```python
181
+ # myproject/repl.py
182
+ def sql_of_last_query(reader):
183
+ """Type out the SQL of the most recent query."""
184
+ from django.db import connection
185
+
186
+ if connection.queries:
187
+ reader.insert(connection.queries[-1]["sql"])
188
+ ```
189
+
190
+ ```python
191
+ from myproject.repl import sql_of_last_query
192
+
193
+ PYREPL_BINDINGS = {"F9": sql_of_last_query}
194
+ ```
195
+
196
+ A function taking three arguments is called with the event too, the way [pyrepl-hacks][] `with_event=True` commands are.
197
+ That is what you need to move the cursor, since the movement commands take the event:
198
+
199
+ ```python
200
+ import pyrepl_hacks as repl
201
+
202
+
203
+ def filter_call(reader, event_name, event):
204
+ """Type `User.objects.filter()` and park the cursor inside the parens."""
205
+ reader.insert("User.objects.filter()")
206
+ repl.commands.left(reader, event_name, event)
207
+ ```
208
+
209
+ Lambdas are rejected, because a command has to be registered under a name.
210
+ `manage.py check` tells you so rather than waiting until you press the key.
211
+
212
+ **4. `None` turns off one of the defaults.**
213
+
214
+ ```python
215
+ PYREPL_BINDINGS = {
216
+ "Home": None, # Give Home back to beginning-of-line
217
+ "Ctrl+Up": None,
218
+ }
219
+ ```
220
+
221
+ Putting it together:
222
+
223
+ ```python
224
+ from django_pyrepl_hacks import insert
225
+ from myproject.repl import sql_of_last_query
226
+
227
+ PYREPL_BINDINGS = {
228
+ "Ctrl+K": "kill-line",
229
+ "Ctrl+N": insert("User.objects.filter("),
230
+ "F9": sql_of_last_query,
231
+ "Home": None,
232
+ }
233
+ ```
234
+
235
+ ```console
236
+ $ ./manage.py shell --show-bindings
237
+ End end
238
+ Alt+M move-to-indentation
239
+ Shift+Tab dedent
240
+ Alt+Down move-line-down
241
+ Alt+Up move-line-up
242
+ Ctrl+Up previous-history
243
+ Ctrl+Down next-history
244
+ Alt+{ previous-paragraph
245
+ Alt+} next-paragraph
246
+ Ctrl+K kill-line
247
+ Ctrl+N insert 'User.objects.filter('
248
+ F9 sql-of-last-query
249
+ ```
250
+
251
+
252
+ ### `PYREPL_USE_DEFAULT_BINDINGS`
253
+
254
+ Set to `False` to start from nothing instead of from the default bindings.
255
+ `PYREPL_BINDINGS` is then the whole set.
256
+
257
+
258
+ ### `PYREPL_THEME`
259
+
260
+ Syntax highlighting colors for the code you type at the prompt.
261
+ The REPL colors your input as you type it, and this changes which color each kind of token gets.
262
+
263
+ The keys are the eleven token types the REPL knows about, and the values are colors:
264
+
265
+ ```python
266
+ PYREPL_THEME = {
267
+ "string": "green",
268
+ "number": "intense blue",
269
+ "comment": "grey",
270
+ "keyword": "bold magenta",
271
+ "prompt": "bold green",
272
+ }
273
+ ```
274
+
275
+ The token types:
276
+
277
+ | Token | What it colors |
278
+ | ------------------ | --------------------------------------- |
279
+ | `prompt` | The `>>>` and `...` prompts |
280
+ | `keyword` | `def`, `if`, `for`, `import`, `return` |
281
+ | `keyword_constant` | `None`, `True`, `False` |
282
+ | `soft_keyword` | `match`, `case`, `type` |
283
+ | `builtin` | `len`, `print`, `sorted` |
284
+ | `comment` | `# like this` |
285
+ | `string` | `"like this"` |
286
+ | `number` | `42`, `3.14` |
287
+ | `op` | `+`, `-`, `=`, `(`, `,` |
288
+ | `definition` | The name in `def name` or `class Name` |
289
+ | `reset` | Everything else, and the default style |
290
+
291
+ The colors are `black`, `blue`, `cyan`, `green`, `grey`, `magenta`, `red`, `white`, and `yellow`, each of which can be prefixed:
292
+
293
+ | Specification | Example |
294
+ | -------------------------- | --------------------------- |
295
+ | plain | `"red"` |
296
+ | `bold` | `"bold red"` |
297
+ | `intense` | `"intense red"` |
298
+ | `background` | `"background red"` |
299
+ | `intense background` | `"intense background red"` |
300
+ | combined with a comma | `"background black, bold white"` |
301
+ | nothing at all | `"reset"` |
302
+
303
+ So a low-contrast theme that leaves keywords loud:
304
+
305
+ ```python
306
+ PYREPL_THEME = {
307
+ "keyword": "bold magenta",
308
+ "keyword_constant": "bold magenta",
309
+ "soft_keyword": "bold magenta",
310
+ "builtin": "cyan",
311
+ "string": "green",
312
+ "number": "green",
313
+ "comment": "intense black",
314
+ "op": "reset",
315
+ "definition": "bold blue",
316
+ }
317
+ ```
318
+
319
+ Anything you leave out keeps the REPL's own color.
320
+
321
+ Named themes (`PYREPL_THEME = "solarized-light"`) are not implemented yet.
322
+ A string is not a valid value today, so they can be added without breaking a dictionary you have already written.
323
+
324
+ This setting needs Python 3.14 or later, since that is when the REPL's theme became something a program could set.
325
+ `manage.py check` warns when it is set on an older Python, and the shell refuses to start rather than pretending it worked.
326
+
327
+
328
+ ### `PYREPL_SETUP`
329
+
330
+ A callable, an import path, or a list of either, called once the REPL is configured and just before it starts.
331
+
332
+ This is the escape hatch: the one place to put code that should run when the REPL starts and nowhere else.
333
+ Settings modules will not do: they are imported by every management command and by your web server too.
334
+
335
+ ```python
336
+ PYREPL_SETUP = "myproject.repl.setup"
337
+ ```
338
+
339
+ Here's how you'd use the full [pyrepl-hacks][] API to register a command, rather than going through `PYREPL_BINDINGS`:
340
+
341
+ ```python
342
+ # myproject/repl.py
343
+ import pyrepl_hacks as repl
344
+
345
+
346
+ def setup():
347
+ @repl.bind("Ctrl+X Ctrl+Q", with_event=True)
348
+ def insert_query(reader, event_name, event):
349
+ """Insert a queryset skeleton and park the cursor inside it."""
350
+ reader.insert("User.objects.filter()")
351
+ repl.commands.left(reader, event_name, event)
352
+ ```
353
+
354
+ Import `pyrepl_hacks` inside the hook rather than at module level: importing it builds a REPL reader, which needs a terminal.
355
+
356
+ The hook is also where the settings this package deliberately does not have go.
357
+
358
+ **A prompt that tells you which environment you are in.**
359
+ The REPL only sets `sys.ps1` and `sys.ps2` if it finds them missing, so setting them in the hook means it leaves yours alone:
360
+
361
+ ```python
362
+ def setup():
363
+ import sys
364
+ from django.conf import settings
365
+
366
+ if not settings.DEBUG:
367
+ sys.ps1, sys.ps2 = "prod>>> ", "prod... "
368
+ ```
369
+
370
+ Worth setting up before the day you run a quick query in what turns out to be production.
371
+ A prompt is on every line, so unlike a banner it does not scroll away.
372
+
373
+ **A banner above the prompt.**
374
+
375
+ ```python
376
+ def setup():
377
+ import django
378
+ from django.db import connections
379
+
380
+ database = connections["default"].settings_dict["NAME"]
381
+ print(f"Django {django.get_version()} | {database}")
382
+ ```
383
+
384
+
385
+ ## When the REPL cannot run 🚧
386
+
387
+ The new REPL needs a terminal, and it refuses to start under `PYTHON_BASIC_REPL` or on an old Windows.
388
+ When that happens 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.
389
+
390
+ A mistake in your own configuration is a different thing: the shell reports it rather than quietly falling back to a plain REPL.
391
+
392
+
393
+ ## Checks ✅
394
+
395
+ `manage.py check` validates every `PYREPL_*` setting, so a typo turns up before you are staring at a broken prompt.
396
+
397
+
398
+ ## Contributing 🤝
399
+
400
+ See [CONTRIBUTING.md][].
401
+
402
+ [pyrepl-hacks]: https://github.com/treyhunner/pyrepl-hacks
403
+ [CONTRIBUTING.md]: CONTRIBUTING.md
404
+ [pypi-badge]: https://img.shields.io/pypi/v/django-pyrepl-hacks.svg
405
+ [pypi]: https://pypi.org/project/django-pyrepl-hacks/
406
+ [ci-badge]: https://github.com/treyhunner/django-pyrepl-hacks/actions/workflows/ci.yml/badge.svg
407
+ [ci]: https://github.com/treyhunner/django-pyrepl-hacks/actions/workflows/ci.yml
408
+ [coverage-badge]: https://codecov.io/gh/treyhunner/django-pyrepl-hacks/branch/main/graph/badge.svg
409
+ [coverage]: https://codecov.io/gh/treyhunner/django-pyrepl-hacks