ipython-postfix-completion 0.2.0__py3-none-any.whl → 0.2.1__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.
- ipython_postfix_completion/__init__.py +141 -16
- ipython_postfix_completion-0.2.1.dist-info/METADATA +115 -0
- ipython_postfix_completion-0.2.1.dist-info/RECORD +6 -0
- ipython_postfix_completion-0.2.0.dist-info/METADATA +0 -282
- ipython_postfix_completion-0.2.0.dist-info/RECORD +0 -6
- {ipython_postfix_completion-0.2.0.dist-info → ipython_postfix_completion-0.2.1.dist-info}/WHEEL +0 -0
- {ipython_postfix_completion-0.2.0.dist-info → ipython_postfix_completion-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {ipython_postfix_completion-0.2.0.dist-info → ipython_postfix_completion-0.2.1.dist-info}/top_level.txt +0 -0
|
@@ -35,6 +35,19 @@ _NO_MAGIC_ATTR = object()
|
|
|
35
35
|
_ACTIVE_STATE: PostfixState | None = None
|
|
36
36
|
_VAR_TEMPLATE = "key = {expr}"
|
|
37
37
|
_VAR_PLACEHOLDER = "key"
|
|
38
|
+
_FOR_TEMPLATE = "for item in {expr}:\n{indent} pass"
|
|
39
|
+
_FORI_TEMPLATE = "for i, value in enumerate({expr}):\n{indent} pass"
|
|
40
|
+
_IF_TEMPLATE = "if {expr}:\n{indent} pass"
|
|
41
|
+
_EF_TEMPLATE = (
|
|
42
|
+
"if {expr}:\n{indent} pass\n{indent}elif cond:\n{indent} pass"
|
|
43
|
+
"\n{indent}else:\n{indent} pass"
|
|
44
|
+
)
|
|
45
|
+
_TEMPLATE_PLACEHOLDERS = {
|
|
46
|
+
"for": (_FOR_TEMPLATE, ("item", "pass")),
|
|
47
|
+
"fori": (_FORI_TEMPLATE, ("i", "value", "pass")),
|
|
48
|
+
"if": (_IF_TEMPLATE, ("pass",)),
|
|
49
|
+
"ef": (_EF_TEMPLATE, ("pass", "cond", "pass", "pass")),
|
|
50
|
+
}
|
|
38
51
|
_PLACEHOLDER_ATTR = "_postfix_completion_placeholder"
|
|
39
52
|
_OPENING_BRACKETS = {"(": ")", "[": "]", "{": "}"}
|
|
40
53
|
_CLOSING_BRACKETS = {closer: opener for opener, closer in _OPENING_BRACKETS.items()}
|
|
@@ -59,8 +72,11 @@ DEFAULT_TEMPLATES: dict[str, str] = {
|
|
|
59
72
|
"var": _VAR_TEMPLATE,
|
|
60
73
|
"await": "await {expr}",
|
|
61
74
|
"return": "return {expr}",
|
|
62
|
-
"if":
|
|
75
|
+
"if": _IF_TEMPLATE,
|
|
63
76
|
"while": "while {expr}:\n{indent} ",
|
|
77
|
+
"for": _FOR_TEMPLATE,
|
|
78
|
+
"fori": _FORI_TEMPLATE,
|
|
79
|
+
"ef": _EF_TEMPLATE,
|
|
64
80
|
"raise": "raise {expr}",
|
|
65
81
|
"yield": "yield {expr}",
|
|
66
82
|
"str": "str({expr})",
|
|
@@ -68,6 +84,8 @@ DEFAULT_TEMPLATES: dict[str, str] = {
|
|
|
68
84
|
"set": "set({expr})",
|
|
69
85
|
"dict": "dict({expr})",
|
|
70
86
|
"tuple": "tuple({expr})",
|
|
87
|
+
"type": "type({expr})",
|
|
88
|
+
"range": "range({expr})",
|
|
71
89
|
}
|
|
72
90
|
|
|
73
91
|
|
|
@@ -143,6 +161,30 @@ def _render_template(template: str, expr: str, indent: str = "") -> str:
|
|
|
143
161
|
return _FORMATTER.format(template, expr=expr, indent=indent)
|
|
144
162
|
|
|
145
163
|
|
|
164
|
+
def _is_integer_literal(expr: str) -> bool:
|
|
165
|
+
try:
|
|
166
|
+
node = ast.parse(expr, mode="eval").body
|
|
167
|
+
except SyntaxError:
|
|
168
|
+
return False
|
|
169
|
+
if isinstance(node, ast.Constant):
|
|
170
|
+
return type(node.value) is int
|
|
171
|
+
return (
|
|
172
|
+
isinstance(node, ast.UnaryOp)
|
|
173
|
+
and isinstance(node.op, (ast.UAdd, ast.USub))
|
|
174
|
+
and _is_integer_literal(ast.unparse(node.operand))
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _render_candidate(name: str, template: str, expr: str, indent: str = "") -> str:
|
|
179
|
+
wraps_integer = _is_integer_literal(expr) and (
|
|
180
|
+
(name == "for" and template == _FOR_TEMPLATE)
|
|
181
|
+
or (name == "fori" and template == _FORI_TEMPLATE)
|
|
182
|
+
)
|
|
183
|
+
if wraps_integer:
|
|
184
|
+
expr = f"range({expr})"
|
|
185
|
+
return _render_template(template, expr, indent)
|
|
186
|
+
|
|
187
|
+
|
|
146
188
|
@dataclass
|
|
147
189
|
class PostfixState:
|
|
148
190
|
matcher: Callable[[CompletionContext], dict]
|
|
@@ -244,7 +286,7 @@ def _effective_templates(state: PostfixState | None = None) -> dict[str, str]:
|
|
|
244
286
|
def _apply_template(
|
|
245
287
|
name: str, expr: str, indent: str = "", state: PostfixState | None = None
|
|
246
288
|
) -> str:
|
|
247
|
-
return
|
|
289
|
+
return _render_candidate(name, _effective_templates(state)[name], expr, indent)
|
|
248
290
|
|
|
249
291
|
|
|
250
292
|
def _postfix_candidates(
|
|
@@ -264,7 +306,7 @@ def _postfix_candidates(
|
|
|
264
306
|
if (name == prefix if exact else name.startswith(prefix))
|
|
265
307
|
]
|
|
266
308
|
return f"{expr}.{prefix}", [
|
|
267
|
-
|
|
309
|
+
_render_candidate(name, templates[name], expr, indent) for name in names
|
|
268
310
|
]
|
|
269
311
|
|
|
270
312
|
|
|
@@ -278,7 +320,7 @@ def _template_name(
|
|
|
278
320
|
for name, template in _effective_templates(state).items():
|
|
279
321
|
if not name.startswith(prefix):
|
|
280
322
|
continue
|
|
281
|
-
if
|
|
323
|
+
if _render_candidate(name, template, expr, indent) == expansion:
|
|
282
324
|
return name
|
|
283
325
|
return None
|
|
284
326
|
|
|
@@ -306,7 +348,28 @@ def _expand_buffer(buffer) -> bool:
|
|
|
306
348
|
buffer.delete_before_cursor(len(matched_fragment))
|
|
307
349
|
expansion_start = buffer.cursor_position
|
|
308
350
|
buffer.insert_text(candidates[0])
|
|
351
|
+
placeholder_config = (
|
|
352
|
+
_TEMPLATE_PLACEHOLDERS.get(parsed[2]) if parsed is not None else None
|
|
353
|
+
)
|
|
309
354
|
if (
|
|
355
|
+
parsed is not None
|
|
356
|
+
and placeholder_config is not None
|
|
357
|
+
and _effective_templates().get(parsed[2]) == placeholder_config[0]
|
|
358
|
+
):
|
|
359
|
+
search_from = 0
|
|
360
|
+
ranges = []
|
|
361
|
+
for placeholder_text in placeholder_config[1]:
|
|
362
|
+
start = candidates[0].index(placeholder_text, search_from)
|
|
363
|
+
end = start + len(placeholder_text)
|
|
364
|
+
ranges.append((start, end))
|
|
365
|
+
search_from = end
|
|
366
|
+
_select_placeholders(
|
|
367
|
+
buffer,
|
|
368
|
+
expansion_start,
|
|
369
|
+
ranges,
|
|
370
|
+
buffer.cursor_position,
|
|
371
|
+
)
|
|
372
|
+
elif (
|
|
310
373
|
parsed is not None
|
|
311
374
|
and parsed[2] == "var"
|
|
312
375
|
and _effective_templates().get("var") == _VAR_TEMPLATE
|
|
@@ -321,35 +384,93 @@ def _expand_buffer(buffer) -> bool:
|
|
|
321
384
|
|
|
322
385
|
|
|
323
386
|
def _select_placeholder(buffer, start: int, end: int, final_cursor: int) -> None:
|
|
387
|
+
_select_placeholders(buffer, start, [(start, end)], final_cursor, absolute=True)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _select_placeholders(
|
|
391
|
+
buffer,
|
|
392
|
+
start: int,
|
|
393
|
+
ranges: list[tuple[int, int]],
|
|
394
|
+
final_cursor: int,
|
|
395
|
+
*,
|
|
396
|
+
absolute: bool = False,
|
|
397
|
+
index: int = 0,
|
|
398
|
+
) -> None:
|
|
324
399
|
from prompt_toolkit.selection import SelectionState, SelectionType
|
|
325
400
|
|
|
326
|
-
|
|
327
|
-
|
|
401
|
+
if not absolute:
|
|
402
|
+
ranges = [(start + left, start + right) for left, right in ranges]
|
|
403
|
+
setattr(buffer, _PLACEHOLDER_ATTR, (ranges, index, final_cursor))
|
|
404
|
+
left, right = ranges[index]
|
|
405
|
+
buffer.cursor_position = right
|
|
406
|
+
buffer.selection_state = SelectionState(left, SelectionType.CHARACTERS)
|
|
328
407
|
buffer.selection_state.enter_shift_mode()
|
|
329
|
-
setattr(buffer, _PLACEHOLDER_ATTR, (start, end, final_cursor))
|
|
330
408
|
|
|
331
409
|
|
|
332
410
|
def _has_active_placeholder(buffer) -> bool:
|
|
333
411
|
placeholder = getattr(buffer, _PLACEHOLDER_ATTR, None)
|
|
334
412
|
selection = buffer.selection_state
|
|
335
|
-
if placeholder is None
|
|
413
|
+
if placeholder is None:
|
|
336
414
|
return False
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
415
|
+
ranges, index, _ = placeholder
|
|
416
|
+
start, end = ranges[index]
|
|
417
|
+
return (selection is None and len(ranges) > 1) or (
|
|
418
|
+
selection is not None
|
|
419
|
+
and selection.shift_mode
|
|
340
420
|
and selection.original_cursor_position == start
|
|
341
421
|
and buffer.cursor_position == end
|
|
342
|
-
and buffer.text[start:end] == _VAR_PLACEHOLDER
|
|
422
|
+
and (len(ranges) > 1 or buffer.text[start:end] == _VAR_PLACEHOLDER)
|
|
343
423
|
)
|
|
344
424
|
|
|
345
425
|
|
|
346
426
|
def _accept_placeholder(buffer) -> bool:
|
|
347
427
|
if not _has_active_placeholder(buffer):
|
|
348
428
|
return False
|
|
349
|
-
|
|
350
|
-
buffer.
|
|
351
|
-
|
|
352
|
-
|
|
429
|
+
ranges, index, final_cursor = _update_placeholder_after_edit(buffer)
|
|
430
|
+
if buffer.selection_state is not None:
|
|
431
|
+
buffer.exit_selection()
|
|
432
|
+
if index + 1 < len(ranges):
|
|
433
|
+
_select_placeholders(
|
|
434
|
+
buffer, 0, ranges, final_cursor, absolute=True, index=index + 1
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
delattr(buffer, _PLACEHOLDER_ATTR)
|
|
438
|
+
buffer.cursor_position = final_cursor
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _update_placeholder_after_edit(buffer):
|
|
443
|
+
ranges, index, final_cursor = getattr(buffer, _PLACEHOLDER_ATTR)
|
|
444
|
+
if buffer.selection_state is None:
|
|
445
|
+
start, old_end = ranges[index]
|
|
446
|
+
new_end = buffer.cursor_position
|
|
447
|
+
delta = new_end - old_end
|
|
448
|
+
final_cursor += delta
|
|
449
|
+
ranges = [
|
|
450
|
+
(start, new_end)
|
|
451
|
+
if stop == index
|
|
452
|
+
else (left + delta, right + delta)
|
|
453
|
+
if stop > index
|
|
454
|
+
else (left, right)
|
|
455
|
+
for stop, (left, right) in enumerate(ranges)
|
|
456
|
+
]
|
|
457
|
+
setattr(buffer, _PLACEHOLDER_ATTR, (ranges, index, final_cursor))
|
|
458
|
+
return ranges, index, final_cursor
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _reverse_placeholder(buffer) -> bool:
|
|
462
|
+
if not _has_active_placeholder(buffer):
|
|
463
|
+
return False
|
|
464
|
+
ranges, index, final_cursor = _update_placeholder_after_edit(buffer)
|
|
465
|
+
previous_index = max(0, index - 1)
|
|
466
|
+
_select_placeholders(
|
|
467
|
+
buffer,
|
|
468
|
+
0,
|
|
469
|
+
ranges,
|
|
470
|
+
final_cursor,
|
|
471
|
+
absolute=True,
|
|
472
|
+
index=previous_index,
|
|
473
|
+
)
|
|
353
474
|
return True
|
|
354
475
|
|
|
355
476
|
|
|
@@ -657,6 +778,10 @@ def _make_key_bindings(state: PostfixState | None = None):
|
|
|
657
778
|
def accept_placeholder(event) -> None:
|
|
658
779
|
_accept_placeholder(event.current_buffer)
|
|
659
780
|
|
|
781
|
+
@key_bindings.add("s-tab", filter=has_active_placeholder)
|
|
782
|
+
def reverse_placeholder(event) -> None:
|
|
783
|
+
_reverse_placeholder(event.current_buffer)
|
|
784
|
+
|
|
660
785
|
@key_bindings.add("enter", filter=has_active_placeholder)
|
|
661
786
|
def accept_placeholder_with_enter(event) -> None:
|
|
662
787
|
_accept_placeholder(event.current_buffer)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ipython-postfix-completion
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Configurable postfix completion extension for IPython.
|
|
5
|
+
Author: IPython Postfix Completion Contributors
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/fishandsheep/ipython-postfix-completion
|
|
8
|
+
Project-URL: Repository, https://github.com/fishandsheep/ipython-postfix-completion
|
|
9
|
+
Project-URL: Issues, https://github.com/fishandsheep/ipython-postfix-completion/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/fishandsheep/ipython-postfix-completion/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: ipython,completion,postfix,extension
|
|
12
|
+
Classifier: Framework :: IPython
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: ipython<10,>=9.0
|
|
25
|
+
Requires-Dist: traitlets>=5.13
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build; extra == "dev"
|
|
30
|
+
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
32
|
+
Requires-Dist: twine; extra == "dev"
|
|
33
|
+
Requires-Dist: ipython-postfix-completion[test]; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# IPython Postfix Completion
|
|
37
|
+
|
|
38
|
+
Complete Python expressions with postfix templates in IPython: type an
|
|
39
|
+
expression, a dot, and a template name, then press Tab.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool install ipython --with ipython-postfix-completion
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Start IPython and load the extension:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
%load_ext ipython_postfix_completion
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Common Templates
|
|
54
|
+
|
|
55
|
+
`items.for<Tab>` creates a loop. Tab selects `item`, then `pass`; type to
|
|
56
|
+
replace a selection.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
for item in items:
|
|
60
|
+
pass
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
An integer literal is wrapped in `range(...)`, so `10.for<Tab>` produces
|
|
64
|
+
`for item in range(10):`.
|
|
65
|
+
|
|
66
|
+
`items.fori<Tab>` creates an indexed loop. Tab selects `i`, `value`, then
|
|
67
|
+
`pass`. For an integer literal, it uses `enumerate(range(n))`.
|
|
68
|
+
|
|
69
|
+
`condition.if<Tab>` creates an `if` block with a selected `pass`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
if condition:
|
|
73
|
+
pass
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`condition.ef<Tab>` creates `if`/`elif`/`else` branches. Tab selects each
|
|
77
|
+
placeholder in order: `pass`, `cond`, `pass`, `pass`. The `cond` selection
|
|
78
|
+
does not include its colon.
|
|
79
|
+
|
|
80
|
+
Other built-ins: `print`, `len`, `not`, `par`, `var`, `await`, `return`,
|
|
81
|
+
`while`, `raise`, `yield`, `str`, `list`, `set`, `dict`, `tuple`, `type`, and
|
|
82
|
+
`range`. Use `%postfix_template list` to see their expansions.
|
|
83
|
+
|
|
84
|
+
Shift+Tab moves back to the previous placeholder. Custom Tab navigation is
|
|
85
|
+
supported in terminal IPython.
|
|
86
|
+
|
|
87
|
+
## Load Automatically
|
|
88
|
+
|
|
89
|
+
The default configuration file is:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
~/.ipython/profile_default/ipython_config.py
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Create it with:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
ipython profile create
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Add the extension and any persistent templates there:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
c.InteractiveShellApp.extensions = ["ipython_postfix_completion"]
|
|
105
|
+
c.PostfixCompletionConfig.templates = {
|
|
106
|
+
"debug": "print({expr}=)",
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`IPYTHONDIR` and `--ipython-dir` can change the configuration directory. Use
|
|
111
|
+
`ipython locate profile default` to find the active profile. See the [official
|
|
112
|
+
IPython configuration guide](https://ipython.readthedocs.io/en/stable/development/config.html).
|
|
113
|
+
|
|
114
|
+
For runtime template commands, detailed behavior, and contributor or release
|
|
115
|
+
steps, see [the extended guide](docs/REFERENCE.md).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
ipython_postfix_completion/__init__.py,sha256=OeNt29f_YF-MT01GxZu233B28Il7xUuWLOVLiSdWEPk,34444
|
|
2
|
+
ipython_postfix_completion-0.2.1.dist-info/licenses/LICENSE,sha256=omLR-7QW1K3l6AG7ih1N5L8Z58UY-ROrQL82ANN9PnM,1547
|
|
3
|
+
ipython_postfix_completion-0.2.1.dist-info/METADATA,sha256=uLGeW00R_fljuKcFU9TGUX8UUaC6B9EreQvi_qmfVnE,3655
|
|
4
|
+
ipython_postfix_completion-0.2.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
ipython_postfix_completion-0.2.1.dist-info/top_level.txt,sha256=W2sJCvsBheoCB2nbzJeD7-p5PR6lVDNicVLxNxmeraY,27
|
|
6
|
+
ipython_postfix_completion-0.2.1.dist-info/RECORD,,
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ipython-postfix-completion
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: Configurable postfix completion extension for IPython.
|
|
5
|
-
Author: IPython Postfix Completion Contributors
|
|
6
|
-
License-Expression: BSD-3-Clause
|
|
7
|
-
Project-URL: Homepage, https://github.com/fishandsheep/ipython-postfix-completion
|
|
8
|
-
Project-URL: Repository, https://github.com/fishandsheep/ipython-postfix-completion
|
|
9
|
-
Project-URL: Issues, https://github.com/fishandsheep/ipython-postfix-completion/issues
|
|
10
|
-
Project-URL: Changelog, https://github.com/fishandsheep/ipython-postfix-completion/blob/main/CHANGELOG.md
|
|
11
|
-
Keywords: ipython,completion,postfix,extension
|
|
12
|
-
Classifier: Framework :: IPython
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
-
Requires-Python: >=3.11
|
|
22
|
-
Description-Content-Type: text/markdown
|
|
23
|
-
License-File: LICENSE
|
|
24
|
-
Requires-Dist: ipython<10,>=9.0
|
|
25
|
-
Requires-Dist: traitlets>=5.13
|
|
26
|
-
Provides-Extra: test
|
|
27
|
-
Requires-Dist: pytest>=7; extra == "test"
|
|
28
|
-
Provides-Extra: dev
|
|
29
|
-
Requires-Dist: build; extra == "dev"
|
|
30
|
-
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
31
|
-
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
32
|
-
Requires-Dist: twine; extra == "dev"
|
|
33
|
-
Requires-Dist: ipython-postfix-completion[test]; extra == "dev"
|
|
34
|
-
Dynamic: license-file
|
|
35
|
-
|
|
36
|
-
# IPython Postfix Completion
|
|
37
|
-
|
|
38
|
-
Configurable postfix completion extension for IPython.
|
|
39
|
-
|
|
40
|
-
Package on PyPI: `ipython-postfix-completion`
|
|
41
|
-
|
|
42
|
-
## Install
|
|
43
|
-
|
|
44
|
-
Install into the current Python environment with `uv`:
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
uvx --with ipython-postfix-completion ipython
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Install into the current ipython environment with `uv`:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
uv tool install ipython --with ipython-postfix-completion
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Load
|
|
57
|
-
|
|
58
|
-
Inside IPython:
|
|
59
|
-
|
|
60
|
-
```python
|
|
61
|
-
%load_ext ipython_postfix_completion
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
To load it automatically, add this to `ipython_config.py`:
|
|
65
|
-
|
|
66
|
-
```python
|
|
67
|
-
c.InteractiveShellApp.extensions = ["ipython_postfix_completion"]
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Smart Tab key bindings are supported in terminal IPython. Postfix matcher
|
|
71
|
-
completion can also work in other IPython frontends, but this package does not
|
|
72
|
-
promise frontend-specific Tab behavior outside the terminal.
|
|
73
|
-
|
|
74
|
-
## Quick Example: Add a `for` Template
|
|
75
|
-
|
|
76
|
-
Add a template for the current IPython session:
|
|
77
|
-
|
|
78
|
-
```python
|
|
79
|
-
%postfix_template add for "for item in {expr}:\n{indent} "
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Use it:
|
|
83
|
-
|
|
84
|
-
```python
|
|
85
|
-
items.for<Tab>
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
It expands to:
|
|
89
|
-
|
|
90
|
-
```python
|
|
91
|
-
for item in items:
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Runtime templates only affect the current IPython session. Put templates in
|
|
96
|
-
`ipython_config.py` if you want them to persist.
|
|
97
|
-
|
|
98
|
-
## Runtime Magic
|
|
99
|
-
|
|
100
|
-
List effective templates:
|
|
101
|
-
|
|
102
|
-
```python
|
|
103
|
-
%postfix_template list
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Add or override a template for the current session:
|
|
107
|
-
|
|
108
|
-
```python
|
|
109
|
-
%postfix_template add debug "print({expr}=)"
|
|
110
|
-
%postfix_template add forin "for item in {expr}:\n{indent} "
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Disable a template for the current session:
|
|
114
|
-
|
|
115
|
-
```python
|
|
116
|
-
%postfix_template remove tuple
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Reset one runtime change:
|
|
120
|
-
|
|
121
|
-
```python
|
|
122
|
-
%postfix_template reset forin
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Reset all runtime changes:
|
|
126
|
-
|
|
127
|
-
```python
|
|
128
|
-
%postfix_template reset --all
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Persistent Config
|
|
132
|
-
|
|
133
|
-
Add persistent templates in `ipython_config.py`:
|
|
134
|
-
|
|
135
|
-
```python
|
|
136
|
-
c.PostfixCompletionConfig.templates = {
|
|
137
|
-
"debug": "print({expr}=)",
|
|
138
|
-
"forin": "for item in {expr}:\n{indent} ",
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
c.PostfixCompletionConfig.disabled_templates = ["tuple"]
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Smart Tab jump is enabled by default. Disable it while keeping postfix
|
|
145
|
-
completion with:
|
|
146
|
-
|
|
147
|
-
```python
|
|
148
|
-
c.PostfixCompletionConfig.smart_tab_jump = False
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Template names must match `[A-Za-z_][A-Za-z0-9_]*`.
|
|
152
|
-
|
|
153
|
-
Templates must include `{expr}` and may also use `{indent}`. No other template
|
|
154
|
-
fields are allowed.
|
|
155
|
-
|
|
156
|
-
## `.var` Placeholder
|
|
157
|
-
|
|
158
|
-
The built-in `.var` template creates an assignment and selects `key` as an
|
|
159
|
-
editable placeholder:
|
|
160
|
-
|
|
161
|
-
```text
|
|
162
|
-
"hello".var<Tab> -> key = "hello"
|
|
163
|
-
^^^ selected
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
While `key` remains selected:
|
|
167
|
-
|
|
168
|
-
- Tab accepts `key` and moves cursor to the end of the assignment.
|
|
169
|
-
- Enter behaves like Tab for this selection only; press Enter again to submit.
|
|
170
|
-
- Any other typed text replaces `key` with a custom variable name.
|
|
171
|
-
|
|
172
|
-
In 0.1.x, `.var` produced `expr = ` with the cursor after the assignment
|
|
173
|
-
target. Version 0.2.0 changes this to `key = expr` with an editable
|
|
174
|
-
placeholder; use a custom template if you need the old behavior.
|
|
175
|
-
|
|
176
|
-
## Smart Tab Jump
|
|
177
|
-
|
|
178
|
-
When cursor is immediately before a valid Python closing token, Tab moves over
|
|
179
|
-
it without changing source text. Repeated Tab presses exit nested constructs:
|
|
180
|
-
|
|
181
|
-
```text
|
|
182
|
-
"hello|" -> "hello"|
|
|
183
|
-
print("hello|") -> print("hello"|) -> print("hello")|
|
|
184
|
-
print(f"{name|}") -> print(f"{name}|") -> print(f"{name}"|) -> print(f"{name}")|
|
|
185
|
-
items[index|] -> items[index]|
|
|
186
|
-
list[dict[str, int|]] -> list[dict[str, int]|] -> list[dict[str, int]]|
|
|
187
|
-
{"name": value|} -> {"name": value}|
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
`|` marks cursor and is not typed. Supported closers are single and triple
|
|
191
|
-
quotes plus `)`, `]`, and `}`. Detection follows Python tokens, including
|
|
192
|
-
multiline input, string prefixes, and f-string expressions. Tab still accepts
|
|
193
|
-
the `.var` name selection or an exact postfix template first; otherwise it
|
|
194
|
-
falls back to IPython completion or indentation. Ambiguous `< >`, colon, and
|
|
195
|
-
comma are intentionally excluded.
|
|
196
|
-
|
|
197
|
-
## Built-in Templates
|
|
198
|
-
|
|
199
|
-
Default templates:
|
|
200
|
-
|
|
201
|
-
| Name | Expansion |
|
|
202
|
-
| --- | --- |
|
|
203
|
-
| `print` | `print({expr})` |
|
|
204
|
-
| `len` | `len({expr})` |
|
|
205
|
-
| `not` | `not {expr}` |
|
|
206
|
-
| `par` | `({expr})` |
|
|
207
|
-
| `var` | `key = {expr}`; selects `key`; Tab or Enter accepts it |
|
|
208
|
-
| `await` | `await {expr}` |
|
|
209
|
-
| `return` | `return {expr}` |
|
|
210
|
-
| `if` | `if {expr}:\n{indent} ` |
|
|
211
|
-
| `while` | `while {expr}:\n{indent} ` |
|
|
212
|
-
| `raise` | `raise {expr}` |
|
|
213
|
-
| `yield` | `yield {expr}` |
|
|
214
|
-
| `str` | `str({expr})` |
|
|
215
|
-
| `list` | `list({expr})` |
|
|
216
|
-
| `set` | `set({expr})` |
|
|
217
|
-
| `dict` | `dict({expr})` |
|
|
218
|
-
| `tuple` | `tuple({expr})` |
|
|
219
|
-
|
|
220
|
-
Use `%postfix_template list` in IPython to see the exact effective set, including
|
|
221
|
-
custom and disabled templates.
|
|
222
|
-
|
|
223
|
-
## Local Validation
|
|
224
|
-
|
|
225
|
-
Run tests:
|
|
226
|
-
|
|
227
|
-
```bash
|
|
228
|
-
uv run --extra test pytest -q
|
|
229
|
-
uv run --extra dev ruff check .
|
|
230
|
-
uv run --extra dev ruff format --check .
|
|
231
|
-
uv run --isolated --no-project --with "ipython>=9,<10" --with "traitlets>=5.13" --with "pip-audit>=2.7" pip-audit --strict --local
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Build and check release artifacts:
|
|
235
|
-
|
|
236
|
-
```bash
|
|
237
|
-
uv run --extra dev python -m build
|
|
238
|
-
uv run --extra dev python -m twine check dist/*
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Validate the wheel in a clean local virtual environment:
|
|
242
|
-
|
|
243
|
-
```bash
|
|
244
|
-
uv venv .venv-check
|
|
245
|
-
uv pip install --python .venv-check/bin/python dist/*.whl
|
|
246
|
-
.venv-check/bin/ipython
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Then inside IPython:
|
|
250
|
-
|
|
251
|
-
```python
|
|
252
|
-
%load_ext ipython_postfix_completion
|
|
253
|
-
%postfix_template add for "for item in {expr}:\n{indent} "
|
|
254
|
-
%postfix_template list
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
## Publish
|
|
258
|
-
|
|
259
|
-
Publishing uses GitHub Actions and PyPI Trusted Publishing. Configure the
|
|
260
|
-
existing PyPI project once under **Manage > Publishing > Add a new publisher**:
|
|
261
|
-
|
|
262
|
-
| Setting | Value |
|
|
263
|
-
| --- | --- |
|
|
264
|
-
| Owner | `fishandsheep` |
|
|
265
|
-
| Repository | `ipython-postfix-completion` |
|
|
266
|
-
| Workflow | `publish.yml` |
|
|
267
|
-
| Environment | `pypi` |
|
|
268
|
-
|
|
269
|
-
For each release, update `project.version` in `pyproject.toml`, commit and push
|
|
270
|
-
the change, then create a matching `v` tag. For this release:
|
|
271
|
-
|
|
272
|
-
```bash
|
|
273
|
-
git tag v0.2.0
|
|
274
|
-
git push origin v0.2.0
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
The workflow verifies the tag against `project.version`, runs tests, builds and
|
|
278
|
-
checks both distributions, then publishes them to PyPI using a short-lived OIDC
|
|
279
|
-
credential. PyPI versions are immutable: never reuse a published version or tag;
|
|
280
|
-
fixes require the next version.
|
|
281
|
-
|
|
282
|
-
See [CHANGELOG.md](CHANGELOG.md) for release notes and migration guidance.
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
ipython_postfix_completion/__init__.py,sha256=K9fMAo6kLY3JeUe9SfEp83Om-KsYTcDrC6Fyhhy3VUQ,30361
|
|
2
|
-
ipython_postfix_completion-0.2.0.dist-info/licenses/LICENSE,sha256=omLR-7QW1K3l6AG7ih1N5L8Z58UY-ROrQL82ANN9PnM,1547
|
|
3
|
-
ipython_postfix_completion-0.2.0.dist-info/METADATA,sha256=CwKUKdYbwrxPvVFLnTEJ3n-_sD_E4CRvBoiM1pHb_ds,7767
|
|
4
|
-
ipython_postfix_completion-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
-
ipython_postfix_completion-0.2.0.dist-info/top_level.txt,sha256=W2sJCvsBheoCB2nbzJeD7-p5PR6lVDNicVLxNxmeraY,27
|
|
6
|
-
ipython_postfix_completion-0.2.0.dist-info/RECORD,,
|
{ipython_postfix_completion-0.2.0.dist-info → ipython_postfix_completion-0.2.1.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|