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.
- pyrepl_hacks-0.1.0/PKG-INFO +224 -0
- pyrepl_hacks-0.1.0/README.md +217 -0
- pyrepl_hacks-0.1.0/pyproject.toml +7 -0
- pyrepl_hacks-0.1.0/pyrepl_hacks/__init__.py +7 -0
- pyrepl_hacks-0.1.0/pyrepl_hacks/bind_utils.py +52 -0
- pyrepl_hacks-0.1.0/pyrepl_hacks/command_utils.py +38 -0
- pyrepl_hacks-0.1.0/pyrepl_hacks/commands.py +112 -0
- pyrepl_hacks-0.1.0/pyrepl_hacks/key_utils.py +54 -0
- pyrepl_hacks-0.1.0/pyrepl_hacks/theme_utils.py +17 -0
- pyrepl_hacks-0.1.0/pyrepl_hacks.egg-info/PKG-INFO +224 -0
- pyrepl_hacks-0.1.0/pyrepl_hacks.egg-info/SOURCES.txt +12 -0
- pyrepl_hacks-0.1.0/pyrepl_hacks.egg-info/dependency_links.txt +1 -0
- pyrepl_hacks-0.1.0/pyrepl_hacks.egg-info/top_level.txt +1 -0
- pyrepl_hacks-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrepl-hacks
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hacky extensions and helper functions for the new Python REPL.
|
|
5
|
+
Requires-Python: <3.15,>=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# pyrepl-hacks
|
|
9
|
+
|
|
10
|
+
Hacky extensions and helper functions for the new Python REPL.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import pyrepl_hacks as repl
|
|
14
|
+
|
|
15
|
+
repl.bind("Alt+M", "move-to-indentation") # Move to first non-space in current line
|
|
16
|
+
repl.bind("Shift+Tab", "dedent") # Dedent the whole input
|
|
17
|
+
repl.bind("Alt+Down", "move-line-down") # Swap current line with next in block
|
|
18
|
+
repl.bind("Alt+Up", "move-line-up") # Swap current line with previous in block
|
|
19
|
+
repl.bind("Shift+Home", "home") # Move to first character in the input
|
|
20
|
+
repl.bind("Shift+End", "end") # Move to last character in the input
|
|
21
|
+
|
|
22
|
+
# Make Ctrl+N insert a specific list of numbers
|
|
23
|
+
repl.bind_to_insert("Ctrl+N", "[2, 1, 3, 4, 7, 11, 18, 29]")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@repl.bind(r"Ctrl+X Ctrl+R", with_event=True)
|
|
27
|
+
def subprocess_run(reader, event_name, event):
|
|
28
|
+
"""Ctrl+X followed by Ctrl+R will insert a subprocess.run command."""
|
|
29
|
+
reader.insert("import subprocess\n")
|
|
30
|
+
code = 'subprocess.run("", shell=True)'
|
|
31
|
+
reader.insert(code)
|
|
32
|
+
for _ in range(len(code) - code.index('""') - 1):
|
|
33
|
+
repl.commands.left(reader, event_name, event)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## ⚠️ WARNING: this could all break
|
|
38
|
+
|
|
39
|
+
This library relies on Python implementation details which may change in future Python versions.
|
|
40
|
+
|
|
41
|
+
This library uses the `_pyrepl` module (and optionally `_colorize`).
|
|
42
|
+
As the `_` prefix implies, these modules are not designed for public use.
|
|
43
|
+
|
|
44
|
+
That means that when you upgrade to a newer Python (for example Python 3.15) this code may break.
|
|
45
|
+
For that reason, the Python versions this package claims to work with are pinned to only known-to-be-working Python versions.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Installing
|
|
49
|
+
|
|
50
|
+
To install globally:
|
|
51
|
+
|
|
52
|
+
```console
|
|
53
|
+
pip install pyrepl-hacks
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then you can use it in [your `PYTHONSTARTUP` file][PYTHONSTARTUP]:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
def _main():
|
|
60
|
+
try:
|
|
61
|
+
import pyrepl_hacks as repl
|
|
62
|
+
except ImportError:
|
|
63
|
+
pass # We must be on Python 3.12 or earlier
|
|
64
|
+
else:
|
|
65
|
+
repl.bind("Alt+M", "move-to-indentation")
|
|
66
|
+
repl.bind("Shift+Tab", "dedent")
|
|
67
|
+
repl.bind("Alt+Down", "move-line-down")
|
|
68
|
+
repl.bind("Alt+Up", "move-line-up")
|
|
69
|
+
repl.bind("Shift+Home", "home")
|
|
70
|
+
repl.bind("Shift+End", "end")
|
|
71
|
+
|
|
72
|
+
repl.bind_to_insert("Ctrl+N", "[2, 1, 3, 4, 7, 11, 18, 29]")
|
|
73
|
+
|
|
74
|
+
_main()
|
|
75
|
+
del _main # Don't polute the global namespace in our REPL
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Note that this will only modify the REPL in environments where `pyrepl-hacks` is installed.
|
|
79
|
+
So if you want it everywhere, you would need to install `pyrepl-hacks` system-wide *and* in every virtual environment.
|
|
80
|
+
|
|
81
|
+
If you just want to play with this tool, try this:
|
|
82
|
+
|
|
83
|
+
```console
|
|
84
|
+
uvx --with pyrepl-hacks python
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Command Registering and Key Binding
|
|
89
|
+
|
|
90
|
+
This library includes features for easily registering and binding new REPL commands.
|
|
91
|
+
|
|
92
|
+
### Binding to existing commands
|
|
93
|
+
|
|
94
|
+
You can bind a key to an existing command:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import pyrepl_hacks as repl
|
|
98
|
+
|
|
99
|
+
repl.bind("Shift+Home", "home")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Inserting text with a binding
|
|
103
|
+
|
|
104
|
+
You can use the `bind_to_insert` helper to bind a key to insert specific text:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import pyrepl_hacks as repl
|
|
108
|
+
|
|
109
|
+
repl.bind_to_insert("Ctrl+P", "Python?!")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Registering new commands
|
|
113
|
+
|
|
114
|
+
Need something fancy that doesn't exist yet?
|
|
115
|
+
|
|
116
|
+
You can register a new command:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import pyrepl_hacks as repl
|
|
120
|
+
|
|
121
|
+
@repl.register_command
|
|
122
|
+
def exit(reader):
|
|
123
|
+
"""Exits Python immediately."""
|
|
124
|
+
import sys
|
|
125
|
+
sys.exit(0)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The `register_command` decorator will turn the `under_score` separated name into a `kebab-case` name by default.
|
|
129
|
+
|
|
130
|
+
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:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import pyrepl_hacks as repl
|
|
134
|
+
|
|
135
|
+
@repl.register_command("delete-line", with_event=True)
|
|
136
|
+
def delete_whole_line(reader, event_name, event):
|
|
137
|
+
"""Move to beginning of line and delete all text."""
|
|
138
|
+
reader.pos = reader.bol()
|
|
139
|
+
repl.commands.kill_line(reader, event_name, event)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
After commands have been registered, they can be used with the `bind` function to bind them to specific keys:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import pyrepl_hacks as repl
|
|
146
|
+
|
|
147
|
+
repl.bind("F4", "exit")
|
|
148
|
+
repl.bind("Ctrl+X Ctrl+D", "delete-line")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Binding keys while registering
|
|
152
|
+
|
|
153
|
+
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:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import pyrepl_hacks as repl
|
|
157
|
+
|
|
158
|
+
@repl.bind("F4")
|
|
159
|
+
def exit(reader):
|
|
160
|
+
"""Exits Python immediately."""
|
|
161
|
+
import sys
|
|
162
|
+
sys.exit(0)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
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`.
|
|
166
|
+
|
|
167
|
+
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.
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
## Available Commands
|
|
171
|
+
|
|
172
|
+
Here are some of the interesting commands provided by Python (in `_pyrepl.commands`):
|
|
173
|
+
|
|
174
|
+
- `clear-screen`: Clear screen (`Ctrl+L`)
|
|
175
|
+
- `accept`: Run current code block (`Alt+Enter`)
|
|
176
|
+
- `beginning-of-line`: Move cursor to the first character of the current line (`Ctrl+A`)
|
|
177
|
+
- `end-of-line`: Move cursor to the last character of the current line (`Ctrl+E`)
|
|
178
|
+
- `home`: Move cursor the first character in the code block
|
|
179
|
+
- `end`: Move cursor the last character in the code block
|
|
180
|
+
- `kill-line`: Delete to end of line (`Ctrl+K`)
|
|
181
|
+
- `unix-line-discard`: Delete to beginning of line (`Ctrl+U`)
|
|
182
|
+
- `backward-word`: Move cursor back one word (`Ctrl+Left`)
|
|
183
|
+
- `forward-word`: Move cursor forward one word (`Ctrl+Right`)
|
|
184
|
+
- `backward-kill-word`: Delete to beginning of word (`Alt+Backspace`)
|
|
185
|
+
- `kill-word`: Delete to end of word (`Alt+D`)
|
|
186
|
+
|
|
187
|
+
This `pyrepl-hacks` project provides some additional commands as well:
|
|
188
|
+
|
|
189
|
+
- `move-to-indentation`: Move to first non-space in current line
|
|
190
|
+
- `dedent`: Dedent the whole code block
|
|
191
|
+
- `move-line-down`: Swap current line with next one in the block
|
|
192
|
+
- `move-line-up`: Swap current line with previous one in the block
|
|
193
|
+
|
|
194
|
+
These 4 additional commands have no key bindings by default.
|
|
195
|
+
|
|
196
|
+
I recommend binding these commands as well as the `home` and `end` commands (provided by `_pyrepl.commands`) which are also unbound by default:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
repl.bind("Alt+M", "move-to-indentation") # Move to first non-space in current line
|
|
200
|
+
repl.bind("Shift+Tab", "dedent") # Dedent the whole input
|
|
201
|
+
repl.bind("Alt+Down", "move-line-down") # Swap current line with next in block
|
|
202
|
+
repl.bind("Alt+Up", "move-line-up") # Swap current line with previous in block
|
|
203
|
+
repl.bind("Shift+Home", "home") # Move to first character in the input
|
|
204
|
+
repl.bind("Shift+End", "end") # Move to last character in the input
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Note that these custom REPL commands and all existing commands provided by `_pyrepl.commands` include wrapper functions in the `commands` submodule.
|
|
208
|
+
These functions are named the same as their command name, except `-` must be replaced by `_`:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from repl.commands import move_to_indentation, clear_screen
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
## The Future is Obsolescence?
|
|
216
|
+
|
|
217
|
+
My hope is that this package will be obsolete one day.
|
|
218
|
+
|
|
219
|
+
I hope that Python will eventually include an official interface for creating new REPL commands and binding keys to commands.
|
|
220
|
+
|
|
221
|
+
I also hope that some (or all?) of the 4 new commands this module includes will eventually be included with Python by default.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
[PYTHONSTARTUP]: https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# pyrepl-hacks
|
|
2
|
+
|
|
3
|
+
Hacky extensions and helper functions for the new Python REPL.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import pyrepl_hacks as repl
|
|
7
|
+
|
|
8
|
+
repl.bind("Alt+M", "move-to-indentation") # Move to first non-space in current line
|
|
9
|
+
repl.bind("Shift+Tab", "dedent") # Dedent the whole input
|
|
10
|
+
repl.bind("Alt+Down", "move-line-down") # Swap current line with next in block
|
|
11
|
+
repl.bind("Alt+Up", "move-line-up") # Swap current line with previous in block
|
|
12
|
+
repl.bind("Shift+Home", "home") # Move to first character in the input
|
|
13
|
+
repl.bind("Shift+End", "end") # Move to last character in the input
|
|
14
|
+
|
|
15
|
+
# Make Ctrl+N insert a specific list of numbers
|
|
16
|
+
repl.bind_to_insert("Ctrl+N", "[2, 1, 3, 4, 7, 11, 18, 29]")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@repl.bind(r"Ctrl+X Ctrl+R", with_event=True)
|
|
20
|
+
def subprocess_run(reader, event_name, event):
|
|
21
|
+
"""Ctrl+X followed by Ctrl+R will insert a subprocess.run command."""
|
|
22
|
+
reader.insert("import subprocess\n")
|
|
23
|
+
code = 'subprocess.run("", shell=True)'
|
|
24
|
+
reader.insert(code)
|
|
25
|
+
for _ in range(len(code) - code.index('""') - 1):
|
|
26
|
+
repl.commands.left(reader, event_name, event)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## ⚠️ WARNING: this could all break
|
|
31
|
+
|
|
32
|
+
This library relies on Python implementation details which may change in future Python versions.
|
|
33
|
+
|
|
34
|
+
This library uses the `_pyrepl` module (and optionally `_colorize`).
|
|
35
|
+
As the `_` prefix implies, these modules are not designed for public use.
|
|
36
|
+
|
|
37
|
+
That means that when you upgrade to a newer Python (for example Python 3.15) this code may break.
|
|
38
|
+
For that reason, the Python versions this package claims to work with are pinned to only known-to-be-working Python versions.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Installing
|
|
42
|
+
|
|
43
|
+
To install globally:
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
pip install pyrepl-hacks
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then you can use it in [your `PYTHONSTARTUP` file][PYTHONSTARTUP]:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
def _main():
|
|
53
|
+
try:
|
|
54
|
+
import pyrepl_hacks as repl
|
|
55
|
+
except ImportError:
|
|
56
|
+
pass # We must be on Python 3.12 or earlier
|
|
57
|
+
else:
|
|
58
|
+
repl.bind("Alt+M", "move-to-indentation")
|
|
59
|
+
repl.bind("Shift+Tab", "dedent")
|
|
60
|
+
repl.bind("Alt+Down", "move-line-down")
|
|
61
|
+
repl.bind("Alt+Up", "move-line-up")
|
|
62
|
+
repl.bind("Shift+Home", "home")
|
|
63
|
+
repl.bind("Shift+End", "end")
|
|
64
|
+
|
|
65
|
+
repl.bind_to_insert("Ctrl+N", "[2, 1, 3, 4, 7, 11, 18, 29]")
|
|
66
|
+
|
|
67
|
+
_main()
|
|
68
|
+
del _main # Don't polute the global namespace in our REPL
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Note that this will only modify the REPL in environments where `pyrepl-hacks` is installed.
|
|
72
|
+
So if you want it everywhere, you would need to install `pyrepl-hacks` system-wide *and* in every virtual environment.
|
|
73
|
+
|
|
74
|
+
If you just want to play with this tool, try this:
|
|
75
|
+
|
|
76
|
+
```console
|
|
77
|
+
uvx --with pyrepl-hacks python
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## Command Registering and Key Binding
|
|
82
|
+
|
|
83
|
+
This library includes features for easily registering and binding new REPL commands.
|
|
84
|
+
|
|
85
|
+
### Binding to existing commands
|
|
86
|
+
|
|
87
|
+
You can bind a key to an existing command:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import pyrepl_hacks as repl
|
|
91
|
+
|
|
92
|
+
repl.bind("Shift+Home", "home")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Inserting text with a binding
|
|
96
|
+
|
|
97
|
+
You can use the `bind_to_insert` helper to bind a key to insert specific text:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import pyrepl_hacks as repl
|
|
101
|
+
|
|
102
|
+
repl.bind_to_insert("Ctrl+P", "Python?!")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Registering new commands
|
|
106
|
+
|
|
107
|
+
Need something fancy that doesn't exist yet?
|
|
108
|
+
|
|
109
|
+
You can register a new command:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import pyrepl_hacks as repl
|
|
113
|
+
|
|
114
|
+
@repl.register_command
|
|
115
|
+
def exit(reader):
|
|
116
|
+
"""Exits Python immediately."""
|
|
117
|
+
import sys
|
|
118
|
+
sys.exit(0)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The `register_command` decorator will turn the `under_score` separated name into a `kebab-case` name by default.
|
|
122
|
+
|
|
123
|
+
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:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import pyrepl_hacks as repl
|
|
127
|
+
|
|
128
|
+
@repl.register_command("delete-line", with_event=True)
|
|
129
|
+
def delete_whole_line(reader, event_name, event):
|
|
130
|
+
"""Move to beginning of line and delete all text."""
|
|
131
|
+
reader.pos = reader.bol()
|
|
132
|
+
repl.commands.kill_line(reader, event_name, event)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
After commands have been registered, they can be used with the `bind` function to bind them to specific keys:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
import pyrepl_hacks as repl
|
|
139
|
+
|
|
140
|
+
repl.bind("F4", "exit")
|
|
141
|
+
repl.bind("Ctrl+X Ctrl+D", "delete-line")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Binding keys while registering
|
|
145
|
+
|
|
146
|
+
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:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import pyrepl_hacks as repl
|
|
150
|
+
|
|
151
|
+
@repl.bind("F4")
|
|
152
|
+
def exit(reader):
|
|
153
|
+
"""Exits Python immediately."""
|
|
154
|
+
import sys
|
|
155
|
+
sys.exit(0)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
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`.
|
|
159
|
+
|
|
160
|
+
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.
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
## Available Commands
|
|
164
|
+
|
|
165
|
+
Here are some of the interesting commands provided by Python (in `_pyrepl.commands`):
|
|
166
|
+
|
|
167
|
+
- `clear-screen`: Clear screen (`Ctrl+L`)
|
|
168
|
+
- `accept`: Run current code block (`Alt+Enter`)
|
|
169
|
+
- `beginning-of-line`: Move cursor to the first character of the current line (`Ctrl+A`)
|
|
170
|
+
- `end-of-line`: Move cursor to the last character of the current line (`Ctrl+E`)
|
|
171
|
+
- `home`: Move cursor the first character in the code block
|
|
172
|
+
- `end`: Move cursor the last character in the code block
|
|
173
|
+
- `kill-line`: Delete to end of line (`Ctrl+K`)
|
|
174
|
+
- `unix-line-discard`: Delete to beginning of line (`Ctrl+U`)
|
|
175
|
+
- `backward-word`: Move cursor back one word (`Ctrl+Left`)
|
|
176
|
+
- `forward-word`: Move cursor forward one word (`Ctrl+Right`)
|
|
177
|
+
- `backward-kill-word`: Delete to beginning of word (`Alt+Backspace`)
|
|
178
|
+
- `kill-word`: Delete to end of word (`Alt+D`)
|
|
179
|
+
|
|
180
|
+
This `pyrepl-hacks` project provides some additional commands as well:
|
|
181
|
+
|
|
182
|
+
- `move-to-indentation`: Move to first non-space in current line
|
|
183
|
+
- `dedent`: Dedent the whole code block
|
|
184
|
+
- `move-line-down`: Swap current line with next one in the block
|
|
185
|
+
- `move-line-up`: Swap current line with previous one in the block
|
|
186
|
+
|
|
187
|
+
These 4 additional commands have no key bindings by default.
|
|
188
|
+
|
|
189
|
+
I recommend binding these commands as well as the `home` and `end` commands (provided by `_pyrepl.commands`) which are also unbound by default:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
repl.bind("Alt+M", "move-to-indentation") # Move to first non-space in current line
|
|
193
|
+
repl.bind("Shift+Tab", "dedent") # Dedent the whole input
|
|
194
|
+
repl.bind("Alt+Down", "move-line-down") # Swap current line with next in block
|
|
195
|
+
repl.bind("Alt+Up", "move-line-up") # Swap current line with previous in block
|
|
196
|
+
repl.bind("Shift+Home", "home") # Move to first character in the input
|
|
197
|
+
repl.bind("Shift+End", "end") # Move to last character in the input
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Note that these custom REPL commands and all existing commands provided by `_pyrepl.commands` include wrapper functions in the `commands` submodule.
|
|
201
|
+
These functions are named the same as their command name, except `-` must be replaced by `_`:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from repl.commands import move_to_indentation, clear_screen
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## The Future is Obsolescence?
|
|
209
|
+
|
|
210
|
+
My hope is that this package will be obsolete one day.
|
|
211
|
+
|
|
212
|
+
I hope that Python will eventually include an official interface for creating new REPL commands and binding keys to commands.
|
|
213
|
+
|
|
214
|
+
I also hope that some (or all?) of the 4 new commands this module includes will eventually be included with Python by default.
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
[PYTHONSTARTUP]: https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from _pyrepl.simple_interact import _get_reader
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from .command_utils import register_command
|
|
6
|
+
from .key_utils import slugify, to_keyspec
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = ["bind", "bind_to_insert"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _bind_decorator(keybinding: str, with_event: bool):
|
|
16
|
+
def decorator(command_function: Callable):
|
|
17
|
+
command = register_command(command_function, with_event=with_event)
|
|
18
|
+
return _bind_existing_command(keybinding, command.name)
|
|
19
|
+
return decorator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _bind_existing_command(keybinding: str, command_name: str = None):
|
|
23
|
+
keyspec = to_keyspec(keybinding)
|
|
24
|
+
logger.debug("binding: %s for %s", keyspec, command_name)
|
|
25
|
+
reader = _get_reader()
|
|
26
|
+
reader.bind(keyspec, command_name)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _bind_new_command(keybinding: str, command_name: str = None, command_function: Callable = None):
|
|
30
|
+
command = register_command(command_name)(command_function)
|
|
31
|
+
_bind_existing_command(keybinding, command_name)
|
|
32
|
+
return command
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def bind(
|
|
36
|
+
keybinding: str,
|
|
37
|
+
command_name: str = None,
|
|
38
|
+
command_function: Callable = None,
|
|
39
|
+
*,
|
|
40
|
+
with_event = False,
|
|
41
|
+
):
|
|
42
|
+
if command_function is not None:
|
|
43
|
+
return _bind_new_command(keybinding, command_name, command_function)
|
|
44
|
+
elif command_name is not None:
|
|
45
|
+
return _bind_existing_command(keybinding, command_name)
|
|
46
|
+
else:
|
|
47
|
+
return _bind_decorator(keybinding, with_event)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def bind_to_insert(keybinding: str, text: str):
|
|
51
|
+
def command_function(reader): reader.insert(text)
|
|
52
|
+
bind(keybinding, slugify(keybinding), command_function)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from _pyrepl.simple_interact import _get_reader
|
|
2
|
+
from _pyrepl.commands import Command
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
__all__ = ["register_command", "custom_commands"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def under_to_kebab(name):
|
|
10
|
+
"""Convert under_score_case to kebab-case."""
|
|
11
|
+
return name.replace("_", "-")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_command(command_name: str = None, /, *, with_event: bool = False):
|
|
15
|
+
def decorator(command_function: Callable):
|
|
16
|
+
name = command_name or under_to_kebab(command_function.__name__)
|
|
17
|
+
|
|
18
|
+
def do(self):
|
|
19
|
+
if with_event:
|
|
20
|
+
return command_function(self.reader, self.event_name, self.event)
|
|
21
|
+
else:
|
|
22
|
+
return command_function(self.reader)
|
|
23
|
+
command_class = type(
|
|
24
|
+
name,
|
|
25
|
+
(Command,),
|
|
26
|
+
{"do": do},
|
|
27
|
+
)
|
|
28
|
+
reader = _get_reader()
|
|
29
|
+
reader.commands[name] = command_class
|
|
30
|
+
command_function.command_class = command_class
|
|
31
|
+
command_function.name = name
|
|
32
|
+
return command_function
|
|
33
|
+
if isinstance(command_name, Callable):
|
|
34
|
+
command_function = command_name
|
|
35
|
+
command_name = None
|
|
36
|
+
return decorator(command_function)
|
|
37
|
+
else:
|
|
38
|
+
return decorator
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import textwrap
|
|
3
|
+
|
|
4
|
+
from .command_utils import register_command
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# _pyrepl.commands are also included later (see _add_pyrepl_commands)
|
|
8
|
+
__all__ = ["move_to_indentation", "dedent", "move_line_down", "move_line_up"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register_command
|
|
12
|
+
def move_to_indentation(reader):
|
|
13
|
+
"""Move to the start of indentation for the current line."""
|
|
14
|
+
x, y = reader.pos2xy()
|
|
15
|
+
lines = reader.get_unicode().splitlines(keepends=True)
|
|
16
|
+
line = lines[y]
|
|
17
|
+
if match := re.search(r"^\s+", line):
|
|
18
|
+
index = match.end()
|
|
19
|
+
else:
|
|
20
|
+
index = 0
|
|
21
|
+
reader.pos = reader.bol() + index
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@register_command
|
|
25
|
+
def dedent(reader):
|
|
26
|
+
"""Dedent the current code block."""
|
|
27
|
+
x, y = reader.pos2xy()
|
|
28
|
+
original_text = reader.get_unicode()
|
|
29
|
+
dedented_text = textwrap.dedent(original_text)
|
|
30
|
+
|
|
31
|
+
# Dedent buffer and invalidate cache
|
|
32
|
+
reader.buffer[:] = list(dedented_text)
|
|
33
|
+
reader.last_refresh_cache.invalidated = True
|
|
34
|
+
reader.dirty = True
|
|
35
|
+
|
|
36
|
+
# Reposition cursor correctly
|
|
37
|
+
original_lines = original_text.splitlines()
|
|
38
|
+
dedented_lines = dedented_text.splitlines()
|
|
39
|
+
removed_characters = sum(
|
|
40
|
+
len(old) - len(new)
|
|
41
|
+
for old, new in zip(original_lines[:y+1], dedented_lines)
|
|
42
|
+
)
|
|
43
|
+
reader.pos -= removed_characters
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@register_command
|
|
47
|
+
def move_line_down(reader):
|
|
48
|
+
"""Move the current line down."""
|
|
49
|
+
x, y = reader.pos2xy()
|
|
50
|
+
lines = reader.get_unicode().splitlines(keepends=True)
|
|
51
|
+
|
|
52
|
+
# Can't move down if we're on the last line
|
|
53
|
+
if y >= len(lines) - 1:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# Swap current line with next line
|
|
57
|
+
lines[y], lines[y+1] = lines[y+1], lines[y]
|
|
58
|
+
|
|
59
|
+
if not lines[y].endswith("\n"):
|
|
60
|
+
lines[y] += "\n"
|
|
61
|
+
|
|
62
|
+
# Update buffer with swapped lines
|
|
63
|
+
reader.buffer[:] = list("".join(lines))
|
|
64
|
+
reader.last_refresh_cache.invalidated = True
|
|
65
|
+
reader.dirty = True
|
|
66
|
+
|
|
67
|
+
# Move cursor to same column in the moved line (one line up)
|
|
68
|
+
reader.pos += len(lines[y])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@register_command
|
|
72
|
+
def move_line_up(reader):
|
|
73
|
+
"""Move the current line up."""
|
|
74
|
+
x, y = reader.pos2xy()
|
|
75
|
+
lines = reader.get_unicode().splitlines(keepends=True)
|
|
76
|
+
|
|
77
|
+
# Can't move up if we're on the first line
|
|
78
|
+
if y <= 0:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Swap current line with previous line
|
|
82
|
+
lines[y-1], lines[y] = lines[y], lines[y-1]
|
|
83
|
+
|
|
84
|
+
# Update buffer with swapped lines
|
|
85
|
+
reader.buffer[:] = list("".join(lines))
|
|
86
|
+
reader.last_refresh_cache.invalidated = True
|
|
87
|
+
reader.dirty = True
|
|
88
|
+
|
|
89
|
+
# Move cursor to same column in the moved line (one line up)
|
|
90
|
+
reader.pos -= len(lines[y])
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _add_pyrepl_commands():
|
|
94
|
+
"""Create simple command functions for all _pyrepl commands also."""
|
|
95
|
+
import _pyrepl.commands
|
|
96
|
+
from functools import wraps
|
|
97
|
+
for name, value in vars(_pyrepl.commands).items():
|
|
98
|
+
if (
|
|
99
|
+
isinstance(value, type)
|
|
100
|
+
and issubclass(value, _pyrepl.commands.Command)
|
|
101
|
+
and hasattr(value, "do")
|
|
102
|
+
):
|
|
103
|
+
def wrapper(command_class):
|
|
104
|
+
@wraps(value, assigned=["__name__", "__doc__"], updated=[])
|
|
105
|
+
def command_function(reader, event_name, event):
|
|
106
|
+
return command_class(reader, event_name, event).do()
|
|
107
|
+
return command_function
|
|
108
|
+
globals()[name] = wrapper(value)
|
|
109
|
+
__all__.append(name)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_add_pyrepl_commands()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
bindings_to_specs = {
|
|
5
|
+
"ctrl": r"\C",
|
|
6
|
+
"alt": r"\M",
|
|
7
|
+
"pgup": r"\<page up>",
|
|
8
|
+
"pgdn": r"\<page down>",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Cases that can't be handled by \C- or \M- notation
|
|
13
|
+
SPECIAL_CASES = {
|
|
14
|
+
"alt+up": r"\e[1;3A",
|
|
15
|
+
"alt+down": r"\e[1;3B",
|
|
16
|
+
"alt+right": r"\e[1;3C",
|
|
17
|
+
"alt+left": r"\e[1;3D",
|
|
18
|
+
"shift+tab": r"\e[Z",
|
|
19
|
+
"shift+up": r"\e[1;2A",
|
|
20
|
+
"shift+down": r"\e[1;2B",
|
|
21
|
+
"shift+right": r"\e[1;2C",
|
|
22
|
+
"shift+left": r"\e[1;2D",
|
|
23
|
+
"shift+home": r"\e[1;2H",
|
|
24
|
+
"shift+end": r"\e[1;2F",
|
|
25
|
+
"shift+pageup": r"\e[5;2~",
|
|
26
|
+
"shift+pagedown": r"\e[6;2~",
|
|
27
|
+
"shift+pgup": r"\e[5;2~",
|
|
28
|
+
"shift+pgdn": r"\e[6;2~",
|
|
29
|
+
"shift+insert": r"\e[2;2~",
|
|
30
|
+
"shift+delete": r"\e[3;2~",
|
|
31
|
+
# Add more as we discover them
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def slugify(keybinding):
|
|
36
|
+
"""Create unique slug for keybinding."""
|
|
37
|
+
return "_" + "".join(
|
|
38
|
+
c if c.isalnum() else "_"
|
|
39
|
+
for c in keybinding
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def to_keyspec(keybinding: str):
|
|
44
|
+
r"""Convert human-readable bindings to specs (e.g. Ctrl+A to \C-a)."""
|
|
45
|
+
normalized = keybinding.lower().strip()
|
|
46
|
+
if normalized in SPECIAL_CASES:
|
|
47
|
+
return SPECIAL_CASES[normalized]
|
|
48
|
+
spec = ""
|
|
49
|
+
for section in normalized.split():
|
|
50
|
+
spec += "-".join([
|
|
51
|
+
bindings_to_specs.get(part, rf"\<{part}>") if len(part) != 1 else part
|
|
52
|
+
for part in section.split("+")
|
|
53
|
+
])
|
|
54
|
+
return spec
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
def _convert_color(color):
|
|
2
|
+
"""Convert strings like 'reset, intense blue' into valid color."""
|
|
3
|
+
subcolors = color.split(",")
|
|
4
|
+
return "".join(
|
|
5
|
+
getattr(ANSIColors, c.strip().replace(" ", "_").upper())
|
|
6
|
+
for c in subcolors
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def update_theme(**kwargs):
|
|
11
|
+
from _colorize import set_theme, default_theme, Syntax, ANSIColors
|
|
12
|
+
items = {
|
|
13
|
+
name: _convert_color(color)
|
|
14
|
+
for name, color in kwargs.items()
|
|
15
|
+
}
|
|
16
|
+
new_theme = default_theme.copy_with(syntax=Syntax(**items))
|
|
17
|
+
set_theme(new_theme)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrepl-hacks
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hacky extensions and helper functions for the new Python REPL.
|
|
5
|
+
Requires-Python: <3.15,>=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# pyrepl-hacks
|
|
9
|
+
|
|
10
|
+
Hacky extensions and helper functions for the new Python REPL.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import pyrepl_hacks as repl
|
|
14
|
+
|
|
15
|
+
repl.bind("Alt+M", "move-to-indentation") # Move to first non-space in current line
|
|
16
|
+
repl.bind("Shift+Tab", "dedent") # Dedent the whole input
|
|
17
|
+
repl.bind("Alt+Down", "move-line-down") # Swap current line with next in block
|
|
18
|
+
repl.bind("Alt+Up", "move-line-up") # Swap current line with previous in block
|
|
19
|
+
repl.bind("Shift+Home", "home") # Move to first character in the input
|
|
20
|
+
repl.bind("Shift+End", "end") # Move to last character in the input
|
|
21
|
+
|
|
22
|
+
# Make Ctrl+N insert a specific list of numbers
|
|
23
|
+
repl.bind_to_insert("Ctrl+N", "[2, 1, 3, 4, 7, 11, 18, 29]")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@repl.bind(r"Ctrl+X Ctrl+R", with_event=True)
|
|
27
|
+
def subprocess_run(reader, event_name, event):
|
|
28
|
+
"""Ctrl+X followed by Ctrl+R will insert a subprocess.run command."""
|
|
29
|
+
reader.insert("import subprocess\n")
|
|
30
|
+
code = 'subprocess.run("", shell=True)'
|
|
31
|
+
reader.insert(code)
|
|
32
|
+
for _ in range(len(code) - code.index('""') - 1):
|
|
33
|
+
repl.commands.left(reader, event_name, event)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## ⚠️ WARNING: this could all break
|
|
38
|
+
|
|
39
|
+
This library relies on Python implementation details which may change in future Python versions.
|
|
40
|
+
|
|
41
|
+
This library uses the `_pyrepl` module (and optionally `_colorize`).
|
|
42
|
+
As the `_` prefix implies, these modules are not designed for public use.
|
|
43
|
+
|
|
44
|
+
That means that when you upgrade to a newer Python (for example Python 3.15) this code may break.
|
|
45
|
+
For that reason, the Python versions this package claims to work with are pinned to only known-to-be-working Python versions.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Installing
|
|
49
|
+
|
|
50
|
+
To install globally:
|
|
51
|
+
|
|
52
|
+
```console
|
|
53
|
+
pip install pyrepl-hacks
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then you can use it in [your `PYTHONSTARTUP` file][PYTHONSTARTUP]:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
def _main():
|
|
60
|
+
try:
|
|
61
|
+
import pyrepl_hacks as repl
|
|
62
|
+
except ImportError:
|
|
63
|
+
pass # We must be on Python 3.12 or earlier
|
|
64
|
+
else:
|
|
65
|
+
repl.bind("Alt+M", "move-to-indentation")
|
|
66
|
+
repl.bind("Shift+Tab", "dedent")
|
|
67
|
+
repl.bind("Alt+Down", "move-line-down")
|
|
68
|
+
repl.bind("Alt+Up", "move-line-up")
|
|
69
|
+
repl.bind("Shift+Home", "home")
|
|
70
|
+
repl.bind("Shift+End", "end")
|
|
71
|
+
|
|
72
|
+
repl.bind_to_insert("Ctrl+N", "[2, 1, 3, 4, 7, 11, 18, 29]")
|
|
73
|
+
|
|
74
|
+
_main()
|
|
75
|
+
del _main # Don't polute the global namespace in our REPL
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Note that this will only modify the REPL in environments where `pyrepl-hacks` is installed.
|
|
79
|
+
So if you want it everywhere, you would need to install `pyrepl-hacks` system-wide *and* in every virtual environment.
|
|
80
|
+
|
|
81
|
+
If you just want to play with this tool, try this:
|
|
82
|
+
|
|
83
|
+
```console
|
|
84
|
+
uvx --with pyrepl-hacks python
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Command Registering and Key Binding
|
|
89
|
+
|
|
90
|
+
This library includes features for easily registering and binding new REPL commands.
|
|
91
|
+
|
|
92
|
+
### Binding to existing commands
|
|
93
|
+
|
|
94
|
+
You can bind a key to an existing command:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import pyrepl_hacks as repl
|
|
98
|
+
|
|
99
|
+
repl.bind("Shift+Home", "home")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Inserting text with a binding
|
|
103
|
+
|
|
104
|
+
You can use the `bind_to_insert` helper to bind a key to insert specific text:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import pyrepl_hacks as repl
|
|
108
|
+
|
|
109
|
+
repl.bind_to_insert("Ctrl+P", "Python?!")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Registering new commands
|
|
113
|
+
|
|
114
|
+
Need something fancy that doesn't exist yet?
|
|
115
|
+
|
|
116
|
+
You can register a new command:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import pyrepl_hacks as repl
|
|
120
|
+
|
|
121
|
+
@repl.register_command
|
|
122
|
+
def exit(reader):
|
|
123
|
+
"""Exits Python immediately."""
|
|
124
|
+
import sys
|
|
125
|
+
sys.exit(0)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The `register_command` decorator will turn the `under_score` separated name into a `kebab-case` name by default.
|
|
129
|
+
|
|
130
|
+
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:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import pyrepl_hacks as repl
|
|
134
|
+
|
|
135
|
+
@repl.register_command("delete-line", with_event=True)
|
|
136
|
+
def delete_whole_line(reader, event_name, event):
|
|
137
|
+
"""Move to beginning of line and delete all text."""
|
|
138
|
+
reader.pos = reader.bol()
|
|
139
|
+
repl.commands.kill_line(reader, event_name, event)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
After commands have been registered, they can be used with the `bind` function to bind them to specific keys:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import pyrepl_hacks as repl
|
|
146
|
+
|
|
147
|
+
repl.bind("F4", "exit")
|
|
148
|
+
repl.bind("Ctrl+X Ctrl+D", "delete-line")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Binding keys while registering
|
|
152
|
+
|
|
153
|
+
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:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import pyrepl_hacks as repl
|
|
157
|
+
|
|
158
|
+
@repl.bind("F4")
|
|
159
|
+
def exit(reader):
|
|
160
|
+
"""Exits Python immediately."""
|
|
161
|
+
import sys
|
|
162
|
+
sys.exit(0)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
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`.
|
|
166
|
+
|
|
167
|
+
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.
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
## Available Commands
|
|
171
|
+
|
|
172
|
+
Here are some of the interesting commands provided by Python (in `_pyrepl.commands`):
|
|
173
|
+
|
|
174
|
+
- `clear-screen`: Clear screen (`Ctrl+L`)
|
|
175
|
+
- `accept`: Run current code block (`Alt+Enter`)
|
|
176
|
+
- `beginning-of-line`: Move cursor to the first character of the current line (`Ctrl+A`)
|
|
177
|
+
- `end-of-line`: Move cursor to the last character of the current line (`Ctrl+E`)
|
|
178
|
+
- `home`: Move cursor the first character in the code block
|
|
179
|
+
- `end`: Move cursor the last character in the code block
|
|
180
|
+
- `kill-line`: Delete to end of line (`Ctrl+K`)
|
|
181
|
+
- `unix-line-discard`: Delete to beginning of line (`Ctrl+U`)
|
|
182
|
+
- `backward-word`: Move cursor back one word (`Ctrl+Left`)
|
|
183
|
+
- `forward-word`: Move cursor forward one word (`Ctrl+Right`)
|
|
184
|
+
- `backward-kill-word`: Delete to beginning of word (`Alt+Backspace`)
|
|
185
|
+
- `kill-word`: Delete to end of word (`Alt+D`)
|
|
186
|
+
|
|
187
|
+
This `pyrepl-hacks` project provides some additional commands as well:
|
|
188
|
+
|
|
189
|
+
- `move-to-indentation`: Move to first non-space in current line
|
|
190
|
+
- `dedent`: Dedent the whole code block
|
|
191
|
+
- `move-line-down`: Swap current line with next one in the block
|
|
192
|
+
- `move-line-up`: Swap current line with previous one in the block
|
|
193
|
+
|
|
194
|
+
These 4 additional commands have no key bindings by default.
|
|
195
|
+
|
|
196
|
+
I recommend binding these commands as well as the `home` and `end` commands (provided by `_pyrepl.commands`) which are also unbound by default:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
repl.bind("Alt+M", "move-to-indentation") # Move to first non-space in current line
|
|
200
|
+
repl.bind("Shift+Tab", "dedent") # Dedent the whole input
|
|
201
|
+
repl.bind("Alt+Down", "move-line-down") # Swap current line with next in block
|
|
202
|
+
repl.bind("Alt+Up", "move-line-up") # Swap current line with previous in block
|
|
203
|
+
repl.bind("Shift+Home", "home") # Move to first character in the input
|
|
204
|
+
repl.bind("Shift+End", "end") # Move to last character in the input
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Note that these custom REPL commands and all existing commands provided by `_pyrepl.commands` include wrapper functions in the `commands` submodule.
|
|
208
|
+
These functions are named the same as their command name, except `-` must be replaced by `_`:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from repl.commands import move_to_indentation, clear_screen
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
## The Future is Obsolescence?
|
|
216
|
+
|
|
217
|
+
My hope is that this package will be obsolete one day.
|
|
218
|
+
|
|
219
|
+
I hope that Python will eventually include an official interface for creating new REPL commands and binding keys to commands.
|
|
220
|
+
|
|
221
|
+
I also hope that some (or all?) of the 4 new commands this module includes will eventually be included with Python by default.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
[PYTHONSTARTUP]: https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pyrepl_hacks/__init__.py
|
|
4
|
+
pyrepl_hacks/bind_utils.py
|
|
5
|
+
pyrepl_hacks/command_utils.py
|
|
6
|
+
pyrepl_hacks/commands.py
|
|
7
|
+
pyrepl_hacks/key_utils.py
|
|
8
|
+
pyrepl_hacks/theme_utils.py
|
|
9
|
+
pyrepl_hacks.egg-info/PKG-INFO
|
|
10
|
+
pyrepl_hacks.egg-info/SOURCES.txt
|
|
11
|
+
pyrepl_hacks.egg-info/dependency_links.txt
|
|
12
|
+
pyrepl_hacks.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyrepl_hacks
|