falyx 0.1.61__py3-none-any.whl → 0.1.63__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.
- falyx/action/action_factory.py +10 -1
- falyx/action/action_group.py +11 -3
- falyx/action/action_types.py +2 -0
- falyx/action/chained_action.py +21 -10
- falyx/action/confirm_action.py +16 -2
- falyx/action/process_pool_action.py +0 -2
- falyx/completer.py +20 -0
- falyx/falyx.py +0 -1
- falyx/parser/argument.py +2 -0
- falyx/parser/command_argument_parser.py +162 -43
- falyx/parser/signature.py +4 -1
- falyx/version.py +1 -1
- {falyx-0.1.61.dist-info → falyx-0.1.63.dist-info}/METADATA +1 -1
- {falyx-0.1.61.dist-info → falyx-0.1.63.dist-info}/RECORD +17 -18
- falyx/falyx_completer.py +0 -128
- {falyx-0.1.61.dist-info → falyx-0.1.63.dist-info}/LICENSE +0 -0
- {falyx-0.1.61.dist-info → falyx-0.1.63.dist-info}/WHEEL +0 -0
- {falyx-0.1.61.dist-info → falyx-0.1.63.dist-info}/entry_points.txt +0 -0
falyx/action/action_factory.py
CHANGED
@@ -112,7 +112,16 @@ class ActionFactory(BaseAction):
|
|
112
112
|
tree = parent.add(label) if parent else Tree(label)
|
113
113
|
|
114
114
|
try:
|
115
|
-
generated =
|
115
|
+
generated = None
|
116
|
+
if self.args or self.kwargs:
|
117
|
+
try:
|
118
|
+
generated = await self.factory(*self.args, **self.kwargs)
|
119
|
+
except TypeError:
|
120
|
+
...
|
121
|
+
|
122
|
+
if not generated:
|
123
|
+
generated = await self.factory(*self.preview_args, **self.preview_kwargs)
|
124
|
+
|
116
125
|
if isinstance(generated, BaseAction):
|
117
126
|
await generated.preview(parent=tree)
|
118
127
|
else:
|
falyx/action/action_group.py
CHANGED
@@ -60,6 +60,8 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
60
60
|
Sequence[BaseAction | Callable[..., Any] | Callable[..., Awaitable]] | None
|
61
61
|
) = None,
|
62
62
|
*,
|
63
|
+
args: tuple[Any, ...] = (),
|
64
|
+
kwargs: dict[str, Any] | None = None,
|
63
65
|
hooks: HookManager | None = None,
|
64
66
|
inject_last_result: bool = False,
|
65
67
|
inject_into: str = "last_result",
|
@@ -71,6 +73,8 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
71
73
|
inject_into=inject_into,
|
72
74
|
)
|
73
75
|
ActionListMixin.__init__(self)
|
76
|
+
self.args = args
|
77
|
+
self.kwargs = kwargs or {}
|
74
78
|
if actions:
|
75
79
|
self.set_actions(actions)
|
76
80
|
|
@@ -115,13 +119,17 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
115
119
|
async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
|
116
120
|
if not self.actions:
|
117
121
|
raise EmptyGroupError(f"[{self.name}] No actions to execute.")
|
122
|
+
|
123
|
+
combined_args = args + self.args
|
124
|
+
combined_kwargs = {**self.kwargs, **kwargs}
|
125
|
+
|
118
126
|
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
119
127
|
if self.shared_context:
|
120
128
|
shared_context.set_shared_result(self.shared_context.last_result())
|
121
|
-
updated_kwargs = self._maybe_inject_last_result(
|
129
|
+
updated_kwargs = self._maybe_inject_last_result(combined_kwargs)
|
122
130
|
context = ExecutionContext(
|
123
131
|
name=self.name,
|
124
|
-
args=
|
132
|
+
args=combined_args,
|
125
133
|
kwargs=updated_kwargs,
|
126
134
|
action=self,
|
127
135
|
extra={"results": [], "errors": []},
|
@@ -131,7 +139,7 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
131
139
|
async def run_one(action: BaseAction):
|
132
140
|
try:
|
133
141
|
prepared = action.prepare(shared_context, self.options_manager)
|
134
|
-
result = await prepared(*
|
142
|
+
result = await prepared(*combined_args, **updated_kwargs)
|
135
143
|
shared_context.add_result((action.name, result))
|
136
144
|
context.extra["results"].append((action.name, result))
|
137
145
|
except Exception as error:
|
falyx/action/action_types.py
CHANGED
@@ -61,7 +61,9 @@ class ConfirmType(Enum):
|
|
61
61
|
YES_CANCEL = "yes_cancel"
|
62
62
|
YES_NO_CANCEL = "yes_no_cancel"
|
63
63
|
TYPE_WORD = "type_word"
|
64
|
+
TYPE_WORD_CANCEL = "type_word_cancel"
|
64
65
|
OK_CANCEL = "ok_cancel"
|
66
|
+
ACKNOWLEDGE = "acknowledge"
|
65
67
|
|
66
68
|
@classmethod
|
67
69
|
def choices(cls) -> list[ConfirmType]:
|
falyx/action/chained_action.py
CHANGED
@@ -54,6 +54,8 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
54
54
|
| None
|
55
55
|
) = None,
|
56
56
|
*,
|
57
|
+
args: tuple[Any, ...] = (),
|
58
|
+
kwargs: dict[str, Any] | None = None,
|
57
59
|
hooks: HookManager | None = None,
|
58
60
|
inject_last_result: bool = False,
|
59
61
|
inject_into: str = "last_result",
|
@@ -67,6 +69,8 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
67
69
|
inject_into=inject_into,
|
68
70
|
)
|
69
71
|
ActionListMixin.__init__(self)
|
72
|
+
self.args = args
|
73
|
+
self.kwargs = kwargs or {}
|
70
74
|
self.auto_inject = auto_inject
|
71
75
|
self.return_list = return_list
|
72
76
|
if actions:
|
@@ -111,13 +115,16 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
111
115
|
if not self.actions:
|
112
116
|
raise EmptyChainError(f"[{self.name}] No actions to execute.")
|
113
117
|
|
118
|
+
combined_args = args + self.args
|
119
|
+
combined_kwargs = {**self.kwargs, **kwargs}
|
120
|
+
|
114
121
|
shared_context = SharedContext(name=self.name, action=self)
|
115
122
|
if self.shared_context:
|
116
123
|
shared_context.add_result(self.shared_context.last_result())
|
117
|
-
updated_kwargs = self._maybe_inject_last_result(
|
124
|
+
updated_kwargs = self._maybe_inject_last_result(combined_kwargs)
|
118
125
|
context = ExecutionContext(
|
119
126
|
name=self.name,
|
120
|
-
args=
|
127
|
+
args=combined_args,
|
121
128
|
kwargs=updated_kwargs,
|
122
129
|
action=self,
|
123
130
|
extra={"results": [], "rollback_stack": []},
|
@@ -136,7 +143,7 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
136
143
|
shared_context.current_index = index
|
137
144
|
prepared = action.prepare(shared_context, self.options_manager)
|
138
145
|
try:
|
139
|
-
result = await prepared(*
|
146
|
+
result = await prepared(*combined_args, **updated_kwargs)
|
140
147
|
except Exception as error:
|
141
148
|
if index + 1 < len(self.actions) and isinstance(
|
142
149
|
self.actions[index + 1], FallbackAction
|
@@ -155,10 +162,12 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
155
162
|
fallback._skip_in_chain = True
|
156
163
|
else:
|
157
164
|
raise
|
158
|
-
args, updated_kwargs = self._clear_args()
|
159
165
|
shared_context.add_result(result)
|
160
166
|
context.extra["results"].append(result)
|
161
|
-
context.extra["rollback_stack"].append(
|
167
|
+
context.extra["rollback_stack"].append(
|
168
|
+
(prepared, combined_args, updated_kwargs)
|
169
|
+
)
|
170
|
+
combined_args, updated_kwargs = self._clear_args()
|
162
171
|
|
163
172
|
all_results = context.extra["results"]
|
164
173
|
assert (
|
@@ -171,11 +180,11 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
171
180
|
logger.info("[%s] Chain broken: %s", self.name, error)
|
172
181
|
context.exception = error
|
173
182
|
shared_context.add_error(shared_context.current_index, error)
|
174
|
-
await self._rollback(context.extra["rollback_stack"]
|
183
|
+
await self._rollback(context.extra["rollback_stack"])
|
175
184
|
except Exception as error:
|
176
185
|
context.exception = error
|
177
186
|
shared_context.add_error(shared_context.current_index, error)
|
178
|
-
await self._rollback(context.extra["rollback_stack"]
|
187
|
+
await self._rollback(context.extra["rollback_stack"])
|
179
188
|
await self.hooks.trigger(HookType.ON_ERROR, context)
|
180
189
|
raise
|
181
190
|
finally:
|
@@ -184,7 +193,9 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
184
193
|
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
|
185
194
|
er.record(context)
|
186
195
|
|
187
|
-
async def _rollback(
|
196
|
+
async def _rollback(
|
197
|
+
self, rollback_stack: list[tuple[Action, tuple[Any, ...], dict[str, Any]]]
|
198
|
+
):
|
188
199
|
"""
|
189
200
|
Roll back all executed actions in reverse order.
|
190
201
|
|
@@ -197,12 +208,12 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|
197
208
|
rollback_stack (list): Actions to roll back.
|
198
209
|
*args, **kwargs: Passed to rollback handlers.
|
199
210
|
"""
|
200
|
-
for action in reversed(rollback_stack):
|
211
|
+
for action, args, kwargs in reversed(rollback_stack):
|
201
212
|
rollback = getattr(action, "rollback", None)
|
202
213
|
if rollback:
|
203
214
|
try:
|
204
215
|
logger.warning("[%s] Rolling back...", action.name)
|
205
|
-
await
|
216
|
+
await rollback(*args, **kwargs)
|
206
217
|
except Exception as error:
|
207
218
|
logger.error("[%s] Rollback failed: %s", action.name, error)
|
208
219
|
|
falyx/action/confirm_action.py
CHANGED
@@ -112,6 +112,14 @@ class ConfirmAction(BaseAction):
|
|
112
112
|
validator=word_validator(self.word),
|
113
113
|
)
|
114
114
|
return answer.upper().strip() != "N"
|
115
|
+
case ConfirmType.TYPE_WORD_CANCEL:
|
116
|
+
answer = await self.prompt_session.prompt_async(
|
117
|
+
f"❓ {self.message} [{self.word}] to confirm or [N/n] > ",
|
118
|
+
validator=word_validator(self.word),
|
119
|
+
)
|
120
|
+
if answer.upper().strip() == "N":
|
121
|
+
raise CancelSignal(f"Action '{self.name}' was cancelled by the user.")
|
122
|
+
return answer.upper().strip() == self.word.upper().strip()
|
115
123
|
case ConfirmType.YES_CANCEL:
|
116
124
|
answer = await confirm_async(
|
117
125
|
self.message,
|
@@ -131,6 +139,12 @@ class ConfirmAction(BaseAction):
|
|
131
139
|
if answer.upper() == "C":
|
132
140
|
raise CancelSignal(f"Action '{self.name}' was cancelled by the user.")
|
133
141
|
return answer.upper() == "O"
|
142
|
+
case ConfirmType.ACKNOWLEDGE:
|
143
|
+
answer = await self.prompt_session.prompt_async(
|
144
|
+
f"❓ {self.message} [A]cknowledge > ",
|
145
|
+
validator=word_validator("A"),
|
146
|
+
)
|
147
|
+
return answer.upper().strip() == "A"
|
134
148
|
case _:
|
135
149
|
raise ValueError(f"Unknown confirm_type: {self.confirm_type}")
|
136
150
|
|
@@ -151,7 +165,7 @@ class ConfirmAction(BaseAction):
|
|
151
165
|
and not should_prompt_user(confirm=True, options=self.options_manager)
|
152
166
|
):
|
153
167
|
logger.debug(
|
154
|
-
"Skipping confirmation for
|
168
|
+
"Skipping confirmation for '%s' due to never_prompt or options_manager settings.",
|
155
169
|
self.name,
|
156
170
|
)
|
157
171
|
if self.return_last_result:
|
@@ -189,7 +203,7 @@ class ConfirmAction(BaseAction):
|
|
189
203
|
tree.add(f"[bold]Message:[/] {self.message}")
|
190
204
|
tree.add(f"[bold]Type:[/] {self.confirm_type.value}")
|
191
205
|
tree.add(f"[bold]Prompt Required:[/] {'No' if self.never_prompt else 'Yes'}")
|
192
|
-
if self.confirm_type
|
206
|
+
if self.confirm_type in (ConfirmType.TYPE_WORD, ConfirmType.TYPE_WORD_CANCEL):
|
193
207
|
tree.add(f"[bold]Confirmation Word:[/] {self.word}")
|
194
208
|
if parent is None:
|
195
209
|
self.console.print(tree)
|
@@ -91,9 +91,7 @@ class ProcessPoolAction(BaseAction):
|
|
91
91
|
f"Cannot inject last result into {self.name}: "
|
92
92
|
f"last result is not pickleable."
|
93
93
|
)
|
94
|
-
print(kwargs)
|
95
94
|
updated_kwargs = self._maybe_inject_last_result(kwargs)
|
96
|
-
print(updated_kwargs)
|
97
95
|
context = ExecutionContext(
|
98
96
|
name=self.name,
|
99
97
|
args=args,
|
falyx/completer.py
CHANGED
@@ -29,6 +29,26 @@ class FalyxCompleter(Completer):
|
|
29
29
|
yield from self._suggest_commands(tokens[0] if tokens else "")
|
30
30
|
return
|
31
31
|
|
32
|
+
# Identify command
|
33
|
+
command_key = tokens[0].upper()
|
34
|
+
command = self.falyx._name_map.get(command_key)
|
35
|
+
if not command or not command.arg_parser:
|
36
|
+
return
|
37
|
+
|
38
|
+
# If at end of token, e.g., "--t" vs "--tag ", add a stub so suggest_next sees it
|
39
|
+
parsed_args = tokens[1:] if cursor_at_end_of_token else tokens[1:-1]
|
40
|
+
stub = "" if cursor_at_end_of_token else tokens[-1]
|
41
|
+
|
42
|
+
try:
|
43
|
+
suggestions = command.arg_parser.suggest_next(
|
44
|
+
parsed_args + ([stub] if stub else [])
|
45
|
+
)
|
46
|
+
for suggestion in suggestions:
|
47
|
+
if suggestion.startswith(stub):
|
48
|
+
yield Completion(suggestion, start_position=-len(stub))
|
49
|
+
except Exception:
|
50
|
+
return
|
51
|
+
|
32
52
|
def _suggest_commands(self, prefix: str) -> Iterable[Completion]:
|
33
53
|
prefix = prefix.upper()
|
34
54
|
keys = [self.falyx.exit_command.key]
|
falyx/falyx.py
CHANGED
@@ -507,7 +507,6 @@ class Falyx:
|
|
507
507
|
message=self.prompt,
|
508
508
|
multiline=False,
|
509
509
|
completer=self._get_completer(),
|
510
|
-
reserve_space_for_menu=1,
|
511
510
|
validator=CommandValidator(self, self._get_validator_error_message()),
|
512
511
|
bottom_toolbar=self._get_bottom_bar_render(),
|
513
512
|
key_bindings=self.key_bindings,
|
falyx/parser/argument.py
CHANGED
@@ -26,6 +26,7 @@ class Argument:
|
|
26
26
|
resolver (BaseAction | None):
|
27
27
|
An action object that resolves the argument, if applicable.
|
28
28
|
lazy_resolver (bool): True if the resolver should be called lazily, False otherwise
|
29
|
+
suggestions (list[str] | None): A list of suggestions for the argument.
|
29
30
|
"""
|
30
31
|
|
31
32
|
flags: tuple[str, ...]
|
@@ -40,6 +41,7 @@ class Argument:
|
|
40
41
|
positional: bool = False
|
41
42
|
resolver: BaseAction | None = None
|
42
43
|
lazy_resolver: bool = False
|
44
|
+
suggestions: list[str] | None = None
|
43
45
|
|
44
46
|
def get_positional_text(self) -> str:
|
45
47
|
"""Get the positional text for the argument."""
|
@@ -4,7 +4,8 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from collections import defaultdict
|
6
6
|
from copy import deepcopy
|
7
|
-
from
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from typing import Any, Iterable, Sequence
|
8
9
|
|
9
10
|
from rich.console import Console
|
10
11
|
from rich.markup import escape
|
@@ -19,6 +20,12 @@ from falyx.parser.utils import coerce_value
|
|
19
20
|
from falyx.signals import HelpSignal
|
20
21
|
|
21
22
|
|
23
|
+
@dataclass
|
24
|
+
class ArgumentState:
|
25
|
+
arg: Argument
|
26
|
+
consumed: bool = False
|
27
|
+
|
28
|
+
|
22
29
|
class CommandArgumentParser:
|
23
30
|
"""
|
24
31
|
Custom argument parser for Falyx Commands.
|
@@ -64,6 +71,8 @@ class CommandArgumentParser:
|
|
64
71
|
self._flag_map: dict[str, Argument] = {}
|
65
72
|
self._dest_set: set[str] = set()
|
66
73
|
self._add_help()
|
74
|
+
self._last_positional_states: dict[str, ArgumentState] = {}
|
75
|
+
self._last_keyword_states: dict[str, ArgumentState] = {}
|
67
76
|
|
68
77
|
def _add_help(self):
|
69
78
|
"""Add help argument to the parser."""
|
@@ -359,19 +368,19 @@ class CommandArgumentParser:
|
|
359
368
|
)
|
360
369
|
|
361
370
|
self._register_argument(argument)
|
362
|
-
self._register_argument(negated_argument)
|
371
|
+
self._register_argument(negated_argument, bypass_validation=True)
|
363
372
|
|
364
|
-
def _register_argument(
|
373
|
+
def _register_argument(
|
374
|
+
self, argument: Argument, bypass_validation: bool = False
|
375
|
+
) -> None:
|
365
376
|
|
366
377
|
for flag in argument.flags:
|
367
|
-
if
|
368
|
-
flag in self._flag_map
|
369
|
-
and not argument.action == ArgumentAction.STORE_BOOL_OPTIONAL
|
370
|
-
):
|
378
|
+
if flag in self._flag_map and not bypass_validation:
|
371
379
|
existing = self._flag_map[flag]
|
372
380
|
raise CommandArgumentError(
|
373
381
|
f"Flag '{flag}' is already used by argument '{existing.dest}'"
|
374
382
|
)
|
383
|
+
|
375
384
|
for flag in argument.flags:
|
376
385
|
self._flag_map[flag] = argument
|
377
386
|
if not argument.positional:
|
@@ -396,6 +405,7 @@ class CommandArgumentParser:
|
|
396
405
|
dest: str | None = None,
|
397
406
|
resolver: BaseAction | None = None,
|
398
407
|
lazy_resolver: bool = True,
|
408
|
+
suggestions: list[str] | None = None,
|
399
409
|
) -> None:
|
400
410
|
"""Add an argument to the parser.
|
401
411
|
For `ArgumentAction.ACTION`, `nargs` and `type` determine how many and what kind
|
@@ -415,6 +425,8 @@ class CommandArgumentParser:
|
|
415
425
|
help: A brief description of the argument.
|
416
426
|
dest: The name of the attribute to be added to the object returned by parse_args().
|
417
427
|
resolver: A BaseAction called with optional nargs specified parsed arguments.
|
428
|
+
lazy_resolver: If True, the resolver is called lazily when the argument is accessed.
|
429
|
+
suggestions: A list of suggestions for the argument.
|
418
430
|
"""
|
419
431
|
expected_type = type
|
420
432
|
self._validate_flags(flags)
|
@@ -445,6 +457,10 @@ class CommandArgumentParser:
|
|
445
457
|
f"Default value '{default}' not in allowed choices: {choices}"
|
446
458
|
)
|
447
459
|
required = self._determine_required(required, positional, nargs, action)
|
460
|
+
if not isinstance(suggestions, Sequence) and suggestions is not None:
|
461
|
+
raise CommandArgumentError(
|
462
|
+
f"suggestions must be a list or None, got {type(suggestions)}"
|
463
|
+
)
|
448
464
|
if not isinstance(lazy_resolver, bool):
|
449
465
|
raise CommandArgumentError(
|
450
466
|
f"lazy_resolver must be a boolean, got {type(lazy_resolver)}"
|
@@ -465,6 +481,7 @@ class CommandArgumentParser:
|
|
465
481
|
positional=positional,
|
466
482
|
resolver=resolver,
|
467
483
|
lazy_resolver=lazy_resolver,
|
484
|
+
suggestions=suggestions,
|
468
485
|
)
|
469
486
|
self._register_argument(argument)
|
470
487
|
|
@@ -490,6 +507,27 @@ class CommandArgumentParser:
|
|
490
507
|
)
|
491
508
|
return defs
|
492
509
|
|
510
|
+
def raise_remaining_args_error(
|
511
|
+
self, token: str, arg_states: dict[str, ArgumentState]
|
512
|
+
) -> None:
|
513
|
+
consumed_dests = [
|
514
|
+
state.arg.dest for state in arg_states.values() if state.consumed
|
515
|
+
]
|
516
|
+
remaining_flags = [
|
517
|
+
flag
|
518
|
+
for flag, arg in self._keyword.items()
|
519
|
+
if arg.dest not in consumed_dests and flag.startswith(token)
|
520
|
+
]
|
521
|
+
|
522
|
+
if remaining_flags:
|
523
|
+
raise CommandArgumentError(
|
524
|
+
f"Unrecognized option '{token}'. Did you mean one of: {', '.join(remaining_flags)}?"
|
525
|
+
)
|
526
|
+
else:
|
527
|
+
raise CommandArgumentError(
|
528
|
+
f"Unrecognized option '{token}'. Use --help to see available options."
|
529
|
+
)
|
530
|
+
|
493
531
|
def _consume_nargs(
|
494
532
|
self, args: list[str], start: int, spec: Argument
|
495
533
|
) -> tuple[list[str], int]:
|
@@ -535,6 +573,7 @@ class CommandArgumentParser:
|
|
535
573
|
result: dict[str, Any],
|
536
574
|
positional_args: list[Argument],
|
537
575
|
consumed_positional_indicies: set[int],
|
576
|
+
arg_states: dict[str, ArgumentState],
|
538
577
|
from_validate: bool = False,
|
539
578
|
) -> int:
|
540
579
|
remaining_positional_args = [
|
@@ -580,17 +619,7 @@ class CommandArgumentParser:
|
|
580
619
|
except Exception as error:
|
581
620
|
if len(args[i - new_i :]) == 1 and args[i - new_i].startswith("-"):
|
582
621
|
token = args[i - new_i]
|
583
|
-
|
584
|
-
flag for flag in self._flag_map if flag.startswith(token)
|
585
|
-
]
|
586
|
-
if valid_flags:
|
587
|
-
raise CommandArgumentError(
|
588
|
-
f"Unrecognized option '{token}'. Did you mean one of: {', '.join(valid_flags)}?"
|
589
|
-
) from error
|
590
|
-
else:
|
591
|
-
raise CommandArgumentError(
|
592
|
-
f"Unrecognized option '{token}'. Use --help to see available options."
|
593
|
-
) from error
|
622
|
+
self.raise_remaining_args_error(token, arg_states)
|
594
623
|
else:
|
595
624
|
raise CommandArgumentError(
|
596
625
|
f"Invalid value for '{spec.dest}': {error}"
|
@@ -606,6 +635,7 @@ class CommandArgumentParser:
|
|
606
635
|
raise CommandArgumentError(
|
607
636
|
f"[{spec.dest}] Action failed: {error}"
|
608
637
|
) from error
|
638
|
+
arg_states[spec.dest].consumed = True
|
609
639
|
elif not typed and spec.default:
|
610
640
|
result[spec.dest] = spec.default
|
611
641
|
elif spec.action == ArgumentAction.APPEND:
|
@@ -618,8 +648,10 @@ class CommandArgumentParser:
|
|
618
648
|
assert result.get(spec.dest) is not None, "dest should not be None"
|
619
649
|
result[spec.dest].extend(typed)
|
620
650
|
elif spec.nargs in (None, 1, "?"):
|
651
|
+
arg_states[spec.dest].consumed = True
|
621
652
|
result[spec.dest] = typed[0] if len(typed) == 1 else typed
|
622
653
|
else:
|
654
|
+
arg_states[spec.dest].consumed = True
|
623
655
|
result[spec.dest] = typed
|
624
656
|
|
625
657
|
if spec.nargs not in ("*", "+"):
|
@@ -628,15 +660,7 @@ class CommandArgumentParser:
|
|
628
660
|
if i < len(args):
|
629
661
|
if len(args[i:]) == 1 and args[i].startswith("-"):
|
630
662
|
token = args[i]
|
631
|
-
|
632
|
-
if valid_flags:
|
633
|
-
raise CommandArgumentError(
|
634
|
-
f"Unrecognized option '{token}'. Did you mean one of: {', '.join(valid_flags)}?"
|
635
|
-
)
|
636
|
-
else:
|
637
|
-
raise CommandArgumentError(
|
638
|
-
f"Unrecognized option '{token}'. Use --help to see available options."
|
639
|
-
)
|
663
|
+
self.raise_remaining_args_error(token, arg_states)
|
640
664
|
else:
|
641
665
|
plural = "s" if len(args[i:]) > 1 else ""
|
642
666
|
raise CommandArgumentError(
|
@@ -670,6 +694,7 @@ class CommandArgumentParser:
|
|
670
694
|
positional_args: list[Argument],
|
671
695
|
consumed_positional_indices: set[int],
|
672
696
|
consumed_indices: set[int],
|
697
|
+
arg_states: dict[str, ArgumentState],
|
673
698
|
from_validate: bool = False,
|
674
699
|
) -> int:
|
675
700
|
if token in self._keyword:
|
@@ -679,6 +704,7 @@ class CommandArgumentParser:
|
|
679
704
|
if action == ArgumentAction.HELP:
|
680
705
|
if not from_validate:
|
681
706
|
self.render_help()
|
707
|
+
arg_states[spec.dest].consumed = True
|
682
708
|
raise HelpSignal()
|
683
709
|
elif action == ArgumentAction.ACTION:
|
684
710
|
assert isinstance(
|
@@ -691,24 +717,29 @@ class CommandArgumentParser:
|
|
691
717
|
raise CommandArgumentError(
|
692
718
|
f"Invalid value for '{spec.dest}': {error}"
|
693
719
|
) from error
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
720
|
+
if not spec.lazy_resolver or not from_validate:
|
721
|
+
try:
|
722
|
+
result[spec.dest] = await spec.resolver(*typed_values)
|
723
|
+
except Exception as error:
|
724
|
+
raise CommandArgumentError(
|
725
|
+
f"[{spec.dest}] Action failed: {error}"
|
726
|
+
) from error
|
727
|
+
arg_states[spec.dest].consumed = True
|
700
728
|
consumed_indices.update(range(i, new_i))
|
701
729
|
i = new_i
|
702
730
|
elif action == ArgumentAction.STORE_TRUE:
|
703
731
|
result[spec.dest] = True
|
732
|
+
arg_states[spec.dest].consumed = True
|
704
733
|
consumed_indices.add(i)
|
705
734
|
i += 1
|
706
735
|
elif action == ArgumentAction.STORE_FALSE:
|
707
736
|
result[spec.dest] = False
|
737
|
+
arg_states[spec.dest].consumed = True
|
708
738
|
consumed_indices.add(i)
|
709
739
|
i += 1
|
710
740
|
elif action == ArgumentAction.STORE_BOOL_OPTIONAL:
|
711
741
|
result[spec.dest] = spec.type(True)
|
742
|
+
arg_states[spec.dest].consumed = True
|
712
743
|
consumed_indices.add(i)
|
713
744
|
i += 1
|
714
745
|
elif action == ArgumentAction.COUNT:
|
@@ -778,19 +809,11 @@ class CommandArgumentParser:
|
|
778
809
|
)
|
779
810
|
else:
|
780
811
|
result[spec.dest] = typed_values
|
812
|
+
arg_states[spec.dest].consumed = True
|
781
813
|
consumed_indices.update(range(i, new_i))
|
782
814
|
i = new_i
|
783
815
|
elif token.startswith("-"):
|
784
|
-
|
785
|
-
valid_flags = [flag for flag in self._flag_map if flag.startswith(token)]
|
786
|
-
if valid_flags:
|
787
|
-
raise CommandArgumentError(
|
788
|
-
f"Unrecognized option '{token}'. Did you mean one of: {', '.join(valid_flags)}?"
|
789
|
-
)
|
790
|
-
else:
|
791
|
-
raise CommandArgumentError(
|
792
|
-
f"Unrecognized option '{token}'. Use --help to see available options."
|
793
|
-
)
|
816
|
+
self.raise_remaining_args_error(token, arg_states)
|
794
817
|
else:
|
795
818
|
# Get the next flagged argument index if it exists
|
796
819
|
next_flagged_index = -1
|
@@ -805,6 +828,7 @@ class CommandArgumentParser:
|
|
805
828
|
result,
|
806
829
|
positional_args,
|
807
830
|
consumed_positional_indices,
|
831
|
+
arg_states=arg_states,
|
808
832
|
from_validate=from_validate,
|
809
833
|
)
|
810
834
|
i += args_consumed
|
@@ -817,6 +841,14 @@ class CommandArgumentParser:
|
|
817
841
|
if args is None:
|
818
842
|
args = []
|
819
843
|
|
844
|
+
arg_states = {arg.dest: ArgumentState(arg) for arg in self._arguments}
|
845
|
+
self._last_positional_states = {
|
846
|
+
arg.dest: arg_states[arg.dest] for arg in self._positional.values()
|
847
|
+
}
|
848
|
+
self._last_keyword_states = {
|
849
|
+
arg.dest: arg_states[arg.dest] for arg in self._keyword_list
|
850
|
+
}
|
851
|
+
|
820
852
|
result = {arg.dest: deepcopy(arg.default) for arg in self._arguments}
|
821
853
|
positional_args: list[Argument] = [
|
822
854
|
arg for arg in self._arguments if arg.positional
|
@@ -838,6 +870,7 @@ class CommandArgumentParser:
|
|
838
870
|
positional_args,
|
839
871
|
consumed_positional_indices,
|
840
872
|
consumed_indices,
|
873
|
+
arg_states=arg_states,
|
841
874
|
from_validate=from_validate,
|
842
875
|
)
|
843
876
|
|
@@ -862,6 +895,7 @@ class CommandArgumentParser:
|
|
862
895
|
)
|
863
896
|
|
864
897
|
if spec.choices and result.get(spec.dest) not in spec.choices:
|
898
|
+
arg_states[spec.dest].consumed = False
|
865
899
|
raise CommandArgumentError(
|
866
900
|
f"Invalid value for '{spec.dest}': must be one of {{{', '.join(spec.choices)}}}"
|
867
901
|
)
|
@@ -914,6 +948,91 @@ class CommandArgumentParser:
|
|
914
948
|
kwargs_dict[arg.dest] = parsed[arg.dest]
|
915
949
|
return tuple(args_list), kwargs_dict
|
916
950
|
|
951
|
+
def suggest_next(self, args: list[str]) -> list[str]:
|
952
|
+
"""
|
953
|
+
Suggest the next possible flags or values given partially typed arguments.
|
954
|
+
|
955
|
+
This does NOT raise errors. It is intended for completions, not validation.
|
956
|
+
|
957
|
+
Returns:
|
958
|
+
A list of possible completions based on the current input.
|
959
|
+
"""
|
960
|
+
|
961
|
+
# Case 1: Next positional argument
|
962
|
+
next_non_consumed_positional: Argument | None = None
|
963
|
+
for state in self._last_positional_states.values():
|
964
|
+
if not state.consumed:
|
965
|
+
next_non_consumed_positional = state.arg
|
966
|
+
break
|
967
|
+
if next_non_consumed_positional:
|
968
|
+
if next_non_consumed_positional.choices:
|
969
|
+
return sorted(
|
970
|
+
(str(choice) for choice in next_non_consumed_positional.choices)
|
971
|
+
)
|
972
|
+
if next_non_consumed_positional.suggestions:
|
973
|
+
return sorted(next_non_consumed_positional.suggestions)
|
974
|
+
|
975
|
+
consumed_dests = [
|
976
|
+
state.arg.dest
|
977
|
+
for state in self._last_keyword_states.values()
|
978
|
+
if state.consumed
|
979
|
+
]
|
980
|
+
|
981
|
+
remaining_flags = [
|
982
|
+
flag for flag, arg in self._keyword.items() if arg.dest not in consumed_dests
|
983
|
+
]
|
984
|
+
|
985
|
+
last = args[-1]
|
986
|
+
next_to_last = args[-2] if len(args) > 1 else ""
|
987
|
+
suggestions: list[str] = []
|
988
|
+
|
989
|
+
# Case 2: Mid-flag (e.g., "--ver")
|
990
|
+
if last.startswith("-") and last not in self._keyword:
|
991
|
+
if (
|
992
|
+
len(args) > 1
|
993
|
+
and next_to_last in self._keyword
|
994
|
+
and next_to_last in remaining_flags
|
995
|
+
):
|
996
|
+
# If the last token is a mid-flag, suggest based on the previous flag
|
997
|
+
arg = self._keyword[next_to_last]
|
998
|
+
if arg.choices:
|
999
|
+
suggestions.extend(arg.choices)
|
1000
|
+
elif arg.suggestions:
|
1001
|
+
suggestions.extend(arg.suggestions)
|
1002
|
+
else:
|
1003
|
+
possible_flags = [
|
1004
|
+
flag
|
1005
|
+
for flag, arg in self._keyword.items()
|
1006
|
+
if flag.startswith(last) and arg.dest not in consumed_dests
|
1007
|
+
]
|
1008
|
+
suggestions.extend(possible_flags)
|
1009
|
+
# Case 3: Flag that expects a value (e.g., ["--tag"])
|
1010
|
+
elif last in self._keyword:
|
1011
|
+
arg = self._keyword[last]
|
1012
|
+
if arg.choices:
|
1013
|
+
suggestions.extend(arg.choices)
|
1014
|
+
elif arg.suggestions:
|
1015
|
+
suggestions.extend(arg.suggestions)
|
1016
|
+
# Case 4: Last flag with choices mid-choice (e.g., ["--tag", "v"])
|
1017
|
+
elif next_to_last in self._keyword:
|
1018
|
+
arg = self._keyword[next_to_last]
|
1019
|
+
if arg.choices and last not in arg.choices:
|
1020
|
+
suggestions.extend(arg.choices)
|
1021
|
+
elif (
|
1022
|
+
arg.suggestions
|
1023
|
+
and last not in arg.suggestions
|
1024
|
+
and not any(last.startswith(suggestion) for suggestion in arg.suggestions)
|
1025
|
+
and any(suggestion.startswith(last) for suggestion in arg.suggestions)
|
1026
|
+
):
|
1027
|
+
suggestions.extend(arg.suggestions)
|
1028
|
+
else:
|
1029
|
+
suggestions.extend(remaining_flags)
|
1030
|
+
# Case 5: Suggest all remaining flags
|
1031
|
+
else:
|
1032
|
+
suggestions.extend(remaining_flags)
|
1033
|
+
|
1034
|
+
return sorted(set(suggestions))
|
1035
|
+
|
917
1036
|
def get_options_text(self, plain_text=False) -> str:
|
918
1037
|
# Options
|
919
1038
|
# Add all keyword arguments to the options list
|
falyx/parser/signature.py
CHANGED
@@ -54,8 +54,10 @@ def infer_args_from_func(
|
|
54
54
|
if arg_type is bool:
|
55
55
|
if param.default is False:
|
56
56
|
action = "store_true"
|
57
|
-
|
57
|
+
default = None
|
58
|
+
elif param.default is True:
|
58
59
|
action = "store_false"
|
60
|
+
default = None
|
59
61
|
|
60
62
|
if arg_type is list:
|
61
63
|
action = "append"
|
@@ -75,6 +77,7 @@ def infer_args_from_func(
|
|
75
77
|
"action": action,
|
76
78
|
"help": metadata.get("help", ""),
|
77
79
|
"choices": metadata.get("choices"),
|
80
|
+
"suggestions": metadata.get("suggestions"),
|
78
81
|
}
|
79
82
|
)
|
80
83
|
|
falyx/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.63"
|
@@ -4,13 +4,13 @@ falyx/__main__.py,sha256=xHO4pB45rccixo-ougF84QJeB36ef8mEZXWVK_CJL9M,3420
|
|
4
4
|
falyx/action/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
5
|
falyx/action/__init__.py,sha256=_DfOQkNOCTmtC8DmS4rD4h5Vv9tmf0dab6VVyvvdQWU,1464
|
6
6
|
falyx/action/action.py,sha256=Pm9lnxKhJoI1vs-fadAJrXkcDGYcBCMwvQ81HHxd0HA,5879
|
7
|
-
falyx/action/action_factory.py,sha256=
|
8
|
-
falyx/action/action_group.py,sha256=
|
7
|
+
falyx/action/action_factory.py,sha256=hxSN1PVVnsZ7mYhO0CqbFjcNBPkPRCHZ9JZ0vvwly38,5103
|
8
|
+
falyx/action/action_group.py,sha256=TBovifJ-HIBTBt_uBcoGXmj3MYpG8NdH37bIB5Q0e6Y,7992
|
9
9
|
falyx/action/action_mixins.py,sha256=oUrjbweCeateshg3tqtbQiGuV8u4GvlioIZCUr9D1m4,1244
|
10
|
-
falyx/action/action_types.py,sha256=
|
10
|
+
falyx/action/action_types.py,sha256=3dT3k4c4aYcuuqcxPXPYO-YLuOqwNTApRAS9alnljoA,2392
|
11
11
|
falyx/action/base_action.py,sha256=o9Nml70-SEVTXnu9J0-VYnO-t5wZZM0o59lYDth24Po,5829
|
12
|
-
falyx/action/chained_action.py,sha256=
|
13
|
-
falyx/action/confirm_action.py,sha256=
|
12
|
+
falyx/action/chained_action.py,sha256=hAFjdUUXhPBIR8AEPV4G3tJq_tjZPrxc6Xk8we2KZNw,9981
|
13
|
+
falyx/action/confirm_action.py,sha256=deyqeO_dYLdumkQD1Io2BajtZ9LkM6MLqTB_9UlEP-o,9385
|
14
14
|
falyx/action/fallback_action.py,sha256=3FGWfoR1MIgY0ZkDNOpKu8p3JqPWzh5ON3943mfgDGs,1708
|
15
15
|
falyx/action/http_action.py,sha256=DNeSBWh58UTFGlfFyTk2GnhS54hpLAJLC0QNbq2cYic,5799
|
16
16
|
falyx/action/io_action.py,sha256=V888tQgAynqsVvkhICnEeE4wRs2vvdTcdlEpDSEbHqo,6128
|
@@ -18,7 +18,7 @@ falyx/action/literal_input_action.py,sha256=ShXXiUYKg01BMZRChlxEWlNcaLXV1B1LW-w5
|
|
18
18
|
falyx/action/load_file_action.py,sha256=HcwSVWI8-4_dp3VC4iHR3ARaTpY0Y1rChMqpyqQjIGg,8091
|
19
19
|
falyx/action/menu_action.py,sha256=UwMF3Y3v8AWXGCkVpzj_k3pCge5BlJvKhqGYXh_dNCc,5775
|
20
20
|
falyx/action/process_action.py,sha256=nUNcJD6Ms34vmj8njWzv1R1P9xJTyJmelnyJksHcp7M,4666
|
21
|
-
falyx/action/process_pool_action.py,sha256=
|
21
|
+
falyx/action/process_pool_action.py,sha256=bBemgzB_shJfPytjbn1n6g9ScrpqrxMCujZU-I-UFf0,6152
|
22
22
|
falyx/action/prompt_menu_action.py,sha256=PTn6US8ql5SU7ilEMVCeoGqKTc31be3AbdCfcrZ6ujU,5034
|
23
23
|
falyx/action/save_file_action.py,sha256=Pe_j0hZjDNsO14bykzVYM0gkWB3zmpB1cExSN01IQOI,9899
|
24
24
|
falyx/action/select_file_action.py,sha256=PcV22_wiPeDoJLIhHRiEUmW8N3pYeqQZMVTscQKXuas,9867
|
@@ -28,15 +28,14 @@ falyx/action/signal_action.py,sha256=GxV-0zqYqODOQUa3-tvFTZ2AS1W1QpW6ExonxmWNWbs
|
|
28
28
|
falyx/action/user_input_action.py,sha256=Up47lumscxnhORMvaft0X-NWpxTXc2GmMZMua__pGhA,3524
|
29
29
|
falyx/bottom_bar.py,sha256=B62N3YCQF_h2Rw_hpc2_FUuLNARI-XIGbQkg-1XvaYE,7405
|
30
30
|
falyx/command.py,sha256=QdcwLEFIaq3a4Lfot4cV3zHbVJNQxwSpShprBgLBkh8,16891
|
31
|
-
falyx/completer.py,sha256=
|
31
|
+
falyx/completer.py,sha256=tCePNM6NoVmgbDobE6HSrR34KiJ9N5GwXYL2lcdqCfk,2461
|
32
32
|
falyx/config.py,sha256=OFEq9pFhV39o6_D7dP_QUDjqEusSQNpgomRsh5AAZYY,9621
|
33
33
|
falyx/console.py,sha256=WIZ004R9x6DJp2g3onBQ4DOJ7iDeTOr8HqJCwRt30Rc,143
|
34
34
|
falyx/context.py,sha256=M7iWEKto_NwI3GM-VCDPwXT0dBpFPf1Y_RvHQKodZgI,10804
|
35
35
|
falyx/debug.py,sha256=pguI0XQcZ-7jte5YUPexAufa1oxxalYO1JgmO6GU3rI,1557
|
36
36
|
falyx/exceptions.py,sha256=58D4BYkyJ7PlpZoNk37GsUsFThm_gIlb2Ex2XXhLklI,1099
|
37
37
|
falyx/execution_registry.py,sha256=RLRMOEmfDElFy4tuC8L9tRyNToX7GJ4GoEBh2Iri8zo,7662
|
38
|
-
falyx/falyx.py,sha256=
|
39
|
-
falyx/falyx_completer.py,sha256=MsfuZXpfGwbsGG-4Zp-j-vNsNnaote-UAJkJh0s2NZI,5236
|
38
|
+
falyx/falyx.py,sha256=WajWR9MZwZAno4gnXQR-RCIS4hEqiY25Egrfu6Wz2ls,49290
|
40
39
|
falyx/hook_manager.py,sha256=TFuHQnAncS_rk6vuw-VSx8bnAppLuHfrZCrzLwqcO9o,2979
|
41
40
|
falyx/hooks.py,sha256=xMfQROib0BNsaQF4AXJpmCiGePoE1f1xpcdibgnVZWM,2913
|
42
41
|
falyx/init.py,sha256=fZ8cvJ9rTGOhvAiAUmA7aPw9FsOUIuobTPW3sz47My8,3287
|
@@ -45,12 +44,12 @@ falyx/menu.py,sha256=9kvLZhkC8PoSQvv1NZQsPIFSDy11dXfFgqVAuDmtfsM,3752
|
|
45
44
|
falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
|
46
45
|
falyx/parser/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
46
|
falyx/parser/__init__.py,sha256=NbxAovKIY-duFTs6DAsdM_OzL7s3VIu19KMOmltX9ts,512
|
48
|
-
falyx/parser/argument.py,sha256=
|
47
|
+
falyx/parser/argument.py,sha256=pX1qTAtsqw2qBlu8dKRdIfViNC3Pg3QZtdUb7ZTT5oc,4226
|
49
48
|
falyx/parser/argument_action.py,sha256=Lcpb9siYr_q2T8qU-jXVtqFb11bFPPKEGH3gurJv2NM,757
|
50
|
-
falyx/parser/command_argument_parser.py,sha256=
|
49
|
+
falyx/parser/command_argument_parser.py,sha256=n6yTIGuwproH0RC2ZJ1T4RNpr97OVs4kt8GxVB2pbcw,46092
|
51
50
|
falyx/parser/parser_types.py,sha256=DLLuIXE8cAVLS41trfsNy-XJmtqSa1HfnJVAYIIc42w,315
|
52
51
|
falyx/parser/parsers.py,sha256=vb-l_NNh5O9L98Lcafhz91flRLxC1BnW6U8JdeabRCw,14118
|
53
|
-
falyx/parser/signature.py,sha256=
|
52
|
+
falyx/parser/signature.py,sha256=yGcd_Clcoz1YmbCgmP61MR2aNU5H6X_PhdW1fW6pvKs,2673
|
54
53
|
falyx/parser/utils.py,sha256=GlxB1WORwoJ5XUtmmAVBUPaDV2nF9Hio7TbvNJvd8oY,3006
|
55
54
|
falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
|
56
55
|
falyx/protocols.py,sha256=vd9JL-TXdLEiAQXLw2UKLd3MUMivoG7iMLo08ZggwYQ,539
|
@@ -63,9 +62,9 @@ falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
|
|
63
62
|
falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
|
64
63
|
falyx/utils.py,sha256=U45xnZFUdoFC4xiji_9S1jHS5V7MvxSDtufP8EgB0SM,6732
|
65
64
|
falyx/validators.py,sha256=AXpMGnk1_7J7MAbbol6pkMAiSIdNHoF5pwtA2-xS6H8,6029
|
66
|
-
falyx/version.py,sha256=
|
67
|
-
falyx-0.1.
|
68
|
-
falyx-0.1.
|
69
|
-
falyx-0.1.
|
70
|
-
falyx-0.1.
|
71
|
-
falyx-0.1.
|
65
|
+
falyx/version.py,sha256=cTmhGBJnpgNHayq6sOIrPTQIgyT8ZhqsvcCSuLORZYQ,23
|
66
|
+
falyx-0.1.63.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
|
67
|
+
falyx-0.1.63.dist-info/METADATA,sha256=-gRHGPkv51x3udjxmGyVcavpHi1SlOSZj_EyIUvM40s,5561
|
68
|
+
falyx-0.1.63.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
69
|
+
falyx-0.1.63.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
|
70
|
+
falyx-0.1.63.dist-info/RECORD,,
|
falyx/falyx_completer.py
DELETED
@@ -1,128 +0,0 @@
|
|
1
|
-
from collections import Counter
|
2
|
-
from prompt_toolkit.completion import Completer, Completion
|
3
|
-
from prompt_toolkit.document import Document
|
4
|
-
from typing import Iterable, Set, Optional
|
5
|
-
import shlex
|
6
|
-
|
7
|
-
from falyx.command import Command
|
8
|
-
from falyx.parser.command_argument_parser import CommandArgumentParser
|
9
|
-
from falyx.parser.argument import Argument
|
10
|
-
from falyx.parser.argument_action import ArgumentAction
|
11
|
-
|
12
|
-
class FalyxCompleter(Completer):
|
13
|
-
"""Completer for Falyx commands and their arguments."""
|
14
|
-
def __init__(self, falyx: "Falyx"):
|
15
|
-
self.falyx = falyx
|
16
|
-
self._used_args: Set[str] = set()
|
17
|
-
self._used_args_counter: Counter = Counter()
|
18
|
-
|
19
|
-
def get_completions(self, document: Document, complete_event) -> Iterable[Completion]:
|
20
|
-
text = document.text_before_cursor
|
21
|
-
try:
|
22
|
-
tokens = shlex.split(text)
|
23
|
-
cursor_at_end_of_token = document.text_before_cursor.endswith((' ', '\t'))
|
24
|
-
except ValueError:
|
25
|
-
return
|
26
|
-
|
27
|
-
if not tokens or (len(tokens) == 1 and not cursor_at_end_of_token):
|
28
|
-
# Suggest command keys and aliases
|
29
|
-
yield from self._suggest_commands(tokens[0] if tokens else "")
|
30
|
-
return
|
31
|
-
|
32
|
-
command = self._match_command(tokens[0])
|
33
|
-
if not command:
|
34
|
-
return
|
35
|
-
|
36
|
-
if command.arg_parser is None:
|
37
|
-
return
|
38
|
-
|
39
|
-
self._set_used_args(tokens, command)
|
40
|
-
|
41
|
-
next_arg = self._next_expected_argument(tokens, command.arg_parser)
|
42
|
-
|
43
|
-
if next_arg:
|
44
|
-
# Positional arguments or required flagged arguments
|
45
|
-
yield from self._suggest_argument(next_arg, document)
|
46
|
-
else:
|
47
|
-
# Optional arguments
|
48
|
-
for arg in command.arg_parser._keyword.values():
|
49
|
-
if not self._arg_already_used(arg.dest):
|
50
|
-
yield from self._suggest_argument(arg, document)
|
51
|
-
|
52
|
-
def _set_used_args(self, tokens: list[str], command: Command) -> None:
|
53
|
-
"""Extracts used argument flags from the provided tokens."""
|
54
|
-
if not command.arg_parser:
|
55
|
-
return
|
56
|
-
self._used_args.clear()
|
57
|
-
self._used_args_counter.clear()
|
58
|
-
for token in tokens[1:]:
|
59
|
-
if token.startswith('-'):
|
60
|
-
if keyword_argument := command.arg_parser._keyword.get(token):
|
61
|
-
self._used_args_counter[keyword_argument.dest] += 1
|
62
|
-
if isinstance(keyword_argument.nargs, int) and self._used_args_counter[keyword_argument.dest] > keyword_argument.nargs:
|
63
|
-
continue
|
64
|
-
elif isinstance(keyword_argument.nargs, str) and keyword_argument.nargs in ("?"):
|
65
|
-
self._used_args.add(keyword_argument.dest)
|
66
|
-
else:
|
67
|
-
self._used_args.add(keyword_argument.dest)
|
68
|
-
else:
|
69
|
-
# Handle positional arguments
|
70
|
-
if command.arg_parser._positional:
|
71
|
-
for arg in command.arg_parser._positional.values():
|
72
|
-
if arg.dest not in self._used_args:
|
73
|
-
self._used_args.add(arg.dest)
|
74
|
-
break
|
75
|
-
print(f"Used args: {self._used_args}, Counter: {self._used_args_counter}")
|
76
|
-
|
77
|
-
def _suggest_commands(self, prefix: str) -> Iterable[Completion]:
|
78
|
-
prefix = prefix.upper()
|
79
|
-
seen = set()
|
80
|
-
for cmd in self.falyx.commands.values():
|
81
|
-
for key in [cmd.key] + cmd.aliases:
|
82
|
-
if key.upper().startswith(prefix) and key not in seen:
|
83
|
-
yield Completion(key, start_position=-len(prefix))
|
84
|
-
seen.add(key)
|
85
|
-
|
86
|
-
def _match_command(self, token: str) -> Optional[Command]:
|
87
|
-
token = token.lstrip("?").upper()
|
88
|
-
return self.falyx._name_map.get(token)
|
89
|
-
|
90
|
-
def _next_expected_argument(
|
91
|
-
self, tokens: list[str], parser: CommandArgumentParser
|
92
|
-
) -> Optional[Argument]:
|
93
|
-
"""Determine the next expected argument based on the current tokens."""
|
94
|
-
# Positional arguments first
|
95
|
-
for arg in parser._positional.values():
|
96
|
-
if arg.dest not in self._used_args:
|
97
|
-
return arg
|
98
|
-
|
99
|
-
# Then required keyword arguments
|
100
|
-
for arg in parser._keyword_list:
|
101
|
-
if arg.required and not self._arg_already_used(arg.dest):
|
102
|
-
return arg
|
103
|
-
|
104
|
-
return None
|
105
|
-
|
106
|
-
def _arg_already_used(self, dest: str) -> bool:
|
107
|
-
print(f"Checking if argument '{dest}' is already used: {dest in self._used_args} - Used args: {self._used_args}")
|
108
|
-
return dest in self._used_args
|
109
|
-
|
110
|
-
def _suggest_argument(self, arg: Argument, document: Document) -> Iterable[Completion]:
|
111
|
-
if not arg.positional:
|
112
|
-
for flag in arg.flags:
|
113
|
-
yield Completion(flag, start_position=0)
|
114
|
-
|
115
|
-
if arg.choices:
|
116
|
-
for choice in arg.choices:
|
117
|
-
yield Completion(
|
118
|
-
choice,
|
119
|
-
start_position=0,
|
120
|
-
display=f"{arg.dest}={choice}"
|
121
|
-
)
|
122
|
-
|
123
|
-
if arg.default is not None and arg.action == ArgumentAction.STORE:
|
124
|
-
yield Completion(
|
125
|
-
str(arg.default),
|
126
|
-
start_position=0,
|
127
|
-
display=f"{arg.dest} (default: {arg.default})"
|
128
|
-
)
|
File without changes
|
File without changes
|
File without changes
|