django-pyrepl-hacks 0.1.0__py3-none-any.whl
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.
- django_pyrepl_hacks/__init__.py +13 -0
- django_pyrepl_hacks/apps.py +16 -0
- django_pyrepl_hacks/bindings.py +126 -0
- django_pyrepl_hacks/checks.py +115 -0
- django_pyrepl_hacks/conf.py +74 -0
- django_pyrepl_hacks/management/__init__.py +0 -0
- django_pyrepl_hacks/management/commands/__init__.py +0 -0
- django_pyrepl_hacks/management/commands/shell.py +72 -0
- django_pyrepl_hacks/py.typed +0 -0
- django_pyrepl_hacks/repl.py +87 -0
- django_pyrepl_hacks-0.1.0.dist-info/METADATA +409 -0
- django_pyrepl_hacks-0.1.0.dist-info/RECORD +14 -0
- django_pyrepl_hacks-0.1.0.dist-info/WHEEL +4 -0
- django_pyrepl_hacks-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""A Django shell that uses the new Python REPL, with pyrepl-hacks key bindings.
|
|
2
|
+
|
|
3
|
+
Add ``"django_pyrepl_hacks"`` to ``INSTALLED_APPS`` and ``manage.py shell``
|
|
4
|
+
will launch the new Python REPL (``_pyrepl``) with Django's auto-imported
|
|
5
|
+
models and a set of extra key bindings.
|
|
6
|
+
|
|
7
|
+
Everything is configurable through ``PYREPL_*`` settings.
|
|
8
|
+
See the README for the full list.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .bindings import Insert, insert
|
|
12
|
+
|
|
13
|
+
__all__ = ["Insert", "insert"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""The app config, which exists to register the settings checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from django.core.checks import register
|
|
7
|
+
|
|
8
|
+
from .checks import check_settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PyreplHacksConfig(AppConfig):
|
|
12
|
+
name = "django_pyrepl_hacks"
|
|
13
|
+
verbose_name = "Python REPL hacks"
|
|
14
|
+
|
|
15
|
+
def ready(self) -> None:
|
|
16
|
+
register(check_settings)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Key bindings for the pyrepl-powered Django shell.
|
|
2
|
+
|
|
3
|
+
Nothing in this module imports ``pyrepl_hacks`` (and therefore ``_pyrepl``) at
|
|
4
|
+
import time.
|
|
5
|
+
Settings modules import ``insert`` from this package, and importing a private
|
|
6
|
+
stdlib module as a side effect of reading settings would be rude.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import inspect
|
|
12
|
+
from collections.abc import Callable, Mapping
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
16
|
+
|
|
17
|
+
__all__ = ["DEFAULT_BINDINGS", "Insert", "apply_bindings", "insert", "resolve_bindings"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Insert:
|
|
22
|
+
"""A binding that inserts literal text at the cursor."""
|
|
23
|
+
|
|
24
|
+
text: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def insert(text: str) -> Insert:
|
|
28
|
+
"""Return a binding that inserts ``text`` at the cursor.
|
|
29
|
+
|
|
30
|
+
Meant for use in the ``PYREPL_BINDINGS`` setting:
|
|
31
|
+
|
|
32
|
+
PYREPL_BINDINGS = {"Ctrl+N": insert("[2, 1, 3, 4, 7, 11, 18, 29]")}
|
|
33
|
+
"""
|
|
34
|
+
return Insert(text)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
#: A binding target: a command name, literal text to insert, or a function.
|
|
38
|
+
type Binding = str | Insert | Callable[..., None]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
DEFAULT_BINDINGS: dict[str, Binding] = {
|
|
42
|
+
"Home": "home",
|
|
43
|
+
"End": "end",
|
|
44
|
+
"Alt+M": "move-to-indentation",
|
|
45
|
+
"Shift+Tab": "dedent",
|
|
46
|
+
"Alt+Down": "move-line-down",
|
|
47
|
+
"Alt+Up": "move-line-up",
|
|
48
|
+
"Ctrl+Up": "previous-history",
|
|
49
|
+
"Ctrl+Down": "next-history",
|
|
50
|
+
"Alt+{": "previous-paragraph",
|
|
51
|
+
"Alt+}": "next-paragraph",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_bindings(
|
|
56
|
+
overrides: Mapping[str, Binding | None] | None = None,
|
|
57
|
+
*,
|
|
58
|
+
use_defaults: bool = True,
|
|
59
|
+
) -> dict[str, Binding]:
|
|
60
|
+
"""Merge ``overrides`` over ``DEFAULT_BINDINGS``.
|
|
61
|
+
|
|
62
|
+
A key mapped to ``None`` is removed, which is how a default binding is
|
|
63
|
+
turned off without turning all of them off.
|
|
64
|
+
"""
|
|
65
|
+
bindings: dict[str, Binding | None] = dict(DEFAULT_BINDINGS) if use_defaults else {}
|
|
66
|
+
bindings.update(overrides or {})
|
|
67
|
+
return {key: target for key, target in bindings.items() if target is not None}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _wants_event(function: Callable[..., None]) -> bool:
|
|
71
|
+
"""Return whether ``function`` expects ``(reader, event_name, event)``."""
|
|
72
|
+
try:
|
|
73
|
+
signature = inspect.signature(function)
|
|
74
|
+
except (TypeError, ValueError):
|
|
75
|
+
return False
|
|
76
|
+
parameters = [
|
|
77
|
+
parameter
|
|
78
|
+
for parameter in signature.parameters.values()
|
|
79
|
+
if parameter.kind
|
|
80
|
+
in (parameter.POSITIONAL_ONLY, parameter.POSITIONAL_OR_KEYWORD)
|
|
81
|
+
]
|
|
82
|
+
return len(parameters) >= 3
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _command_name(function: Callable[..., None]) -> str:
|
|
86
|
+
"""Return the REPL command name for ``function``."""
|
|
87
|
+
name: str | None = getattr(function, "__name__", None)
|
|
88
|
+
if not name or name == "<lambda>":
|
|
89
|
+
raise ImproperlyConfigured(
|
|
90
|
+
"PYREPL_BINDINGS functions need a name: bind a def, not a lambda.",
|
|
91
|
+
)
|
|
92
|
+
return name.replace("_", "-")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def apply_bindings(bindings: Mapping[str, Binding]) -> None:
|
|
96
|
+
"""Bind every key in ``bindings`` in the current REPL reader."""
|
|
97
|
+
import pyrepl_hacks as repl
|
|
98
|
+
|
|
99
|
+
for keybinding, target in bindings.items():
|
|
100
|
+
match target:
|
|
101
|
+
case str():
|
|
102
|
+
repl.bind(keybinding, target)
|
|
103
|
+
case Insert(text=text):
|
|
104
|
+
repl.bind_to_insert(keybinding, text)
|
|
105
|
+
case _ if callable(target):
|
|
106
|
+
name = _command_name(target)
|
|
107
|
+
repl.register_command(name, with_event=_wants_event(target))(target)
|
|
108
|
+
repl.bind(keybinding, name)
|
|
109
|
+
case _:
|
|
110
|
+
raise ImproperlyConfigured(
|
|
111
|
+
f"PYREPL_BINDINGS[{keybinding!r}] should be a command name, "
|
|
112
|
+
f"an insert(...) value, or a function, not {target!r}.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def describe(target: Binding) -> str:
|
|
117
|
+
"""Return a short human-readable description of a binding target."""
|
|
118
|
+
match target:
|
|
119
|
+
case str():
|
|
120
|
+
return target
|
|
121
|
+
case Insert(text=text):
|
|
122
|
+
return f"insert {text!r}"
|
|
123
|
+
case _ if callable(target):
|
|
124
|
+
return _command_name(target)
|
|
125
|
+
case _: # pragma: no cover - guarded by apply_bindings
|
|
126
|
+
return repr(target)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""System checks for the ``PYREPL_*`` settings.
|
|
2
|
+
|
|
3
|
+
These run on ``manage.py check``, which is the only chance to catch a typo in
|
|
4
|
+
a setting before it turns into a traceback in front of the REPL prompt.
|
|
5
|
+
|
|
6
|
+
Nothing here imports ``pyrepl_hacks``: importing it registers commands, which
|
|
7
|
+
builds a REPL reader, which needs a terminal that a check run may not have.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from django.core.checks import Error, Warning
|
|
16
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
17
|
+
|
|
18
|
+
from . import conf, repl
|
|
19
|
+
from .bindings import Insert
|
|
20
|
+
|
|
21
|
+
__all__ = ["check_settings"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# The token names `_colorize.Syntax` accepts, which is what `update_theme`
|
|
25
|
+
# forwards its keyword arguments to.
|
|
26
|
+
THEME_TOKENS = frozenset(
|
|
27
|
+
{
|
|
28
|
+
"prompt",
|
|
29
|
+
"keyword",
|
|
30
|
+
"keyword_constant",
|
|
31
|
+
"soft_keyword",
|
|
32
|
+
"builtin",
|
|
33
|
+
"comment",
|
|
34
|
+
"string",
|
|
35
|
+
"number",
|
|
36
|
+
"op",
|
|
37
|
+
"definition",
|
|
38
|
+
"reset",
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_settings(**kwargs: Any) -> list[Any]:
|
|
44
|
+
"""Validate every ``PYREPL_*`` setting we understand."""
|
|
45
|
+
return [
|
|
46
|
+
*_check_bindings(),
|
|
47
|
+
*_check_theme(),
|
|
48
|
+
*_check_setup(),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _check_bindings() -> list[Any]:
|
|
53
|
+
try:
|
|
54
|
+
bindings = conf.get_bindings()
|
|
55
|
+
except ImproperlyConfigured as error:
|
|
56
|
+
return [_error(f"PYREPL_BINDINGS is invalid: {error}", "pyrepl_hacks.E001")]
|
|
57
|
+
errors: list[Error] = []
|
|
58
|
+
for key, target in bindings.items():
|
|
59
|
+
if not isinstance(target, str | Insert) and not callable(target):
|
|
60
|
+
errors.append(
|
|
61
|
+
_error(
|
|
62
|
+
f"PYREPL_BINDINGS[{key!r}] is {target!r}, which is not a command "
|
|
63
|
+
"name, an insert(...) value, or a function.",
|
|
64
|
+
"pyrepl_hacks.E002",
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
elif getattr(target, "__name__", None) == "<lambda>":
|
|
68
|
+
# A command is registered under a name, and a lambda has none.
|
|
69
|
+
errors.append(
|
|
70
|
+
_error(
|
|
71
|
+
f"PYREPL_BINDINGS[{key!r}] is a lambda, which has no name to "
|
|
72
|
+
"register a command under.",
|
|
73
|
+
"pyrepl_hacks.E003",
|
|
74
|
+
hint="Use a def, or insert(...) if you just want to insert text.",
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
return errors
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _check_theme() -> list[Any]:
|
|
81
|
+
try:
|
|
82
|
+
theme = conf.get_theme()
|
|
83
|
+
except ImproperlyConfigured as error:
|
|
84
|
+
return [_error(f"PYREPL_THEME is invalid: {error}", "pyrepl_hacks.E004")]
|
|
85
|
+
if theme and sys.version_info < repl.THEME_REQUIRES:
|
|
86
|
+
return [
|
|
87
|
+
Warning(
|
|
88
|
+
"PYREPL_THEME needs Python 3.14 or later, so the shell will "
|
|
89
|
+
"refuse to start with it set.",
|
|
90
|
+
hint="Remove the setting to use the REPL's own colors.",
|
|
91
|
+
id="pyrepl_hacks.W001",
|
|
92
|
+
),
|
|
93
|
+
]
|
|
94
|
+
unknown = sorted(set(theme) - THEME_TOKENS)
|
|
95
|
+
if not unknown:
|
|
96
|
+
return []
|
|
97
|
+
return [
|
|
98
|
+
_error(
|
|
99
|
+
f"PYREPL_THEME has unknown token names: {', '.join(unknown)}.",
|
|
100
|
+
"pyrepl_hacks.E005",
|
|
101
|
+
hint=f"Known tokens: {', '.join(sorted(THEME_TOKENS))}.",
|
|
102
|
+
),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _check_setup() -> list[Any]:
|
|
107
|
+
try:
|
|
108
|
+
conf.get_setup_hooks()
|
|
109
|
+
except ImproperlyConfigured as error:
|
|
110
|
+
return [_error(f"PYREPL_SETUP is invalid: {error}", "pyrepl_hacks.E006")]
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _error(message: str, check_id: str, hint: str | None = None) -> Error:
|
|
115
|
+
return Error(message, hint=hint, id=check_id)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Reading the ``PYREPL_*`` settings.
|
|
2
|
+
|
|
3
|
+
Every setting is optional, so each accessor here returns a usable default.
|
|
4
|
+
Values are read at call time rather than at import time, so that
|
|
5
|
+
``override_settings`` works and so that importing this package never requires
|
|
6
|
+
configured settings.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
12
|
+
|
|
13
|
+
from django.conf import settings
|
|
14
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
15
|
+
from django.utils.module_loading import import_string
|
|
16
|
+
|
|
17
|
+
from .bindings import Binding, resolve_bindings
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"get_bindings",
|
|
21
|
+
"get_setup_hooks",
|
|
22
|
+
"get_theme",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _setting(name: str, default: object) -> object:
|
|
27
|
+
return getattr(settings, f"PYREPL_{name}", default)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_bindings() -> dict[str, Binding]:
|
|
31
|
+
"""Return the key bindings to apply, defaults included unless disabled."""
|
|
32
|
+
overrides = _setting("BINDINGS", {})
|
|
33
|
+
if not isinstance(overrides, Mapping):
|
|
34
|
+
raise ImproperlyConfigured("PYREPL_BINDINGS should be a dictionary.")
|
|
35
|
+
use_defaults = bool(_setting("USE_DEFAULT_BINDINGS", True))
|
|
36
|
+
return resolve_bindings(overrides, use_defaults=use_defaults)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_theme() -> dict[str, str]:
|
|
40
|
+
"""Return syntax highlighting colors to pass to ``update_theme``."""
|
|
41
|
+
theme = _setting("THEME", {})
|
|
42
|
+
if not isinstance(theme, Mapping):
|
|
43
|
+
raise ImproperlyConfigured("PYREPL_THEME should be a dictionary.")
|
|
44
|
+
return dict(theme)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_setup_hooks() -> list[Callable[[], None]]:
|
|
48
|
+
"""Return the callables configured by ``PYREPL_SETUP``.
|
|
49
|
+
|
|
50
|
+
A single callable, a single dotted path, or a sequence of either is
|
|
51
|
+
accepted, since one hook is the common case and a list is the general one.
|
|
52
|
+
"""
|
|
53
|
+
configured = _setting("SETUP", [])
|
|
54
|
+
if configured is None:
|
|
55
|
+
return []
|
|
56
|
+
if callable(configured) or isinstance(configured, str):
|
|
57
|
+
configured = [configured]
|
|
58
|
+
if not isinstance(configured, Sequence):
|
|
59
|
+
raise ImproperlyConfigured(
|
|
60
|
+
"PYREPL_SETUP should be a callable, an import path, or a list of them.",
|
|
61
|
+
)
|
|
62
|
+
hooks = []
|
|
63
|
+
for hook in configured:
|
|
64
|
+
if isinstance(hook, str):
|
|
65
|
+
try:
|
|
66
|
+
hook = import_string(hook)
|
|
67
|
+
except ImportError as error:
|
|
68
|
+
raise ImproperlyConfigured(
|
|
69
|
+
f"PYREPL_SETUP could not import {hook!r}: {error}",
|
|
70
|
+
) from error
|
|
71
|
+
if not callable(hook):
|
|
72
|
+
raise ImproperlyConfigured(f"PYREPL_SETUP entry {hook!r} is not callable.")
|
|
73
|
+
hooks.append(hook)
|
|
74
|
+
return hooks
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""A ``shell`` command that prefers the new Python REPL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import types
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from django.core.management.base import CommandError
|
|
9
|
+
from django.core.management.commands.shell import Command as BaseShellCommand
|
|
10
|
+
|
|
11
|
+
from django_pyrepl_hacks import conf, repl
|
|
12
|
+
from django_pyrepl_hacks.bindings import describe
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Command(BaseShellCommand):
|
|
16
|
+
"""Django's shell command, with ``pyrepl`` as the preferred interface."""
|
|
17
|
+
|
|
18
|
+
shells = ["pyrepl", "ipython", "bpython", "python"] # noqa: RUF012
|
|
19
|
+
|
|
20
|
+
def add_arguments(self, parser: Any) -> None:
|
|
21
|
+
super().add_arguments(parser)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--show-bindings",
|
|
24
|
+
action="store_true",
|
|
25
|
+
help="Print the configured REPL key bindings and exit.",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def handle(self, **options: Any) -> None:
|
|
29
|
+
if options.get("show_bindings"):
|
|
30
|
+
self.show_bindings()
|
|
31
|
+
return
|
|
32
|
+
super().handle(**options)
|
|
33
|
+
|
|
34
|
+
def show_bindings(self) -> None:
|
|
35
|
+
"""Write the resolved key bindings to stdout, aligned on the widest key."""
|
|
36
|
+
bindings = conf.get_bindings()
|
|
37
|
+
if not bindings:
|
|
38
|
+
self.stdout.write("No key bindings are configured.")
|
|
39
|
+
return
|
|
40
|
+
width = max(len(key) for key in bindings)
|
|
41
|
+
for key, target in bindings.items():
|
|
42
|
+
self.stdout.write(f"{key:<{width}} {describe(target)}")
|
|
43
|
+
|
|
44
|
+
def pyrepl(self, options: dict[str, Any]) -> None:
|
|
45
|
+
"""Run the new Python REPL with Django's namespace and our bindings."""
|
|
46
|
+
usable, reason = repl.can_use_pyrepl()
|
|
47
|
+
if not usable:
|
|
48
|
+
# ImportError is how a shell interface declines, which lets Django
|
|
49
|
+
# move on to the next one in `shells` instead of failing outright.
|
|
50
|
+
raise ImportError(reason or "the new Python REPL is unavailable")
|
|
51
|
+
repl.load_pyrepl_hacks()
|
|
52
|
+
|
|
53
|
+
from _pyrepl.main import interactive_console
|
|
54
|
+
|
|
55
|
+
# Past this point an ImportError is a broken configuration rather than
|
|
56
|
+
# an unavailable interface, and silently dropping to the plain shell
|
|
57
|
+
# would hide it. Every one of these is only reachable once the REPL
|
|
58
|
+
# itself has said it can run.
|
|
59
|
+
try:
|
|
60
|
+
repl.setup()
|
|
61
|
+
except ImportError as error:
|
|
62
|
+
raise CommandError(f"Could not set up the REPL: {error}") from error
|
|
63
|
+
|
|
64
|
+
# A real __main__ module, so that pickling, dataclasses, and anything
|
|
65
|
+
# else that looks up __module__ behaves the way it does in a REPL.
|
|
66
|
+
mainmodule = types.ModuleType("__main__")
|
|
67
|
+
mainmodule.__dict__.update(self.get_namespace(**options))
|
|
68
|
+
|
|
69
|
+
interactive_console(
|
|
70
|
+
mainmodule=mainmodule,
|
|
71
|
+
pythonstartup=not options.get("no_startup"),
|
|
72
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Setting up the REPL itself: bindings, theme, and setup hooks.
|
|
2
|
+
|
|
3
|
+
The ``pyrepl_hacks`` imports live inside the functions here.
|
|
4
|
+
Importing it builds a REPL reader, which needs a terminal, so it must not
|
|
5
|
+
happen while Django is merely loading apps.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from types import ModuleType
|
|
12
|
+
|
|
13
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
14
|
+
|
|
15
|
+
from . import conf
|
|
16
|
+
from .bindings import apply_bindings
|
|
17
|
+
|
|
18
|
+
__all__ = ["can_use_pyrepl", "load_pyrepl_hacks", "setup"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def can_use_pyrepl() -> tuple[bool, str]:
|
|
22
|
+
"""Return whether the new REPL can run here, and why not when it cannot.
|
|
23
|
+
|
|
24
|
+
``_pyrepl`` silently falls back to the basic REPL when it cannot run, and
|
|
25
|
+
that fallback drops the namespace we spent all this effort building.
|
|
26
|
+
So the shell command asks first and declines the interface instead.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
from _pyrepl.main import CAN_USE_PYREPL, FAIL_REASON
|
|
30
|
+
except ImportError as error: # pragma: no cover - Python 3.12 and earlier
|
|
31
|
+
return False, str(error)
|
|
32
|
+
return CAN_USE_PYREPL, FAIL_REASON
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_pyrepl_hacks() -> ModuleType:
|
|
36
|
+
"""Import ``pyrepl_hacks``, translating its failures into ImportError.
|
|
37
|
+
|
|
38
|
+
Importing it registers commands, which builds a REPL reader, which needs a
|
|
39
|
+
terminal. A RuntimeError from that is the same kind of "not here, not now"
|
|
40
|
+
answer as an ImportError, and only ImportError makes Django try the next
|
|
41
|
+
interface.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
import pyrepl_hacks
|
|
45
|
+
except RuntimeError as error:
|
|
46
|
+
raise ImportError(f"pyrepl-hacks could not set up the REPL: {error}") from error
|
|
47
|
+
return pyrepl_hacks
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
#: Syntax highlighting is themeable from Python 3.14 on, when `_colorize`
|
|
51
|
+
#: grew the `Syntax` class that `update_theme` builds a theme out of.
|
|
52
|
+
THEME_REQUIRES = (3, 14)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def apply_theme() -> None:
|
|
56
|
+
"""Apply ``PYREPL_THEME`` to the REPL's syntax highlighting."""
|
|
57
|
+
theme = conf.get_theme()
|
|
58
|
+
if not theme:
|
|
59
|
+
return
|
|
60
|
+
if sys.version_info < THEME_REQUIRES:
|
|
61
|
+
raise ImproperlyConfigured(
|
|
62
|
+
"PYREPL_THEME needs Python 3.14 or later. "
|
|
63
|
+
"Remove the setting to use the REPL's own colors.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
import pyrepl_hacks as repl
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
repl.update_theme(**theme)
|
|
70
|
+
except (AttributeError, TypeError) as error:
|
|
71
|
+
# An unknown color name is an AttributeError on _colorize.ANSIColors,
|
|
72
|
+
# and an unknown token name is a TypeError from Syntax().
|
|
73
|
+
raise ImproperlyConfigured(f"PYREPL_THEME is invalid: {error}") from error
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run_setup_hooks() -> None:
|
|
77
|
+
"""Call everything listed in ``PYREPL_SETUP``."""
|
|
78
|
+
for hook in conf.get_setup_hooks():
|
|
79
|
+
hook()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def setup() -> None:
|
|
83
|
+
"""Configure the REPL from settings, just before it starts."""
|
|
84
|
+
load_pyrepl_hacks()
|
|
85
|
+
apply_bindings(conf.get_bindings())
|
|
86
|
+
apply_theme()
|
|
87
|
+
run_setup_hooks()
|
|
@@ -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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
django_pyrepl_hacks/__init__.py,sha256=P-yANqP5Nc64s2DHbQ3q-ECDL9hV5JCj4VwMlt79skw,439
|
|
2
|
+
django_pyrepl_hacks/apps.py,sha256=WVv9PicqWt4uAjQ1qOQGb4y02aGmglZuftQJoiCPnag,388
|
|
3
|
+
django_pyrepl_hacks/bindings.py,sha256=kc7KFD9ei_iGr4QkwvVmy5p49DoeRfot-4tXZiRnUk0,4086
|
|
4
|
+
django_pyrepl_hacks/checks.py,sha256=NQXkXwypuujfcOlYnU9xRfCLOU9Qr5eO8JfYvpzktjE,3541
|
|
5
|
+
django_pyrepl_hacks/conf.py,sha256=ARh6l5dIv5Fjni574t9E0BQ6tIeztoRiKt38F8w6BJk,2562
|
|
6
|
+
django_pyrepl_hacks/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
django_pyrepl_hacks/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
django_pyrepl_hacks/management/commands/shell.py,sha256=rRSfPWv7tPNygG6Mwu1SO8x7HjTe0yHF_hsmrLvZPxk,2772
|
|
9
|
+
django_pyrepl_hacks/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
django_pyrepl_hacks/repl.py,sha256=HN_EZZPfnjDrGov3IIiY1pa14yopf6u5E-RgacD9DYQ,2861
|
|
11
|
+
django_pyrepl_hacks-0.1.0.dist-info/licenses/LICENSE.txt,sha256=Oyi3NXRJXSTm0P52rdsbqY-R2swOxbUg0CM7NZqw5N0,1076
|
|
12
|
+
django_pyrepl_hacks-0.1.0.dist-info/WHEEL,sha256=4OL6Foqnnp3xRY5wMkjgc25_i5YJC6dKsC6LPcjqEoU,80
|
|
13
|
+
django_pyrepl_hacks-0.1.0.dist-info/METADATA,sha256=Ks4vh_kV36YmByHaeR-ES-MUt5qdBAYvL8EEF96FP6s,14354
|
|
14
|
+
django_pyrepl_hacks-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|