ipython-postfix-completion 0.1.1__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 +506 -27
- 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.1.1.dist-info → ipython_postfix_completion-0.2.1.dist-info}/WHEEL +1 -1
- ipython_postfix_completion-0.1.1.dist-info/METADATA +0 -218
- ipython_postfix_completion-0.1.1.dist-info/RECORD +0 -6
- {ipython_postfix_completion-0.1.1.dist-info → ipython_postfix_completion-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {ipython_postfix_completion-0.1.1.dist-info → ipython_postfix_completion-0.2.1.dist-info}/top_level.txt +0 -0
|
@@ -7,8 +7,8 @@ import io
|
|
|
7
7
|
import re
|
|
8
8
|
import string
|
|
9
9
|
import tokenize
|
|
10
|
+
from collections.abc import Callable
|
|
10
11
|
from dataclasses import dataclass
|
|
11
|
-
from typing import Callable
|
|
12
12
|
|
|
13
13
|
from IPython.core.completer import (
|
|
14
14
|
CompletionContext,
|
|
@@ -17,15 +17,12 @@ from IPython.core.completer import (
|
|
|
17
17
|
)
|
|
18
18
|
from IPython.core.error import UsageError
|
|
19
19
|
from IPython.terminal.ptutils import IPythonPTCompleter
|
|
20
|
+
from traitlets import Bool, Unicode
|
|
20
21
|
from traitlets import Dict as TraitletsDict
|
|
21
22
|
from traitlets import List as TraitletsList
|
|
22
|
-
from traitlets import Unicode
|
|
23
23
|
from traitlets.config.configurable import Configurable
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
_POSTFIX_RE = re.compile(
|
|
27
|
-
r"^(?P<indent>[ \t]*)(?P<body>.*)\.(?P<prefix>[A-Za-z_]*)$"
|
|
28
|
-
)
|
|
25
|
+
_POSTFIX_RE = re.compile(r"^(?P<indent>[ \t]*)(?P<body>.*)\.(?P<prefix>[A-Za-z_]*)$")
|
|
29
26
|
_PREFIX_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
30
27
|
_STATE_NAME = "postfix_completion"
|
|
31
28
|
_STYLE = "bg:#44475a #f8f8f2"
|
|
@@ -35,7 +32,26 @@ _ALLOWED_TEMPLATE_FIELDS = {"expr", "indent"}
|
|
|
35
32
|
_MAGIC_NAME = "postfix_template"
|
|
36
33
|
_NO_MAGIC = object()
|
|
37
34
|
_NO_MAGIC_ATTR = object()
|
|
38
|
-
_ACTIVE_STATE:
|
|
35
|
+
_ACTIVE_STATE: PostfixState | None = None
|
|
36
|
+
_VAR_TEMPLATE = "key = {expr}"
|
|
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
|
+
}
|
|
51
|
+
_PLACEHOLDER_ATTR = "_postfix_completion_placeholder"
|
|
52
|
+
_OPENING_BRACKETS = {"(": ")", "[": "]", "{": "}"}
|
|
53
|
+
_CLOSING_BRACKETS = {closer: opener for opener, closer in _OPENING_BRACKETS.items()}
|
|
54
|
+
_STRING_START_RE = re.compile(r"(?i)^(?P<prefix>[rubf]*)(?P<quote>'''|\"\"\"|'|\")")
|
|
39
55
|
|
|
40
56
|
|
|
41
57
|
class _TemplateFormatter(string.Formatter):
|
|
@@ -53,11 +69,14 @@ DEFAULT_TEMPLATES: dict[str, str] = {
|
|
|
53
69
|
"len": "len({expr})",
|
|
54
70
|
"not": "not {expr}",
|
|
55
71
|
"par": "({expr})",
|
|
56
|
-
"var":
|
|
72
|
+
"var": _VAR_TEMPLATE,
|
|
57
73
|
"await": "await {expr}",
|
|
58
74
|
"return": "return {expr}",
|
|
59
|
-
"if":
|
|
75
|
+
"if": _IF_TEMPLATE,
|
|
60
76
|
"while": "while {expr}:\n{indent} ",
|
|
77
|
+
"for": _FOR_TEMPLATE,
|
|
78
|
+
"fori": _FORI_TEMPLATE,
|
|
79
|
+
"ef": _EF_TEMPLATE,
|
|
61
80
|
"raise": "raise {expr}",
|
|
62
81
|
"yield": "yield {expr}",
|
|
63
82
|
"str": "str({expr})",
|
|
@@ -65,6 +84,8 @@ DEFAULT_TEMPLATES: dict[str, str] = {
|
|
|
65
84
|
"set": "set({expr})",
|
|
66
85
|
"dict": "dict({expr})",
|
|
67
86
|
"tuple": "tuple({expr})",
|
|
87
|
+
"type": "type({expr})",
|
|
88
|
+
"range": "range({expr})",
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
|
|
@@ -88,22 +109,35 @@ class PostfixCompletionConfig(Configurable):
|
|
|
88
109
|
_SELECTED_STYLE,
|
|
89
110
|
help="Prompt-toolkit selected style for postfix completion menu entries.",
|
|
90
111
|
).tag(config=True)
|
|
112
|
+
smart_tab_jump = Bool(
|
|
113
|
+
True,
|
|
114
|
+
help="Move Tab over adjacent Python string and bracket closing tokens.",
|
|
115
|
+
).tag(config=True)
|
|
91
116
|
|
|
92
117
|
|
|
93
118
|
def _validate_template_name(name: str) -> None:
|
|
94
119
|
if not _TEMPLATE_NAME_RE.match(name):
|
|
95
120
|
raise UsageError(
|
|
96
|
-
f"Invalid postfix template name {name!r}: expected "
|
|
97
|
-
"[A-Za-z_][A-Za-z0-9_]*"
|
|
121
|
+
f"Invalid postfix template name {name!r}: expected [A-Za-z_][A-Za-z0-9_]*"
|
|
98
122
|
)
|
|
99
123
|
|
|
100
124
|
|
|
101
125
|
def _validate_template(template: str) -> None:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
try:
|
|
127
|
+
parsed = list(_FORMATTER.parse(template))
|
|
128
|
+
except ValueError as error:
|
|
129
|
+
raise UsageError(f"Invalid postfix template: {error}") from error
|
|
130
|
+
|
|
131
|
+
fields = set()
|
|
132
|
+
for _, field_name, format_spec, conversion in parsed:
|
|
133
|
+
if field_name is None:
|
|
134
|
+
continue
|
|
135
|
+
if format_spec or conversion is not None:
|
|
136
|
+
raise UsageError(
|
|
137
|
+
"Postfix template fields must use plain {expr} or {indent}."
|
|
138
|
+
)
|
|
139
|
+
fields.add(field_name)
|
|
140
|
+
|
|
107
141
|
unknown = fields - _ALLOWED_TEMPLATE_FIELDS
|
|
108
142
|
if unknown:
|
|
109
143
|
names = ", ".join(sorted(f"{{{name}}}" for name in unknown))
|
|
@@ -127,6 +161,30 @@ def _render_template(template: str, expr: str, indent: str = "") -> str:
|
|
|
127
161
|
return _FORMATTER.format(template, expr=expr, indent=indent)
|
|
128
162
|
|
|
129
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
|
+
|
|
130
188
|
@dataclass
|
|
131
189
|
class PostfixState:
|
|
132
190
|
matcher: Callable[[CompletionContext], dict]
|
|
@@ -228,7 +286,7 @@ def _effective_templates(state: PostfixState | None = None) -> dict[str, str]:
|
|
|
228
286
|
def _apply_template(
|
|
229
287
|
name: str, expr: str, indent: str = "", state: PostfixState | None = None
|
|
230
288
|
) -> str:
|
|
231
|
-
return
|
|
289
|
+
return _render_candidate(name, _effective_templates(state)[name], expr, indent)
|
|
232
290
|
|
|
233
291
|
|
|
234
292
|
def _postfix_candidates(
|
|
@@ -248,7 +306,7 @@ def _postfix_candidates(
|
|
|
248
306
|
if (name == prefix if exact else name.startswith(prefix))
|
|
249
307
|
]
|
|
250
308
|
return f"{expr}.{prefix}", [
|
|
251
|
-
|
|
309
|
+
_render_candidate(name, templates[name], expr, indent) for name in names
|
|
252
310
|
]
|
|
253
311
|
|
|
254
312
|
|
|
@@ -262,7 +320,7 @@ def _template_name(
|
|
|
262
320
|
for name, template in _effective_templates(state).items():
|
|
263
321
|
if not name.startswith(prefix):
|
|
264
322
|
continue
|
|
265
|
-
if
|
|
323
|
+
if _render_candidate(name, template, expr, indent) == expansion:
|
|
266
324
|
return name
|
|
267
325
|
return None
|
|
268
326
|
|
|
@@ -286,8 +344,399 @@ def _expand_buffer(buffer) -> bool:
|
|
|
286
344
|
matched_fragment, candidates = _postfix_candidates(line, exact=True)
|
|
287
345
|
if len(candidates) != 1:
|
|
288
346
|
return False
|
|
347
|
+
parsed = _line_prefix(line)
|
|
289
348
|
buffer.delete_before_cursor(len(matched_fragment))
|
|
349
|
+
expansion_start = buffer.cursor_position
|
|
290
350
|
buffer.insert_text(candidates[0])
|
|
351
|
+
placeholder_config = (
|
|
352
|
+
_TEMPLATE_PLACEHOLDERS.get(parsed[2]) if parsed is not None else None
|
|
353
|
+
)
|
|
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 (
|
|
373
|
+
parsed is not None
|
|
374
|
+
and parsed[2] == "var"
|
|
375
|
+
and _effective_templates().get("var") == _VAR_TEMPLATE
|
|
376
|
+
):
|
|
377
|
+
_select_placeholder(
|
|
378
|
+
buffer,
|
|
379
|
+
expansion_start,
|
|
380
|
+
expansion_start + len(_VAR_PLACEHOLDER),
|
|
381
|
+
buffer.cursor_position,
|
|
382
|
+
)
|
|
383
|
+
return True
|
|
384
|
+
|
|
385
|
+
|
|
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:
|
|
399
|
+
from prompt_toolkit.selection import SelectionState, SelectionType
|
|
400
|
+
|
|
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)
|
|
407
|
+
buffer.selection_state.enter_shift_mode()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _has_active_placeholder(buffer) -> bool:
|
|
411
|
+
placeholder = getattr(buffer, _PLACEHOLDER_ATTR, None)
|
|
412
|
+
selection = buffer.selection_state
|
|
413
|
+
if placeholder is None:
|
|
414
|
+
return False
|
|
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
|
|
420
|
+
and selection.original_cursor_position == start
|
|
421
|
+
and buffer.cursor_position == end
|
|
422
|
+
and (len(ranges) > 1 or buffer.text[start:end] == _VAR_PLACEHOLDER)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _accept_placeholder(buffer) -> bool:
|
|
427
|
+
if not _has_active_placeholder(buffer):
|
|
428
|
+
return False
|
|
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
|
+
)
|
|
474
|
+
return True
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _line_offsets(source: str) -> list[int]:
|
|
478
|
+
offsets = [0]
|
|
479
|
+
offsets.extend(match.end() for match in re.finditer("\n", source))
|
|
480
|
+
return offsets
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _absolute_token_position(
|
|
484
|
+
position: tuple[int, int], line_offsets: list[int], source_length: int
|
|
485
|
+
) -> int:
|
|
486
|
+
row, column = position
|
|
487
|
+
if 0 < row <= len(line_offsets):
|
|
488
|
+
return min(line_offsets[row - 1] + column, source_length)
|
|
489
|
+
return source_length
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _python_tokens(source: str) -> list[tuple[tokenize.TokenInfo, int, int]]:
|
|
493
|
+
line_offsets = _line_offsets(source)
|
|
494
|
+
tokens: list[tuple[tokenize.TokenInfo, int, int]] = []
|
|
495
|
+
generator = tokenize.generate_tokens(io.StringIO(source).readline)
|
|
496
|
+
try:
|
|
497
|
+
for token in generator:
|
|
498
|
+
start = _absolute_token_position(token.start, line_offsets, len(source))
|
|
499
|
+
end = _absolute_token_position(token.end, line_offsets, len(source))
|
|
500
|
+
tokens.append((token, start, end))
|
|
501
|
+
except (IndentationError, SyntaxError, tokenize.TokenError):
|
|
502
|
+
# Incomplete interactive input is normal. Tokens emitted before the
|
|
503
|
+
# error still describe whether the adjacent closer is syntactic.
|
|
504
|
+
pass
|
|
505
|
+
return tokens
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _string_parts(value: str) -> tuple[str, str] | None:
|
|
509
|
+
match = _STRING_START_RE.match(value)
|
|
510
|
+
if match is None:
|
|
511
|
+
return None
|
|
512
|
+
return match.group("prefix"), match.group("quote")
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _quoted_string_end(value: str, quote_start: int) -> int:
|
|
516
|
+
quote = value[quote_start : quote_start + 3]
|
|
517
|
+
delimiter = quote if quote in {"'''", '"""'} else value[quote_start]
|
|
518
|
+
index = quote_start + len(delimiter)
|
|
519
|
+
while index < len(value):
|
|
520
|
+
if value.startswith(delimiter, index):
|
|
521
|
+
backslashes = 0
|
|
522
|
+
before = index - 1
|
|
523
|
+
while before >= quote_start and value[before] == "\\":
|
|
524
|
+
backslashes += 1
|
|
525
|
+
before -= 1
|
|
526
|
+
if backslashes % 2 == 0:
|
|
527
|
+
return index + len(delimiter)
|
|
528
|
+
index += 1
|
|
529
|
+
return len(value)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _fstring_brace_is_closing(value: str, cursor: int) -> bool:
|
|
533
|
+
parts = _string_parts(value)
|
|
534
|
+
if parts is None:
|
|
535
|
+
return False
|
|
536
|
+
prefix, quote = parts
|
|
537
|
+
if "f" not in prefix.lower() or not value.endswith(quote):
|
|
538
|
+
return False
|
|
539
|
+
|
|
540
|
+
content_start = len(prefix) + len(quote)
|
|
541
|
+
content_end = len(value) - len(quote)
|
|
542
|
+
if not (content_start <= cursor < content_end) or value[cursor] != "}":
|
|
543
|
+
return False
|
|
544
|
+
|
|
545
|
+
# Each context is [kind, is_format_spec, bracket_stack]. Literal f-string
|
|
546
|
+
# text and Python replacement expressions follow different brace rules.
|
|
547
|
+
contexts: list[list[object]] = [["literal", False, []]]
|
|
548
|
+
index = content_start
|
|
549
|
+
while index <= cursor and contexts:
|
|
550
|
+
kind, is_format_spec, bracket_stack = contexts[-1]
|
|
551
|
+
character = value[index]
|
|
552
|
+
|
|
553
|
+
if kind == "literal":
|
|
554
|
+
if character == "{" and value.startswith("{{", index):
|
|
555
|
+
index += 2
|
|
556
|
+
continue
|
|
557
|
+
if character == "{":
|
|
558
|
+
contexts.append(["expression", False, []])
|
|
559
|
+
index += 1
|
|
560
|
+
continue
|
|
561
|
+
if character == "}" and is_format_spec:
|
|
562
|
+
if index == cursor:
|
|
563
|
+
return True
|
|
564
|
+
contexts.pop()
|
|
565
|
+
index += 1
|
|
566
|
+
continue
|
|
567
|
+
if character == "}" and value.startswith("}}", index):
|
|
568
|
+
index += 2
|
|
569
|
+
continue
|
|
570
|
+
if character == "}":
|
|
571
|
+
return False
|
|
572
|
+
index += 1
|
|
573
|
+
continue
|
|
574
|
+
|
|
575
|
+
assert isinstance(bracket_stack, list)
|
|
576
|
+
if character in {"'", '"'}:
|
|
577
|
+
string_end = _quoted_string_end(value, index)
|
|
578
|
+
if index < cursor < string_end:
|
|
579
|
+
# A nested f-string can contain the candidate brace.
|
|
580
|
+
prefix_start = index
|
|
581
|
+
while prefix_start > content_start and value[prefix_start - 1] in (
|
|
582
|
+
"rRuUbBfF"
|
|
583
|
+
):
|
|
584
|
+
prefix_start -= 1
|
|
585
|
+
nested = value[prefix_start:string_end]
|
|
586
|
+
if "f" in value[prefix_start:index].lower():
|
|
587
|
+
return _fstring_brace_is_closing(nested, cursor - prefix_start)
|
|
588
|
+
return False
|
|
589
|
+
index = string_end
|
|
590
|
+
continue
|
|
591
|
+
if character == "#":
|
|
592
|
+
newline = value.find("\n", index)
|
|
593
|
+
if newline == -1 or cursor < newline:
|
|
594
|
+
return False
|
|
595
|
+
index = newline + 1
|
|
596
|
+
continue
|
|
597
|
+
if character in _OPENING_BRACKETS:
|
|
598
|
+
bracket_stack.append(character)
|
|
599
|
+
index += 1
|
|
600
|
+
continue
|
|
601
|
+
if character in _CLOSING_BRACKETS:
|
|
602
|
+
if bracket_stack:
|
|
603
|
+
if bracket_stack[-1] != _CLOSING_BRACKETS[character]:
|
|
604
|
+
return False
|
|
605
|
+
bracket_stack.pop()
|
|
606
|
+
index += 1
|
|
607
|
+
continue
|
|
608
|
+
if character == "}":
|
|
609
|
+
if index == cursor:
|
|
610
|
+
return True
|
|
611
|
+
contexts.pop()
|
|
612
|
+
index += 1
|
|
613
|
+
continue
|
|
614
|
+
return False
|
|
615
|
+
if character == ":" and not bracket_stack:
|
|
616
|
+
contexts[-1] = ["literal", True, []]
|
|
617
|
+
index += 1
|
|
618
|
+
continue
|
|
619
|
+
index += 1
|
|
620
|
+
return False
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _string_closer_width(
|
|
624
|
+
source: str,
|
|
625
|
+
cursor: int,
|
|
626
|
+
tokens: list[tuple[tokenize.TokenInfo, int, int]],
|
|
627
|
+
) -> int:
|
|
628
|
+
fstring_end_types = {
|
|
629
|
+
token_type
|
|
630
|
+
for token_type in (
|
|
631
|
+
getattr(tokenize, "FSTRING_END", None),
|
|
632
|
+
getattr(tokenize, "TSTRING_END", None),
|
|
633
|
+
)
|
|
634
|
+
if token_type is not None
|
|
635
|
+
}
|
|
636
|
+
for token, start, end in tokens:
|
|
637
|
+
if (
|
|
638
|
+
token.type in fstring_end_types
|
|
639
|
+
and start == cursor
|
|
640
|
+
and token.string in {"'", '"', "'''", '"""'}
|
|
641
|
+
):
|
|
642
|
+
return len(token.string)
|
|
643
|
+
if token.type != tokenize.STRING:
|
|
644
|
+
continue
|
|
645
|
+
parts = _string_parts(token.string)
|
|
646
|
+
if parts is None:
|
|
647
|
+
continue
|
|
648
|
+
_, quote = parts
|
|
649
|
+
if end - len(quote) == cursor and source.startswith(quote, cursor):
|
|
650
|
+
return len(quote)
|
|
651
|
+
return 0
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _bracket_closer_is_syntactic(
|
|
655
|
+
source: str,
|
|
656
|
+
cursor: int,
|
|
657
|
+
tokens: list[tuple[tokenize.TokenInfo, int, int]],
|
|
658
|
+
) -> bool:
|
|
659
|
+
closer = source[cursor]
|
|
660
|
+
stack: list[str] = []
|
|
661
|
+
for token, start, _ in tokens:
|
|
662
|
+
if start > cursor:
|
|
663
|
+
break
|
|
664
|
+
if token.type != tokenize.OP or token.string not in (
|
|
665
|
+
_OPENING_BRACKETS | _CLOSING_BRACKETS
|
|
666
|
+
):
|
|
667
|
+
continue
|
|
668
|
+
if start == cursor:
|
|
669
|
+
return (
|
|
670
|
+
token.string == closer
|
|
671
|
+
and bool(stack)
|
|
672
|
+
and (stack[-1] == _CLOSING_BRACKETS[closer])
|
|
673
|
+
)
|
|
674
|
+
if token.string in _OPENING_BRACKETS:
|
|
675
|
+
stack.append(token.string)
|
|
676
|
+
elif not stack or stack[-1] != _CLOSING_BRACKETS[token.string]:
|
|
677
|
+
return False
|
|
678
|
+
else:
|
|
679
|
+
stack.pop()
|
|
680
|
+
return False
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _fstring_brace_fallback(
|
|
684
|
+
source: str,
|
|
685
|
+
cursor: int,
|
|
686
|
+
tokens: list[tuple[tokenize.TokenInfo, int, int]],
|
|
687
|
+
) -> bool:
|
|
688
|
+
for token, start, end in tokens:
|
|
689
|
+
if token.type == tokenize.STRING and start < cursor < end:
|
|
690
|
+
return _fstring_brace_is_closing(token.string, cursor - start)
|
|
691
|
+
return False
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _smart_tab_jump_width(source: str, cursor: int) -> int:
|
|
695
|
+
if cursor >= len(source):
|
|
696
|
+
return 0
|
|
697
|
+
|
|
698
|
+
tokens = _python_tokens(source)
|
|
699
|
+
quote_width = _string_closer_width(source, cursor, tokens)
|
|
700
|
+
if quote_width:
|
|
701
|
+
return quote_width
|
|
702
|
+
|
|
703
|
+
if source[cursor] not in _CLOSING_BRACKETS:
|
|
704
|
+
return 0
|
|
705
|
+
line_start = source.rfind("\n", 0, cursor) + 1
|
|
706
|
+
if not source[line_start:cursor].strip():
|
|
707
|
+
return 0
|
|
708
|
+
if _bracket_closer_is_syntactic(source, cursor, tokens):
|
|
709
|
+
return 1
|
|
710
|
+
if source[cursor] == "}" and _fstring_brace_fallback(source, cursor, tokens):
|
|
711
|
+
return 1
|
|
712
|
+
return 0
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _smart_tab_jump_enabled(state: PostfixState | None = None) -> bool:
|
|
716
|
+
state = _state_or_default(state)
|
|
717
|
+
return state is None or state.config.smart_tab_jump
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _can_smart_tab_jump(buffer, state: PostfixState | None = None) -> bool:
|
|
721
|
+
if not _smart_tab_jump_enabled(state) or _has_active_placeholder(buffer):
|
|
722
|
+
return False
|
|
723
|
+
if _postfix_candidates(
|
|
724
|
+
buffer.document.current_line_before_cursor, exact=True, state=state
|
|
725
|
+
)[1]:
|
|
726
|
+
return False
|
|
727
|
+
return bool(_smart_tab_jump_width(buffer.text, buffer.cursor_position))
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _jump_over_closer(buffer) -> bool:
|
|
731
|
+
width = _smart_tab_jump_width(buffer.text, buffer.cursor_position)
|
|
732
|
+
if not width:
|
|
733
|
+
return False
|
|
734
|
+
if buffer.complete_state is not None:
|
|
735
|
+
buffer.cancel_completion()
|
|
736
|
+
width = _smart_tab_jump_width(buffer.text, buffer.cursor_position)
|
|
737
|
+
if not width:
|
|
738
|
+
return False
|
|
739
|
+
buffer.cursor_position += width
|
|
291
740
|
return True
|
|
292
741
|
|
|
293
742
|
|
|
@@ -299,7 +748,7 @@ def _insert_trigger_and_maybe_complete(buffer) -> bool:
|
|
|
299
748
|
return True
|
|
300
749
|
|
|
301
750
|
|
|
302
|
-
def _make_key_bindings():
|
|
751
|
+
def _make_key_bindings(state: PostfixState | None = None):
|
|
303
752
|
from prompt_toolkit.application.current import get_app
|
|
304
753
|
from prompt_toolkit.filters import Condition
|
|
305
754
|
from prompt_toolkit.key_binding import KeyBindings
|
|
@@ -311,14 +760,40 @@ def _make_key_bindings():
|
|
|
311
760
|
buffer = get_app().current_buffer
|
|
312
761
|
return bool(
|
|
313
762
|
_postfix_candidates(
|
|
314
|
-
buffer.document.current_line_before_cursor,
|
|
763
|
+
buffer.document.current_line_before_cursor,
|
|
764
|
+
exact=True,
|
|
765
|
+
state=state,
|
|
315
766
|
)[1]
|
|
316
767
|
)
|
|
317
768
|
|
|
769
|
+
@Condition
|
|
770
|
+
def has_active_placeholder() -> bool:
|
|
771
|
+
return _has_active_placeholder(get_app().current_buffer)
|
|
772
|
+
|
|
773
|
+
@Condition
|
|
774
|
+
def can_smart_tab_jump() -> bool:
|
|
775
|
+
return _can_smart_tab_jump(get_app().current_buffer, state)
|
|
776
|
+
|
|
777
|
+
@key_bindings.add("tab", filter=has_active_placeholder)
|
|
778
|
+
def accept_placeholder(event) -> None:
|
|
779
|
+
_accept_placeholder(event.current_buffer)
|
|
780
|
+
|
|
781
|
+
@key_bindings.add("s-tab", filter=has_active_placeholder)
|
|
782
|
+
def reverse_placeholder(event) -> None:
|
|
783
|
+
_reverse_placeholder(event.current_buffer)
|
|
784
|
+
|
|
785
|
+
@key_bindings.add("enter", filter=has_active_placeholder)
|
|
786
|
+
def accept_placeholder_with_enter(event) -> None:
|
|
787
|
+
_accept_placeholder(event.current_buffer)
|
|
788
|
+
|
|
318
789
|
@key_bindings.add("tab", filter=has_exact_postfix)
|
|
319
790
|
def expand_postfix(event) -> None:
|
|
320
791
|
_expand_buffer(event.current_buffer)
|
|
321
792
|
|
|
793
|
+
@key_bindings.add("tab", filter=can_smart_tab_jump)
|
|
794
|
+
def smart_tab_jump(event) -> None:
|
|
795
|
+
_jump_over_closer(event.current_buffer)
|
|
796
|
+
|
|
322
797
|
@key_bindings.add(".")
|
|
323
798
|
def insert_trigger(event) -> None:
|
|
324
799
|
_insert_trigger_and_maybe_complete(event.current_buffer)
|
|
@@ -400,10 +875,12 @@ def _postfix_template_magic(line: str) -> None:
|
|
|
400
875
|
|
|
401
876
|
def _list_postfix_templates(state: PostfixState) -> None:
|
|
402
877
|
effective = state.effective_templates()
|
|
403
|
-
names =
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
state.
|
|
878
|
+
names = (
|
|
879
|
+
set(DEFAULT_TEMPLATES)
|
|
880
|
+
| set(state.config.templates)
|
|
881
|
+
| set(state.runtime_templates or {})
|
|
882
|
+
| set(state.config.disabled_templates)
|
|
883
|
+
| set(state.runtime_disabled_templates or set())
|
|
407
884
|
)
|
|
408
885
|
for name in sorted(names):
|
|
409
886
|
origin = state.template_origin(name)
|
|
@@ -435,7 +912,7 @@ class PostfixPTCompleter(IPythonPTCompleter):
|
|
|
435
912
|
|
|
436
913
|
fragment = body[completion.start : completion.end]
|
|
437
914
|
line_start = body.rfind("\n", 0, completion.start) + 1
|
|
438
|
-
indent = body[line_start:completion.start]
|
|
915
|
+
indent = body[line_start : completion.start]
|
|
439
916
|
state = _state_or_default()
|
|
440
917
|
key = _template_name(fragment, completion.text, indent, state) or "postfix"
|
|
441
918
|
display = f".{key} -> {completion.text}"
|
|
@@ -484,7 +961,9 @@ def _install_key_binding(ip, state: PostfixState) -> None:
|
|
|
484
961
|
from prompt_toolkit.key_binding import merge_key_bindings
|
|
485
962
|
|
|
486
963
|
state.original_key_bindings = original
|
|
487
|
-
|
|
964
|
+
# prompt-toolkit resolves the last active binding. Keep extension bindings
|
|
965
|
+
# last so their filtered Tab cases win while native bindings remain fallback.
|
|
966
|
+
pt_app.key_bindings = merge_key_bindings([original, _make_key_bindings(state)])
|
|
488
967
|
|
|
489
968
|
|
|
490
969
|
def load_ipython_extension(ip) -> None:
|
|
@@ -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,218 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ipython-postfix-completion
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: Configurable postfix completion extension for IPython.
|
|
5
|
-
Author: IPython Postfix Completion Contributors
|
|
6
|
-
License-Expression: BSD-3-Clause
|
|
7
|
-
Keywords: ipython,completion,postfix,extension
|
|
8
|
-
Classifier: Framework :: IPython
|
|
9
|
-
Classifier: Intended Audience :: Developers
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
-
Requires-Python: >=3.11
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
License-File: LICENSE
|
|
19
|
-
Requires-Dist: ipython>=9.0
|
|
20
|
-
Requires-Dist: traitlets>=5.13
|
|
21
|
-
Provides-Extra: test
|
|
22
|
-
Requires-Dist: pytest>=7; extra == "test"
|
|
23
|
-
Provides-Extra: dev
|
|
24
|
-
Requires-Dist: build; extra == "dev"
|
|
25
|
-
Requires-Dist: twine; extra == "dev"
|
|
26
|
-
Requires-Dist: ipython-postfix-completion[test]; extra == "dev"
|
|
27
|
-
Dynamic: license-file
|
|
28
|
-
|
|
29
|
-
# IPython Postfix Completion
|
|
30
|
-
|
|
31
|
-
Configurable postfix completion extension for IPython.
|
|
32
|
-
|
|
33
|
-
Package on PyPI: `ipython-postfix-completion`
|
|
34
|
-
|
|
35
|
-
## Install
|
|
36
|
-
|
|
37
|
-
Install into the current Python environment with `uv`:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
uvx --with ipython-postfix-completion ipython
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Install into the current ipython environment with `uv`:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
uv tool install ipython --with ipython-postfix-completion
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Load
|
|
50
|
-
|
|
51
|
-
Inside IPython:
|
|
52
|
-
|
|
53
|
-
```python
|
|
54
|
-
%load_ext ipython_postfix_completion
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
To load it automatically, add this to `ipython_config.py`:
|
|
58
|
-
|
|
59
|
-
```python
|
|
60
|
-
c.InteractiveShellApp.extensions = ["ipython_postfix_completion"]
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## Quick Example: Add a `for` Template
|
|
64
|
-
|
|
65
|
-
Add a template for the current IPython session:
|
|
66
|
-
|
|
67
|
-
```python
|
|
68
|
-
%postfix_template add for "for item in {expr}:\n{indent} "
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Use it:
|
|
72
|
-
|
|
73
|
-
```python
|
|
74
|
-
items.for<Tab>
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
It expands to:
|
|
78
|
-
|
|
79
|
-
```python
|
|
80
|
-
for item in items:
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Runtime templates only affect the current IPython session. Put templates in
|
|
85
|
-
`ipython_config.py` if you want them to persist.
|
|
86
|
-
|
|
87
|
-
## Runtime Magic
|
|
88
|
-
|
|
89
|
-
List effective templates:
|
|
90
|
-
|
|
91
|
-
```python
|
|
92
|
-
%postfix_template list
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Add or override a template for the current session:
|
|
96
|
-
|
|
97
|
-
```python
|
|
98
|
-
%postfix_template add debug "print({expr}=)"
|
|
99
|
-
%postfix_template add forin "for item in {expr}:\n{indent} "
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Disable a template for the current session:
|
|
103
|
-
|
|
104
|
-
```python
|
|
105
|
-
%postfix_template remove tuple
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Reset one runtime change:
|
|
109
|
-
|
|
110
|
-
```python
|
|
111
|
-
%postfix_template reset forin
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Reset all runtime changes:
|
|
115
|
-
|
|
116
|
-
```python
|
|
117
|
-
%postfix_template reset --all
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## Persistent Config
|
|
121
|
-
|
|
122
|
-
Add persistent templates in `ipython_config.py`:
|
|
123
|
-
|
|
124
|
-
```python
|
|
125
|
-
c.PostfixCompletionConfig.templates = {
|
|
126
|
-
"debug": "print({expr}=)",
|
|
127
|
-
"forin": "for item in {expr}:\n{indent} ",
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
c.PostfixCompletionConfig.disabled_templates = ["tuple"]
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Template names must match `[A-Za-z_][A-Za-z0-9_]*`.
|
|
134
|
-
|
|
135
|
-
Templates must include `{expr}` and may also use `{indent}`. No other template
|
|
136
|
-
fields are allowed.
|
|
137
|
-
|
|
138
|
-
## Built-in Templates
|
|
139
|
-
|
|
140
|
-
Default templates:
|
|
141
|
-
|
|
142
|
-
| Name | Expansion |
|
|
143
|
-
| --- | --- |
|
|
144
|
-
| `print` | `print({expr})` |
|
|
145
|
-
| `len` | `len({expr})` |
|
|
146
|
-
| `not` | `not {expr}` |
|
|
147
|
-
| `par` | `({expr})` |
|
|
148
|
-
| `var` | `{expr} = ` |
|
|
149
|
-
| `await` | `await {expr}` |
|
|
150
|
-
| `return` | `return {expr}` |
|
|
151
|
-
| `if` | `if {expr}:\n{indent} ` |
|
|
152
|
-
| `while` | `while {expr}:\n{indent} ` |
|
|
153
|
-
| `raise` | `raise {expr}` |
|
|
154
|
-
| `yield` | `yield {expr}` |
|
|
155
|
-
| `str` | `str({expr})` |
|
|
156
|
-
| `list` | `list({expr})` |
|
|
157
|
-
| `set` | `set({expr})` |
|
|
158
|
-
| `dict` | `dict({expr})` |
|
|
159
|
-
| `tuple` | `tuple({expr})` |
|
|
160
|
-
|
|
161
|
-
Use `%postfix_template list` in IPython to see the exact effective set, including
|
|
162
|
-
custom and disabled templates.
|
|
163
|
-
|
|
164
|
-
## Local Validation
|
|
165
|
-
|
|
166
|
-
Run tests:
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
uv run --extra test pytest -q
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Build and check release artifacts:
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
uv run --extra dev python -m build
|
|
176
|
-
uv run --extra dev python -m twine check dist/*
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Validate the wheel in a clean local virtual environment:
|
|
180
|
-
|
|
181
|
-
```bash
|
|
182
|
-
uv venv .venv-check
|
|
183
|
-
uv pip install --python .venv-check/bin/python dist/*.whl
|
|
184
|
-
.venv-check/bin/ipython
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
Then inside IPython:
|
|
188
|
-
|
|
189
|
-
```python
|
|
190
|
-
%load_ext ipython_postfix_completion
|
|
191
|
-
%postfix_template add for "for item in {expr}:\n{indent} "
|
|
192
|
-
%postfix_template list
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## Publish
|
|
196
|
-
|
|
197
|
-
Publishing uses GitHub Actions and PyPI Trusted Publishing. Configure the
|
|
198
|
-
existing PyPI project once under **Manage > Publishing > Add a new publisher**:
|
|
199
|
-
|
|
200
|
-
| Setting | Value |
|
|
201
|
-
| --- | --- |
|
|
202
|
-
| Owner | `fishandsheep` |
|
|
203
|
-
| Repository | `ipython-postfix-completion` |
|
|
204
|
-
| Workflow | `publish.yml` |
|
|
205
|
-
| Environment | `pypi` |
|
|
206
|
-
|
|
207
|
-
For each release, update `project.version` in `pyproject.toml`, commit and push
|
|
208
|
-
the change, then create a matching `v` tag. For example, after changing the
|
|
209
|
-
version to `0.1.1`:
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
git tag v0.1.1
|
|
213
|
-
git push origin v0.1.1
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
The workflow verifies the tag against `project.version`, runs tests, builds and
|
|
217
|
-
checks both distributions, then publishes them to PyPI using a short-lived OIDC
|
|
218
|
-
credential. The already-published `0.1.0` release cannot be uploaded again.
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
ipython_postfix_completion/__init__.py,sha256=Ihs9zrkmwcmDydHIxRn4KU7yeRR_NWxHpB9WaidvyXs,18145
|
|
2
|
-
ipython_postfix_completion-0.1.1.dist-info/licenses/LICENSE,sha256=omLR-7QW1K3l6AG7ih1N5L8Z58UY-ROrQL82ANN9PnM,1547
|
|
3
|
-
ipython_postfix_completion-0.1.1.dist-info/METADATA,sha256=10PHgmlDTJQVOwdahbiqMKFEdkxQvdCEvWyO1jo3uQk,4960
|
|
4
|
-
ipython_postfix_completion-0.1.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
-
ipython_postfix_completion-0.1.1.dist-info/top_level.txt,sha256=W2sJCvsBheoCB2nbzJeD7-p5PR6lVDNicVLxNxmeraY,27
|
|
6
|
-
ipython_postfix_completion-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|