ipython-postfix-completion 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ipython_postfix_completion/__init__.py +378 -22
- ipython_postfix_completion-0.2.0.dist-info/METADATA +282 -0
- ipython_postfix_completion-0.2.0.dist-info/RECORD +6 -0
- {ipython_postfix_completion-0.1.0.dist-info → ipython_postfix_completion-0.2.0.dist-info}/WHEEL +1 -1
- ipython_postfix_completion-0.1.0.dist-info/METADATA +0 -150
- ipython_postfix_completion-0.1.0.dist-info/RECORD +0 -6
- {ipython_postfix_completion-0.1.0.dist-info → ipython_postfix_completion-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {ipython_postfix_completion-0.1.0.dist-info → ipython_postfix_completion-0.2.0.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,13 @@ _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
|
+
_PLACEHOLDER_ATTR = "_postfix_completion_placeholder"
|
|
39
|
+
_OPENING_BRACKETS = {"(": ")", "[": "]", "{": "}"}
|
|
40
|
+
_CLOSING_BRACKETS = {closer: opener for opener, closer in _OPENING_BRACKETS.items()}
|
|
41
|
+
_STRING_START_RE = re.compile(r"(?i)^(?P<prefix>[rubf]*)(?P<quote>'''|\"\"\"|'|\")")
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
class _TemplateFormatter(string.Formatter):
|
|
@@ -53,6 +56,8 @@ DEFAULT_TEMPLATES: dict[str, str] = {
|
|
|
53
56
|
"len": "len({expr})",
|
|
54
57
|
"not": "not {expr}",
|
|
55
58
|
"par": "({expr})",
|
|
59
|
+
"var": _VAR_TEMPLATE,
|
|
60
|
+
"await": "await {expr}",
|
|
56
61
|
"return": "return {expr}",
|
|
57
62
|
"if": "if {expr}:\n{indent} ",
|
|
58
63
|
"while": "while {expr}:\n{indent} ",
|
|
@@ -86,22 +91,35 @@ class PostfixCompletionConfig(Configurable):
|
|
|
86
91
|
_SELECTED_STYLE,
|
|
87
92
|
help="Prompt-toolkit selected style for postfix completion menu entries.",
|
|
88
93
|
).tag(config=True)
|
|
94
|
+
smart_tab_jump = Bool(
|
|
95
|
+
True,
|
|
96
|
+
help="Move Tab over adjacent Python string and bracket closing tokens.",
|
|
97
|
+
).tag(config=True)
|
|
89
98
|
|
|
90
99
|
|
|
91
100
|
def _validate_template_name(name: str) -> None:
|
|
92
101
|
if not _TEMPLATE_NAME_RE.match(name):
|
|
93
102
|
raise UsageError(
|
|
94
|
-
f"Invalid postfix template name {name!r}: expected "
|
|
95
|
-
"[A-Za-z_][A-Za-z0-9_]*"
|
|
103
|
+
f"Invalid postfix template name {name!r}: expected [A-Za-z_][A-Za-z0-9_]*"
|
|
96
104
|
)
|
|
97
105
|
|
|
98
106
|
|
|
99
107
|
def _validate_template(template: str) -> None:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
try:
|
|
109
|
+
parsed = list(_FORMATTER.parse(template))
|
|
110
|
+
except ValueError as error:
|
|
111
|
+
raise UsageError(f"Invalid postfix template: {error}") from error
|
|
112
|
+
|
|
113
|
+
fields = set()
|
|
114
|
+
for _, field_name, format_spec, conversion in parsed:
|
|
115
|
+
if field_name is None:
|
|
116
|
+
continue
|
|
117
|
+
if format_spec or conversion is not None:
|
|
118
|
+
raise UsageError(
|
|
119
|
+
"Postfix template fields must use plain {expr} or {indent}."
|
|
120
|
+
)
|
|
121
|
+
fields.add(field_name)
|
|
122
|
+
|
|
105
123
|
unknown = fields - _ALLOWED_TEMPLATE_FIELDS
|
|
106
124
|
if unknown:
|
|
107
125
|
names = ", ".join(sorted(f"{{{name}}}" for name in unknown))
|
|
@@ -284,8 +302,320 @@ def _expand_buffer(buffer) -> bool:
|
|
|
284
302
|
matched_fragment, candidates = _postfix_candidates(line, exact=True)
|
|
285
303
|
if len(candidates) != 1:
|
|
286
304
|
return False
|
|
305
|
+
parsed = _line_prefix(line)
|
|
287
306
|
buffer.delete_before_cursor(len(matched_fragment))
|
|
307
|
+
expansion_start = buffer.cursor_position
|
|
288
308
|
buffer.insert_text(candidates[0])
|
|
309
|
+
if (
|
|
310
|
+
parsed is not None
|
|
311
|
+
and parsed[2] == "var"
|
|
312
|
+
and _effective_templates().get("var") == _VAR_TEMPLATE
|
|
313
|
+
):
|
|
314
|
+
_select_placeholder(
|
|
315
|
+
buffer,
|
|
316
|
+
expansion_start,
|
|
317
|
+
expansion_start + len(_VAR_PLACEHOLDER),
|
|
318
|
+
buffer.cursor_position,
|
|
319
|
+
)
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _select_placeholder(buffer, start: int, end: int, final_cursor: int) -> None:
|
|
324
|
+
from prompt_toolkit.selection import SelectionState, SelectionType
|
|
325
|
+
|
|
326
|
+
buffer.cursor_position = end
|
|
327
|
+
buffer.selection_state = SelectionState(start, SelectionType.CHARACTERS)
|
|
328
|
+
buffer.selection_state.enter_shift_mode()
|
|
329
|
+
setattr(buffer, _PLACEHOLDER_ATTR, (start, end, final_cursor))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _has_active_placeholder(buffer) -> bool:
|
|
333
|
+
placeholder = getattr(buffer, _PLACEHOLDER_ATTR, None)
|
|
334
|
+
selection = buffer.selection_state
|
|
335
|
+
if placeholder is None or selection is None:
|
|
336
|
+
return False
|
|
337
|
+
start, end, _ = placeholder
|
|
338
|
+
return (
|
|
339
|
+
selection.shift_mode
|
|
340
|
+
and selection.original_cursor_position == start
|
|
341
|
+
and buffer.cursor_position == end
|
|
342
|
+
and buffer.text[start:end] == _VAR_PLACEHOLDER
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _accept_placeholder(buffer) -> bool:
|
|
347
|
+
if not _has_active_placeholder(buffer):
|
|
348
|
+
return False
|
|
349
|
+
_, _, final_cursor = getattr(buffer, _PLACEHOLDER_ATTR)
|
|
350
|
+
buffer.exit_selection()
|
|
351
|
+
buffer.cursor_position = final_cursor
|
|
352
|
+
delattr(buffer, _PLACEHOLDER_ATTR)
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _line_offsets(source: str) -> list[int]:
|
|
357
|
+
offsets = [0]
|
|
358
|
+
offsets.extend(match.end() for match in re.finditer("\n", source))
|
|
359
|
+
return offsets
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _absolute_token_position(
|
|
363
|
+
position: tuple[int, int], line_offsets: list[int], source_length: int
|
|
364
|
+
) -> int:
|
|
365
|
+
row, column = position
|
|
366
|
+
if 0 < row <= len(line_offsets):
|
|
367
|
+
return min(line_offsets[row - 1] + column, source_length)
|
|
368
|
+
return source_length
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _python_tokens(source: str) -> list[tuple[tokenize.TokenInfo, int, int]]:
|
|
372
|
+
line_offsets = _line_offsets(source)
|
|
373
|
+
tokens: list[tuple[tokenize.TokenInfo, int, int]] = []
|
|
374
|
+
generator = tokenize.generate_tokens(io.StringIO(source).readline)
|
|
375
|
+
try:
|
|
376
|
+
for token in generator:
|
|
377
|
+
start = _absolute_token_position(token.start, line_offsets, len(source))
|
|
378
|
+
end = _absolute_token_position(token.end, line_offsets, len(source))
|
|
379
|
+
tokens.append((token, start, end))
|
|
380
|
+
except (IndentationError, SyntaxError, tokenize.TokenError):
|
|
381
|
+
# Incomplete interactive input is normal. Tokens emitted before the
|
|
382
|
+
# error still describe whether the adjacent closer is syntactic.
|
|
383
|
+
pass
|
|
384
|
+
return tokens
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _string_parts(value: str) -> tuple[str, str] | None:
|
|
388
|
+
match = _STRING_START_RE.match(value)
|
|
389
|
+
if match is None:
|
|
390
|
+
return None
|
|
391
|
+
return match.group("prefix"), match.group("quote")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _quoted_string_end(value: str, quote_start: int) -> int:
|
|
395
|
+
quote = value[quote_start : quote_start + 3]
|
|
396
|
+
delimiter = quote if quote in {"'''", '"""'} else value[quote_start]
|
|
397
|
+
index = quote_start + len(delimiter)
|
|
398
|
+
while index < len(value):
|
|
399
|
+
if value.startswith(delimiter, index):
|
|
400
|
+
backslashes = 0
|
|
401
|
+
before = index - 1
|
|
402
|
+
while before >= quote_start and value[before] == "\\":
|
|
403
|
+
backslashes += 1
|
|
404
|
+
before -= 1
|
|
405
|
+
if backslashes % 2 == 0:
|
|
406
|
+
return index + len(delimiter)
|
|
407
|
+
index += 1
|
|
408
|
+
return len(value)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _fstring_brace_is_closing(value: str, cursor: int) -> bool:
|
|
412
|
+
parts = _string_parts(value)
|
|
413
|
+
if parts is None:
|
|
414
|
+
return False
|
|
415
|
+
prefix, quote = parts
|
|
416
|
+
if "f" not in prefix.lower() or not value.endswith(quote):
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
content_start = len(prefix) + len(quote)
|
|
420
|
+
content_end = len(value) - len(quote)
|
|
421
|
+
if not (content_start <= cursor < content_end) or value[cursor] != "}":
|
|
422
|
+
return False
|
|
423
|
+
|
|
424
|
+
# Each context is [kind, is_format_spec, bracket_stack]. Literal f-string
|
|
425
|
+
# text and Python replacement expressions follow different brace rules.
|
|
426
|
+
contexts: list[list[object]] = [["literal", False, []]]
|
|
427
|
+
index = content_start
|
|
428
|
+
while index <= cursor and contexts:
|
|
429
|
+
kind, is_format_spec, bracket_stack = contexts[-1]
|
|
430
|
+
character = value[index]
|
|
431
|
+
|
|
432
|
+
if kind == "literal":
|
|
433
|
+
if character == "{" and value.startswith("{{", index):
|
|
434
|
+
index += 2
|
|
435
|
+
continue
|
|
436
|
+
if character == "{":
|
|
437
|
+
contexts.append(["expression", False, []])
|
|
438
|
+
index += 1
|
|
439
|
+
continue
|
|
440
|
+
if character == "}" and is_format_spec:
|
|
441
|
+
if index == cursor:
|
|
442
|
+
return True
|
|
443
|
+
contexts.pop()
|
|
444
|
+
index += 1
|
|
445
|
+
continue
|
|
446
|
+
if character == "}" and value.startswith("}}", index):
|
|
447
|
+
index += 2
|
|
448
|
+
continue
|
|
449
|
+
if character == "}":
|
|
450
|
+
return False
|
|
451
|
+
index += 1
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
assert isinstance(bracket_stack, list)
|
|
455
|
+
if character in {"'", '"'}:
|
|
456
|
+
string_end = _quoted_string_end(value, index)
|
|
457
|
+
if index < cursor < string_end:
|
|
458
|
+
# A nested f-string can contain the candidate brace.
|
|
459
|
+
prefix_start = index
|
|
460
|
+
while prefix_start > content_start and value[prefix_start - 1] in (
|
|
461
|
+
"rRuUbBfF"
|
|
462
|
+
):
|
|
463
|
+
prefix_start -= 1
|
|
464
|
+
nested = value[prefix_start:string_end]
|
|
465
|
+
if "f" in value[prefix_start:index].lower():
|
|
466
|
+
return _fstring_brace_is_closing(nested, cursor - prefix_start)
|
|
467
|
+
return False
|
|
468
|
+
index = string_end
|
|
469
|
+
continue
|
|
470
|
+
if character == "#":
|
|
471
|
+
newline = value.find("\n", index)
|
|
472
|
+
if newline == -1 or cursor < newline:
|
|
473
|
+
return False
|
|
474
|
+
index = newline + 1
|
|
475
|
+
continue
|
|
476
|
+
if character in _OPENING_BRACKETS:
|
|
477
|
+
bracket_stack.append(character)
|
|
478
|
+
index += 1
|
|
479
|
+
continue
|
|
480
|
+
if character in _CLOSING_BRACKETS:
|
|
481
|
+
if bracket_stack:
|
|
482
|
+
if bracket_stack[-1] != _CLOSING_BRACKETS[character]:
|
|
483
|
+
return False
|
|
484
|
+
bracket_stack.pop()
|
|
485
|
+
index += 1
|
|
486
|
+
continue
|
|
487
|
+
if character == "}":
|
|
488
|
+
if index == cursor:
|
|
489
|
+
return True
|
|
490
|
+
contexts.pop()
|
|
491
|
+
index += 1
|
|
492
|
+
continue
|
|
493
|
+
return False
|
|
494
|
+
if character == ":" and not bracket_stack:
|
|
495
|
+
contexts[-1] = ["literal", True, []]
|
|
496
|
+
index += 1
|
|
497
|
+
continue
|
|
498
|
+
index += 1
|
|
499
|
+
return False
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _string_closer_width(
|
|
503
|
+
source: str,
|
|
504
|
+
cursor: int,
|
|
505
|
+
tokens: list[tuple[tokenize.TokenInfo, int, int]],
|
|
506
|
+
) -> int:
|
|
507
|
+
fstring_end_types = {
|
|
508
|
+
token_type
|
|
509
|
+
for token_type in (
|
|
510
|
+
getattr(tokenize, "FSTRING_END", None),
|
|
511
|
+
getattr(tokenize, "TSTRING_END", None),
|
|
512
|
+
)
|
|
513
|
+
if token_type is not None
|
|
514
|
+
}
|
|
515
|
+
for token, start, end in tokens:
|
|
516
|
+
if (
|
|
517
|
+
token.type in fstring_end_types
|
|
518
|
+
and start == cursor
|
|
519
|
+
and token.string in {"'", '"', "'''", '"""'}
|
|
520
|
+
):
|
|
521
|
+
return len(token.string)
|
|
522
|
+
if token.type != tokenize.STRING:
|
|
523
|
+
continue
|
|
524
|
+
parts = _string_parts(token.string)
|
|
525
|
+
if parts is None:
|
|
526
|
+
continue
|
|
527
|
+
_, quote = parts
|
|
528
|
+
if end - len(quote) == cursor and source.startswith(quote, cursor):
|
|
529
|
+
return len(quote)
|
|
530
|
+
return 0
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _bracket_closer_is_syntactic(
|
|
534
|
+
source: str,
|
|
535
|
+
cursor: int,
|
|
536
|
+
tokens: list[tuple[tokenize.TokenInfo, int, int]],
|
|
537
|
+
) -> bool:
|
|
538
|
+
closer = source[cursor]
|
|
539
|
+
stack: list[str] = []
|
|
540
|
+
for token, start, _ in tokens:
|
|
541
|
+
if start > cursor:
|
|
542
|
+
break
|
|
543
|
+
if token.type != tokenize.OP or token.string not in (
|
|
544
|
+
_OPENING_BRACKETS | _CLOSING_BRACKETS
|
|
545
|
+
):
|
|
546
|
+
continue
|
|
547
|
+
if start == cursor:
|
|
548
|
+
return (
|
|
549
|
+
token.string == closer
|
|
550
|
+
and bool(stack)
|
|
551
|
+
and (stack[-1] == _CLOSING_BRACKETS[closer])
|
|
552
|
+
)
|
|
553
|
+
if token.string in _OPENING_BRACKETS:
|
|
554
|
+
stack.append(token.string)
|
|
555
|
+
elif not stack or stack[-1] != _CLOSING_BRACKETS[token.string]:
|
|
556
|
+
return False
|
|
557
|
+
else:
|
|
558
|
+
stack.pop()
|
|
559
|
+
return False
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _fstring_brace_fallback(
|
|
563
|
+
source: str,
|
|
564
|
+
cursor: int,
|
|
565
|
+
tokens: list[tuple[tokenize.TokenInfo, int, int]],
|
|
566
|
+
) -> bool:
|
|
567
|
+
for token, start, end in tokens:
|
|
568
|
+
if token.type == tokenize.STRING and start < cursor < end:
|
|
569
|
+
return _fstring_brace_is_closing(token.string, cursor - start)
|
|
570
|
+
return False
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _smart_tab_jump_width(source: str, cursor: int) -> int:
|
|
574
|
+
if cursor >= len(source):
|
|
575
|
+
return 0
|
|
576
|
+
|
|
577
|
+
tokens = _python_tokens(source)
|
|
578
|
+
quote_width = _string_closer_width(source, cursor, tokens)
|
|
579
|
+
if quote_width:
|
|
580
|
+
return quote_width
|
|
581
|
+
|
|
582
|
+
if source[cursor] not in _CLOSING_BRACKETS:
|
|
583
|
+
return 0
|
|
584
|
+
line_start = source.rfind("\n", 0, cursor) + 1
|
|
585
|
+
if not source[line_start:cursor].strip():
|
|
586
|
+
return 0
|
|
587
|
+
if _bracket_closer_is_syntactic(source, cursor, tokens):
|
|
588
|
+
return 1
|
|
589
|
+
if source[cursor] == "}" and _fstring_brace_fallback(source, cursor, tokens):
|
|
590
|
+
return 1
|
|
591
|
+
return 0
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _smart_tab_jump_enabled(state: PostfixState | None = None) -> bool:
|
|
595
|
+
state = _state_or_default(state)
|
|
596
|
+
return state is None or state.config.smart_tab_jump
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _can_smart_tab_jump(buffer, state: PostfixState | None = None) -> bool:
|
|
600
|
+
if not _smart_tab_jump_enabled(state) or _has_active_placeholder(buffer):
|
|
601
|
+
return False
|
|
602
|
+
if _postfix_candidates(
|
|
603
|
+
buffer.document.current_line_before_cursor, exact=True, state=state
|
|
604
|
+
)[1]:
|
|
605
|
+
return False
|
|
606
|
+
return bool(_smart_tab_jump_width(buffer.text, buffer.cursor_position))
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _jump_over_closer(buffer) -> bool:
|
|
610
|
+
width = _smart_tab_jump_width(buffer.text, buffer.cursor_position)
|
|
611
|
+
if not width:
|
|
612
|
+
return False
|
|
613
|
+
if buffer.complete_state is not None:
|
|
614
|
+
buffer.cancel_completion()
|
|
615
|
+
width = _smart_tab_jump_width(buffer.text, buffer.cursor_position)
|
|
616
|
+
if not width:
|
|
617
|
+
return False
|
|
618
|
+
buffer.cursor_position += width
|
|
289
619
|
return True
|
|
290
620
|
|
|
291
621
|
|
|
@@ -297,7 +627,7 @@ def _insert_trigger_and_maybe_complete(buffer) -> bool:
|
|
|
297
627
|
return True
|
|
298
628
|
|
|
299
629
|
|
|
300
|
-
def _make_key_bindings():
|
|
630
|
+
def _make_key_bindings(state: PostfixState | None = None):
|
|
301
631
|
from prompt_toolkit.application.current import get_app
|
|
302
632
|
from prompt_toolkit.filters import Condition
|
|
303
633
|
from prompt_toolkit.key_binding import KeyBindings
|
|
@@ -309,14 +639,36 @@ def _make_key_bindings():
|
|
|
309
639
|
buffer = get_app().current_buffer
|
|
310
640
|
return bool(
|
|
311
641
|
_postfix_candidates(
|
|
312
|
-
buffer.document.current_line_before_cursor,
|
|
642
|
+
buffer.document.current_line_before_cursor,
|
|
643
|
+
exact=True,
|
|
644
|
+
state=state,
|
|
313
645
|
)[1]
|
|
314
646
|
)
|
|
315
647
|
|
|
648
|
+
@Condition
|
|
649
|
+
def has_active_placeholder() -> bool:
|
|
650
|
+
return _has_active_placeholder(get_app().current_buffer)
|
|
651
|
+
|
|
652
|
+
@Condition
|
|
653
|
+
def can_smart_tab_jump() -> bool:
|
|
654
|
+
return _can_smart_tab_jump(get_app().current_buffer, state)
|
|
655
|
+
|
|
656
|
+
@key_bindings.add("tab", filter=has_active_placeholder)
|
|
657
|
+
def accept_placeholder(event) -> None:
|
|
658
|
+
_accept_placeholder(event.current_buffer)
|
|
659
|
+
|
|
660
|
+
@key_bindings.add("enter", filter=has_active_placeholder)
|
|
661
|
+
def accept_placeholder_with_enter(event) -> None:
|
|
662
|
+
_accept_placeholder(event.current_buffer)
|
|
663
|
+
|
|
316
664
|
@key_bindings.add("tab", filter=has_exact_postfix)
|
|
317
665
|
def expand_postfix(event) -> None:
|
|
318
666
|
_expand_buffer(event.current_buffer)
|
|
319
667
|
|
|
668
|
+
@key_bindings.add("tab", filter=can_smart_tab_jump)
|
|
669
|
+
def smart_tab_jump(event) -> None:
|
|
670
|
+
_jump_over_closer(event.current_buffer)
|
|
671
|
+
|
|
320
672
|
@key_bindings.add(".")
|
|
321
673
|
def insert_trigger(event) -> None:
|
|
322
674
|
_insert_trigger_and_maybe_complete(event.current_buffer)
|
|
@@ -398,10 +750,12 @@ def _postfix_template_magic(line: str) -> None:
|
|
|
398
750
|
|
|
399
751
|
def _list_postfix_templates(state: PostfixState) -> None:
|
|
400
752
|
effective = state.effective_templates()
|
|
401
|
-
names =
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
state.
|
|
753
|
+
names = (
|
|
754
|
+
set(DEFAULT_TEMPLATES)
|
|
755
|
+
| set(state.config.templates)
|
|
756
|
+
| set(state.runtime_templates or {})
|
|
757
|
+
| set(state.config.disabled_templates)
|
|
758
|
+
| set(state.runtime_disabled_templates or set())
|
|
405
759
|
)
|
|
406
760
|
for name in sorted(names):
|
|
407
761
|
origin = state.template_origin(name)
|
|
@@ -433,7 +787,7 @@ class PostfixPTCompleter(IPythonPTCompleter):
|
|
|
433
787
|
|
|
434
788
|
fragment = body[completion.start : completion.end]
|
|
435
789
|
line_start = body.rfind("\n", 0, completion.start) + 1
|
|
436
|
-
indent = body[line_start:completion.start]
|
|
790
|
+
indent = body[line_start : completion.start]
|
|
437
791
|
state = _state_or_default()
|
|
438
792
|
key = _template_name(fragment, completion.text, indent, state) or "postfix"
|
|
439
793
|
display = f".{key} -> {completion.text}"
|
|
@@ -482,7 +836,9 @@ def _install_key_binding(ip, state: PostfixState) -> None:
|
|
|
482
836
|
from prompt_toolkit.key_binding import merge_key_bindings
|
|
483
837
|
|
|
484
838
|
state.original_key_bindings = original
|
|
485
|
-
|
|
839
|
+
# prompt-toolkit resolves the last active binding. Keep extension bindings
|
|
840
|
+
# last so their filtered Tab cases win while native bindings remain fallback.
|
|
841
|
+
pt_app.key_bindings = merge_key_bindings([original, _make_key_bindings(state)])
|
|
486
842
|
|
|
487
843
|
|
|
488
844
|
def load_ipython_extension(ip) -> None:
|
|
@@ -0,0 +1,282 @@
|
|
|
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.
|
|
@@ -0,0 +1,6 @@
|
|
|
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,,
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ipython-postfix-completion
|
|
3
|
-
Version: 0.1.0
|
|
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
|
-
## Install
|
|
34
|
-
|
|
35
|
-
From a local checkout:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
python -m pip install -e .
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
From PyPI after publishing:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
python -m pip install ipython-postfix-completion
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Load
|
|
48
|
-
|
|
49
|
-
Inside IPython:
|
|
50
|
-
|
|
51
|
-
```python
|
|
52
|
-
%load_ext ipython_postfix_completion
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
To load it automatically, add this to `ipython_config.py`:
|
|
56
|
-
|
|
57
|
-
```python
|
|
58
|
-
c.InteractiveShellApp.extensions = ["ipython_postfix_completion"]
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Runtime Templates
|
|
62
|
-
|
|
63
|
-
Add a template for the current session:
|
|
64
|
-
|
|
65
|
-
```python
|
|
66
|
-
%postfix_template add forin "for item in {expr}:\n{indent} "
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Use it:
|
|
70
|
-
|
|
71
|
-
```python
|
|
72
|
-
items.forin<Tab>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
This expands to:
|
|
76
|
-
|
|
77
|
-
```python
|
|
78
|
-
for item in items:
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
List effective templates:
|
|
83
|
-
|
|
84
|
-
```python
|
|
85
|
-
%postfix_template list
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Disable a template for the current session:
|
|
89
|
-
|
|
90
|
-
```python
|
|
91
|
-
%postfix_template remove tuple
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Reset one runtime change:
|
|
95
|
-
|
|
96
|
-
```python
|
|
97
|
-
%postfix_template reset forin
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Reset all runtime changes:
|
|
101
|
-
|
|
102
|
-
```python
|
|
103
|
-
%postfix_template reset --all
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Persistent Config
|
|
107
|
-
|
|
108
|
-
Add persistent templates in `ipython_config.py`:
|
|
109
|
-
|
|
110
|
-
```python
|
|
111
|
-
c.PostfixCompletionConfig.templates = {
|
|
112
|
-
"debug": "print({expr}=)",
|
|
113
|
-
"forin": "for item in {expr}:\n{indent} ",
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
c.PostfixCompletionConfig.disabled_templates = ["tuple"]
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Template names must match `[A-Za-z_][A-Za-z0-9_]*`.
|
|
120
|
-
|
|
121
|
-
Templates must include `{expr}` and may also use `{indent}`. No other template fields are allowed.
|
|
122
|
-
|
|
123
|
-
## Development
|
|
124
|
-
|
|
125
|
-
Run tests:
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
python -m pip install -e ".[test]"
|
|
129
|
-
python -m pytest
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Build release artifacts:
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
python -m pip install build twine
|
|
136
|
-
python -m build
|
|
137
|
-
python -m twine check dist/*
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Publish to TestPyPI:
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
python -m twine upload --repository testpypi dist/*
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Publish to PyPI:
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
python -m twine upload dist/*
|
|
150
|
-
```
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
ipython_postfix_completion/__init__.py,sha256=DRxZk3AHYjxgytPi07L7mumRC35xpxJIvFQmqING7Hs,18092
|
|
2
|
-
ipython_postfix_completion-0.1.0.dist-info/licenses/LICENSE,sha256=omLR-7QW1K3l6AG7ih1N5L8Z58UY-ROrQL82ANN9PnM,1547
|
|
3
|
-
ipython_postfix_completion-0.1.0.dist-info/METADATA,sha256=TufiR1ICd7vXLBVvvdj7zYPaiiSkiySe3Ub9wkxDneo,2850
|
|
4
|
-
ipython_postfix_completion-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
-
ipython_postfix_completion-0.1.0.dist-info/top_level.txt,sha256=W2sJCvsBheoCB2nbzJeD7-p5PR6lVDNicVLxNxmeraY,27
|
|
6
|
-
ipython_postfix_completion-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|