falyx 0.1.33__py3-none-any.whl → 0.1.35__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/.coverage +0 -0
- falyx/action/__init__.py +2 -0
- falyx/action/action.py +1 -1
- falyx/action/prompt_menu_action.py +134 -0
- falyx/action/selection_action.py +40 -9
- falyx/command.py +4 -2
- falyx/falyx.py +18 -10
- falyx/menu.py +8 -0
- falyx/parsers/argparse.py +79 -35
- falyx/parsers/signature.py +1 -1
- falyx/selection.py +1 -1
- falyx/version.py +1 -1
- {falyx-0.1.33.dist-info → falyx-0.1.35.dist-info}/METADATA +1 -1
- {falyx-0.1.33.dist-info → falyx-0.1.35.dist-info}/RECORD +17 -15
- {falyx-0.1.33.dist-info → falyx-0.1.35.dist-info}/LICENSE +0 -0
- {falyx-0.1.33.dist-info → falyx-0.1.35.dist-info}/WHEEL +0 -0
- {falyx-0.1.33.dist-info → falyx-0.1.35.dist-info}/entry_points.txt +0 -0
falyx/.coverage
ADDED
Binary file
|
falyx/action/__init__.py
CHANGED
@@ -18,6 +18,7 @@ from .action_factory import ActionFactoryAction
|
|
18
18
|
from .http_action import HTTPAction
|
19
19
|
from .io_action import BaseIOAction, ShellAction
|
20
20
|
from .menu_action import MenuAction
|
21
|
+
from .prompt_menu_action import PromptMenuAction
|
21
22
|
from .select_file_action import SelectFileAction
|
22
23
|
from .selection_action import SelectionAction
|
23
24
|
from .signal_action import SignalAction
|
@@ -40,4 +41,5 @@ __all__ = [
|
|
40
41
|
"FallbackAction",
|
41
42
|
"LiteralInputAction",
|
42
43
|
"UserInputAction",
|
44
|
+
"PromptMenuAction",
|
43
45
|
]
|
falyx/action/action.py
CHANGED
@@ -726,7 +726,7 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|
726
726
|
if context.extra["errors"]:
|
727
727
|
context.exception = Exception(
|
728
728
|
f"{len(context.extra['errors'])} action(s) failed: "
|
729
|
-
f"{' ,'.join(name for name, _ in context.extra[
|
729
|
+
f"{' ,'.join(name for name, _ in context.extra['errors'])}"
|
730
730
|
)
|
731
731
|
await self.hooks.trigger(HookType.ON_ERROR, context)
|
732
732
|
raise context.exception
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""prompt_menu_action.py"""
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from prompt_toolkit import PromptSession
|
6
|
+
from prompt_toolkit.formatted_text import FormattedText, merge_formatted_text
|
7
|
+
from rich.console import Console
|
8
|
+
from rich.tree import Tree
|
9
|
+
|
10
|
+
from falyx.action.action import BaseAction
|
11
|
+
from falyx.context import ExecutionContext
|
12
|
+
from falyx.execution_registry import ExecutionRegistry as er
|
13
|
+
from falyx.hook_manager import HookType
|
14
|
+
from falyx.logger import logger
|
15
|
+
from falyx.menu import MenuOptionMap
|
16
|
+
from falyx.signals import BackSignal, QuitSignal
|
17
|
+
from falyx.themes import OneColors
|
18
|
+
|
19
|
+
|
20
|
+
class PromptMenuAction(BaseAction):
|
21
|
+
"""PromptMenuAction class for creating prompt -> actions."""
|
22
|
+
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
name: str,
|
26
|
+
menu_options: MenuOptionMap,
|
27
|
+
*,
|
28
|
+
prompt_message: str = "Select > ",
|
29
|
+
default_selection: str = "",
|
30
|
+
inject_last_result: bool = False,
|
31
|
+
inject_into: str = "last_result",
|
32
|
+
console: Console | None = None,
|
33
|
+
prompt_session: PromptSession | None = None,
|
34
|
+
never_prompt: bool = False,
|
35
|
+
include_reserved: bool = True,
|
36
|
+
):
|
37
|
+
super().__init__(
|
38
|
+
name,
|
39
|
+
inject_last_result=inject_last_result,
|
40
|
+
inject_into=inject_into,
|
41
|
+
never_prompt=never_prompt,
|
42
|
+
)
|
43
|
+
self.menu_options = menu_options
|
44
|
+
self.prompt_message = prompt_message
|
45
|
+
self.default_selection = default_selection
|
46
|
+
self.console = console or Console(color_system="auto")
|
47
|
+
self.prompt_session = prompt_session or PromptSession()
|
48
|
+
self.include_reserved = include_reserved
|
49
|
+
|
50
|
+
def get_infer_target(self) -> tuple[None, None]:
|
51
|
+
return None, None
|
52
|
+
|
53
|
+
async def _run(self, *args, **kwargs) -> Any:
|
54
|
+
kwargs = self._maybe_inject_last_result(kwargs)
|
55
|
+
context = ExecutionContext(
|
56
|
+
name=self.name,
|
57
|
+
args=args,
|
58
|
+
kwargs=kwargs,
|
59
|
+
action=self,
|
60
|
+
)
|
61
|
+
|
62
|
+
effective_default = self.default_selection
|
63
|
+
maybe_result = str(self.last_result)
|
64
|
+
if maybe_result in self.menu_options:
|
65
|
+
effective_default = maybe_result
|
66
|
+
elif self.inject_last_result:
|
67
|
+
logger.warning(
|
68
|
+
"[%s] Injected last result '%s' not found in menu options",
|
69
|
+
self.name,
|
70
|
+
maybe_result,
|
71
|
+
)
|
72
|
+
|
73
|
+
if self.never_prompt and not effective_default:
|
74
|
+
raise ValueError(
|
75
|
+
f"[{self.name}] 'never_prompt' is True but no valid default_selection"
|
76
|
+
" was provided."
|
77
|
+
)
|
78
|
+
|
79
|
+
context.start_timer()
|
80
|
+
try:
|
81
|
+
await self.hooks.trigger(HookType.BEFORE, context)
|
82
|
+
key = effective_default
|
83
|
+
if not self.never_prompt:
|
84
|
+
placeholder_formatted_text = []
|
85
|
+
for index, (key, option) in enumerate(self.menu_options.items()):
|
86
|
+
placeholder_formatted_text.append(option.render_prompt(key))
|
87
|
+
if index < len(self.menu_options) - 1:
|
88
|
+
placeholder_formatted_text.append(
|
89
|
+
FormattedText([(OneColors.WHITE, " | ")])
|
90
|
+
)
|
91
|
+
placeholder = merge_formatted_text(placeholder_formatted_text)
|
92
|
+
key = await self.prompt_session.prompt_async(
|
93
|
+
message=self.prompt_message, placeholder=placeholder
|
94
|
+
)
|
95
|
+
option = self.menu_options[key]
|
96
|
+
result = await option.action(*args, **kwargs)
|
97
|
+
context.result = result
|
98
|
+
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
99
|
+
return result
|
100
|
+
|
101
|
+
except BackSignal:
|
102
|
+
logger.debug("[%s][BackSignal] ← Returning to previous menu", self.name)
|
103
|
+
return None
|
104
|
+
except QuitSignal:
|
105
|
+
logger.debug("[%s][QuitSignal] ← Exiting application", self.name)
|
106
|
+
raise
|
107
|
+
except Exception as error:
|
108
|
+
context.exception = error
|
109
|
+
await self.hooks.trigger(HookType.ON_ERROR, context)
|
110
|
+
raise
|
111
|
+
finally:
|
112
|
+
context.stop_timer()
|
113
|
+
await self.hooks.trigger(HookType.AFTER, context)
|
114
|
+
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
|
115
|
+
er.record(context)
|
116
|
+
|
117
|
+
async def preview(self, parent: Tree | None = None):
|
118
|
+
label = f"[{OneColors.LIGHT_YELLOW_b}]📋 PromptMenuAction[/] '{self.name}'"
|
119
|
+
tree = parent.add(label) if parent else Tree(label)
|
120
|
+
for key, option in self.menu_options.items():
|
121
|
+
tree.add(
|
122
|
+
f"[dim]{key}[/]: {option.description} → [italic]{option.action.name}[/]"
|
123
|
+
)
|
124
|
+
await option.action.preview(parent=tree)
|
125
|
+
if not parent:
|
126
|
+
self.console.print(tree)
|
127
|
+
|
128
|
+
def __str__(self) -> str:
|
129
|
+
return (
|
130
|
+
f"PromptMenuAction(name={self.name!r}, options={list(self.menu_options.keys())!r}, "
|
131
|
+
f"default_selection={self.default_selection!r}, "
|
132
|
+
f"include_reserved={self.include_reserved}, "
|
133
|
+
f"prompt={'off' if self.never_prompt else 'on'})"
|
134
|
+
)
|
falyx/action/selection_action.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
2
|
"""selection_action.py"""
|
3
|
+
from copy import copy
|
3
4
|
from typing import Any
|
4
5
|
|
5
6
|
from prompt_toolkit import PromptSession
|
@@ -72,6 +73,7 @@ class SelectionAction(BaseAction):
|
|
72
73
|
self.default_selection = default_selection
|
73
74
|
self.prompt_message = prompt_message
|
74
75
|
self.show_table = show_table
|
76
|
+
self.cancel_key = self._find_cancel_key()
|
75
77
|
|
76
78
|
def _coerce_return_type(
|
77
79
|
self, return_type: SelectionReturnType | str
|
@@ -115,12 +117,40 @@ class SelectionAction(BaseAction):
|
|
115
117
|
)
|
116
118
|
|
117
119
|
def _find_cancel_key(self) -> str:
|
118
|
-
"""
|
119
|
-
|
120
|
-
|
121
|
-
|
120
|
+
"""Find the cancel key in the selections."""
|
121
|
+
if isinstance(self.selections, dict):
|
122
|
+
for index in range(len(self.selections) + 1):
|
123
|
+
if str(index) not in self.selections:
|
124
|
+
return str(index)
|
122
125
|
return str(len(self.selections))
|
123
126
|
|
127
|
+
@property
|
128
|
+
def cancel_key(self) -> str:
|
129
|
+
return self._cancel_key
|
130
|
+
|
131
|
+
@cancel_key.setter
|
132
|
+
def cancel_key(self, value: str) -> None:
|
133
|
+
"""Set the cancel key for the selection."""
|
134
|
+
if not isinstance(value, str):
|
135
|
+
raise TypeError("Cancel key must be a string.")
|
136
|
+
if isinstance(self.selections, dict) and value in self.selections:
|
137
|
+
raise ValueError(
|
138
|
+
"Cancel key cannot be one of the selection keys. "
|
139
|
+
f"Current selections: {self.selections}"
|
140
|
+
)
|
141
|
+
if isinstance(self.selections, list):
|
142
|
+
if not value.isdigit() or int(value) > len(self.selections):
|
143
|
+
raise ValueError(
|
144
|
+
"cancel_key must be a digit and not greater than the number of selections."
|
145
|
+
)
|
146
|
+
self._cancel_key = value
|
147
|
+
|
148
|
+
def cancel_formatter(self, index: int, selection: str) -> str:
|
149
|
+
"""Format the cancel option for display."""
|
150
|
+
if self.cancel_key == str(index):
|
151
|
+
return f"[{index}] [{OneColors.DARK_RED}]Cancel[/]"
|
152
|
+
return f"[{index}] {selection}"
|
153
|
+
|
124
154
|
def get_infer_target(self) -> tuple[None, None]:
|
125
155
|
return None, None
|
126
156
|
|
@@ -164,16 +194,17 @@ class SelectionAction(BaseAction):
|
|
164
194
|
|
165
195
|
context.start_timer()
|
166
196
|
try:
|
167
|
-
cancel_key = self._find_cancel_key()
|
197
|
+
self.cancel_key = self._find_cancel_key()
|
168
198
|
await self.hooks.trigger(HookType.BEFORE, context)
|
169
199
|
if isinstance(self.selections, list):
|
170
200
|
table = render_selection_indexed_table(
|
171
201
|
title=self.title,
|
172
202
|
selections=self.selections + ["Cancel"],
|
173
203
|
columns=self.columns,
|
204
|
+
formatter=self.cancel_formatter,
|
174
205
|
)
|
175
206
|
if not self.never_prompt:
|
176
|
-
index = await prompt_for_index(
|
207
|
+
index: int | str = await prompt_for_index(
|
177
208
|
len(self.selections),
|
178
209
|
table,
|
179
210
|
default_selection=effective_default,
|
@@ -184,12 +215,12 @@ class SelectionAction(BaseAction):
|
|
184
215
|
)
|
185
216
|
else:
|
186
217
|
index = effective_default
|
187
|
-
if index == cancel_key:
|
218
|
+
if int(index) == int(self.cancel_key):
|
188
219
|
raise CancelSignal("User cancelled the selection.")
|
189
220
|
result: Any = self.selections[int(index)]
|
190
221
|
elif isinstance(self.selections, dict):
|
191
222
|
cancel_option = {
|
192
|
-
cancel_key: SelectionOption(
|
223
|
+
self.cancel_key: SelectionOption(
|
193
224
|
description="Cancel", value=CancelSignal, style=OneColors.DARK_RED
|
194
225
|
)
|
195
226
|
}
|
@@ -210,7 +241,7 @@ class SelectionAction(BaseAction):
|
|
210
241
|
)
|
211
242
|
else:
|
212
243
|
key = effective_default
|
213
|
-
if key == cancel_key:
|
244
|
+
if key == self.cancel_key:
|
214
245
|
raise CancelSignal("User cancelled the selection.")
|
215
246
|
if self.return_type == SelectionReturnType.KEY:
|
216
247
|
result = key
|
falyx/command.py
CHANGED
@@ -139,7 +139,7 @@ class Command(BaseModel):
|
|
139
139
|
|
140
140
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
141
141
|
|
142
|
-
def parse_args(
|
142
|
+
async def parse_args(
|
143
143
|
self, raw_args: list[str] | str, from_validate: bool = False
|
144
144
|
) -> tuple[tuple, dict]:
|
145
145
|
if callable(self.custom_parser):
|
@@ -165,7 +165,9 @@ class Command(BaseModel):
|
|
165
165
|
raw_args,
|
166
166
|
)
|
167
167
|
return ((), {})
|
168
|
-
return self.arg_parser.parse_args_split(
|
168
|
+
return await self.arg_parser.parse_args_split(
|
169
|
+
raw_args, from_validate=from_validate
|
170
|
+
)
|
169
171
|
|
170
172
|
@field_validator("action", mode="before")
|
171
173
|
@classmethod
|
falyx/falyx.py
CHANGED
@@ -83,8 +83,11 @@ class CommandValidator(Validator):
|
|
83
83
|
self.error_message = error_message
|
84
84
|
|
85
85
|
def validate(self, document) -> None:
|
86
|
+
pass
|
87
|
+
|
88
|
+
async def validate_async(self, document) -> None:
|
86
89
|
text = document.text
|
87
|
-
is_preview, choice, _, __ = self.falyx.get_command(text, from_validate=True)
|
90
|
+
is_preview, choice, _, __ = await self.falyx.get_command(text, from_validate=True)
|
88
91
|
if is_preview:
|
89
92
|
return None
|
90
93
|
if not choice:
|
@@ -188,7 +191,7 @@ class Falyx:
|
|
188
191
|
self.cli_args: Namespace | None = cli_args
|
189
192
|
self.render_menu: Callable[[Falyx], None] | None = render_menu
|
190
193
|
self.custom_table: Callable[[Falyx], Table] | Table | None = custom_table
|
191
|
-
self.
|
194
|
+
self._hide_menu_table: bool = hide_menu_table
|
192
195
|
self.validate_options(cli_args, options)
|
193
196
|
self._prompt_session: PromptSession | None = None
|
194
197
|
self.mode = FalyxMode.MENU
|
@@ -740,7 +743,7 @@ class Falyx:
|
|
740
743
|
return True, input_str[1:].strip()
|
741
744
|
return False, input_str.strip()
|
742
745
|
|
743
|
-
def get_command(
|
746
|
+
async def get_command(
|
744
747
|
self, raw_choices: str, from_validate=False
|
745
748
|
) -> tuple[bool, Command | None, tuple, dict[str, Any]]:
|
746
749
|
"""
|
@@ -773,7 +776,9 @@ class Falyx:
|
|
773
776
|
if is_preview:
|
774
777
|
return True, name_map[choice], args, kwargs
|
775
778
|
try:
|
776
|
-
args, kwargs = name_map[choice].parse_args(
|
779
|
+
args, kwargs = await name_map[choice].parse_args(
|
780
|
+
input_args, from_validate
|
781
|
+
)
|
777
782
|
except CommandArgumentError as error:
|
778
783
|
if not from_validate:
|
779
784
|
if not name_map[choice].show_help():
|
@@ -834,7 +839,7 @@ class Falyx:
|
|
834
839
|
"""Processes the action of the selected command."""
|
835
840
|
with patch_stdout(raw=True):
|
836
841
|
choice = await self.prompt_session.prompt_async()
|
837
|
-
is_preview, selected_command, args, kwargs = self.get_command(choice)
|
842
|
+
is_preview, selected_command, args, kwargs = await self.get_command(choice)
|
838
843
|
if not selected_command:
|
839
844
|
logger.info("Invalid command '%s'.", choice)
|
840
845
|
return True
|
@@ -876,7 +881,7 @@ class Falyx:
|
|
876
881
|
) -> Any:
|
877
882
|
"""Run a command by key without displaying the menu (non-interactive mode)."""
|
878
883
|
self.debug_hooks()
|
879
|
-
is_preview, selected_command, _, __ = self.get_command(command_key)
|
884
|
+
is_preview, selected_command, _, __ = await self.get_command(command_key)
|
880
885
|
kwargs = kwargs or {}
|
881
886
|
|
882
887
|
self.last_run_command = selected_command
|
@@ -975,7 +980,7 @@ class Falyx:
|
|
975
980
|
self.print_message(self.welcome_message)
|
976
981
|
try:
|
977
982
|
while True:
|
978
|
-
if not self.hide_menu_table:
|
983
|
+
if not self.options.get("hide_menu_table", self._hide_menu_table):
|
979
984
|
if callable(self.render_menu):
|
980
985
|
self.render_menu(self)
|
981
986
|
else:
|
@@ -1012,6 +1017,9 @@ class Falyx:
|
|
1012
1017
|
if not self.options.get("force_confirm"):
|
1013
1018
|
self.options.set("force_confirm", self._force_confirm)
|
1014
1019
|
|
1020
|
+
if not self.options.get("hide_menu_table"):
|
1021
|
+
self.options.set("hide_menu_table", self._hide_menu_table)
|
1022
|
+
|
1015
1023
|
if self.cli_args.verbose:
|
1016
1024
|
logging.getLogger("falyx").setLevel(logging.DEBUG)
|
1017
1025
|
|
@@ -1029,7 +1037,7 @@ class Falyx:
|
|
1029
1037
|
|
1030
1038
|
if self.cli_args.command == "preview":
|
1031
1039
|
self.mode = FalyxMode.PREVIEW
|
1032
|
-
_, command, args, kwargs = self.get_command(self.cli_args.name)
|
1040
|
+
_, command, args, kwargs = await self.get_command(self.cli_args.name)
|
1033
1041
|
if not command:
|
1034
1042
|
self.console.print(
|
1035
1043
|
f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found."
|
@@ -1043,7 +1051,7 @@ class Falyx:
|
|
1043
1051
|
|
1044
1052
|
if self.cli_args.command == "run":
|
1045
1053
|
self.mode = FalyxMode.RUN
|
1046
|
-
is_preview, command, _, __ = self.get_command(self.cli_args.name)
|
1054
|
+
is_preview, command, _, __ = await self.get_command(self.cli_args.name)
|
1047
1055
|
if is_preview:
|
1048
1056
|
if command is None:
|
1049
1057
|
sys.exit(1)
|
@@ -1054,7 +1062,7 @@ class Falyx:
|
|
1054
1062
|
sys.exit(1)
|
1055
1063
|
self._set_retry_policy(command)
|
1056
1064
|
try:
|
1057
|
-
args, kwargs = command.parse_args(self.cli_args.command_args)
|
1065
|
+
args, kwargs = await command.parse_args(self.cli_args.command_args)
|
1058
1066
|
except HelpSignal:
|
1059
1067
|
sys.exit(0)
|
1060
1068
|
try:
|
falyx/menu.py
CHANGED
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
|
5
|
+
from prompt_toolkit.formatted_text import FormattedText
|
6
|
+
|
5
7
|
from falyx.action import BaseAction
|
6
8
|
from falyx.signals import BackSignal, QuitSignal
|
7
9
|
from falyx.themes import OneColors
|
@@ -26,6 +28,12 @@ class MenuOption:
|
|
26
28
|
"""Render the menu option for display."""
|
27
29
|
return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
|
28
30
|
|
31
|
+
def render_prompt(self, key: str) -> FormattedText:
|
32
|
+
"""Render the menu option for prompt display."""
|
33
|
+
return FormattedText(
|
34
|
+
[(OneColors.WHITE, f"[{key}] "), (self.style, self.description)]
|
35
|
+
)
|
36
|
+
|
29
37
|
|
30
38
|
class MenuOptionMap(CaseInsensitiveDict):
|
31
39
|
"""
|
falyx/parsers/argparse.py
CHANGED
@@ -39,7 +39,7 @@ class ArgumentAction(Enum):
|
|
39
39
|
class Argument:
|
40
40
|
"""Represents a command-line argument."""
|
41
41
|
|
42
|
-
flags:
|
42
|
+
flags: tuple[str, ...]
|
43
43
|
dest: str # Destination name for the argument
|
44
44
|
action: ArgumentAction = (
|
45
45
|
ArgumentAction.STORE
|
@@ -49,7 +49,7 @@ class Argument:
|
|
49
49
|
choices: list[str] | None = None # List of valid choices for the argument
|
50
50
|
required: bool = False # True if the argument is required
|
51
51
|
help: str = "" # Help text for the argument
|
52
|
-
nargs: int | str =
|
52
|
+
nargs: int | str | None = None # int, '?', '*', '+', None
|
53
53
|
positional: bool = False # True if no leading - or -- in flags
|
54
54
|
|
55
55
|
def get_positional_text(self) -> str:
|
@@ -151,6 +151,7 @@ class CommandArgumentParser:
|
|
151
151
|
aliases: list[str] | None = None,
|
152
152
|
) -> None:
|
153
153
|
"""Initialize the CommandArgumentParser."""
|
154
|
+
self.console = Console(color_system="auto")
|
154
155
|
self.command_key: str = command_key
|
155
156
|
self.command_description: str = command_description
|
156
157
|
self.command_style: str = command_style
|
@@ -163,7 +164,6 @@ class CommandArgumentParser:
|
|
163
164
|
self._flag_map: dict[str, Argument] = {}
|
164
165
|
self._dest_set: set[str] = set()
|
165
166
|
self._add_help()
|
166
|
-
self.console = Console(color_system="auto")
|
167
167
|
|
168
168
|
def _add_help(self):
|
169
169
|
"""Add help argument to the parser."""
|
@@ -185,9 +185,7 @@ class CommandArgumentParser:
|
|
185
185
|
raise CommandArgumentError("Positional arguments cannot have multiple flags")
|
186
186
|
return positional
|
187
187
|
|
188
|
-
def _get_dest_from_flags(
|
189
|
-
self, flags: tuple[str, ...], dest: str | None
|
190
|
-
) -> str | None:
|
188
|
+
def _get_dest_from_flags(self, flags: tuple[str, ...], dest: str | None) -> str:
|
191
189
|
"""Convert flags to a destination name."""
|
192
190
|
if dest:
|
193
191
|
if not dest.replace("_", "").isalnum():
|
@@ -216,7 +214,7 @@ class CommandArgumentParser:
|
|
216
214
|
return dest
|
217
215
|
|
218
216
|
def _determine_required(
|
219
|
-
self, required: bool, positional: bool, nargs: int | str
|
217
|
+
self, required: bool, positional: bool, nargs: int | str | None
|
220
218
|
) -> bool:
|
221
219
|
"""Determine if the argument is required."""
|
222
220
|
if required:
|
@@ -234,7 +232,22 @@ class CommandArgumentParser:
|
|
234
232
|
|
235
233
|
return required
|
236
234
|
|
237
|
-
def _validate_nargs(
|
235
|
+
def _validate_nargs(
|
236
|
+
self, nargs: int | str | None, action: ArgumentAction
|
237
|
+
) -> int | str | None:
|
238
|
+
if action in (
|
239
|
+
ArgumentAction.STORE_FALSE,
|
240
|
+
ArgumentAction.STORE_TRUE,
|
241
|
+
ArgumentAction.COUNT,
|
242
|
+
ArgumentAction.HELP,
|
243
|
+
):
|
244
|
+
if nargs is not None:
|
245
|
+
raise CommandArgumentError(
|
246
|
+
f"nargs cannot be specified for {action} actions"
|
247
|
+
)
|
248
|
+
return None
|
249
|
+
if nargs is None:
|
250
|
+
nargs = 1
|
238
251
|
allowed_nargs = ("?", "*", "+")
|
239
252
|
if isinstance(nargs, int):
|
240
253
|
if nargs <= 0:
|
@@ -246,7 +259,9 @@ class CommandArgumentParser:
|
|
246
259
|
raise CommandArgumentError(f"nargs must be an int or one of {allowed_nargs}")
|
247
260
|
return nargs
|
248
261
|
|
249
|
-
def _normalize_choices(
|
262
|
+
def _normalize_choices(
|
263
|
+
self, choices: Iterable | None, expected_type: Any
|
264
|
+
) -> list[Any]:
|
250
265
|
if choices is not None:
|
251
266
|
if isinstance(choices, dict):
|
252
267
|
raise CommandArgumentError("choices cannot be a dict")
|
@@ -293,8 +308,34 @@ class CommandArgumentParser:
|
|
293
308
|
f"Default list value {default!r} for '{dest}' cannot be coerced to {expected_type.__name__}"
|
294
309
|
)
|
295
310
|
|
311
|
+
def _validate_action(
|
312
|
+
self, action: ArgumentAction | str, positional: bool
|
313
|
+
) -> ArgumentAction:
|
314
|
+
if not isinstance(action, ArgumentAction):
|
315
|
+
try:
|
316
|
+
action = ArgumentAction(action)
|
317
|
+
except ValueError:
|
318
|
+
raise CommandArgumentError(
|
319
|
+
f"Invalid action '{action}' is not a valid ArgumentAction"
|
320
|
+
)
|
321
|
+
if action in (
|
322
|
+
ArgumentAction.STORE_TRUE,
|
323
|
+
ArgumentAction.STORE_FALSE,
|
324
|
+
ArgumentAction.COUNT,
|
325
|
+
ArgumentAction.HELP,
|
326
|
+
):
|
327
|
+
if positional:
|
328
|
+
raise CommandArgumentError(
|
329
|
+
f"Action '{action}' cannot be used with positional arguments"
|
330
|
+
)
|
331
|
+
|
332
|
+
return action
|
333
|
+
|
296
334
|
def _resolve_default(
|
297
|
-
self,
|
335
|
+
self,
|
336
|
+
default: Any,
|
337
|
+
action: ArgumentAction,
|
338
|
+
nargs: str | int | None,
|
298
339
|
) -> Any:
|
299
340
|
"""Get the default value for the argument."""
|
300
341
|
if default is None:
|
@@ -328,7 +369,18 @@ class CommandArgumentParser:
|
|
328
369
|
f"Flag '{flag}' must be a single character or start with '--'"
|
329
370
|
)
|
330
371
|
|
331
|
-
def add_argument(
|
372
|
+
def add_argument(
|
373
|
+
self,
|
374
|
+
*flags,
|
375
|
+
action: str | ArgumentAction = "store",
|
376
|
+
nargs: int | str | None = None,
|
377
|
+
default: Any = None,
|
378
|
+
type: Any = str,
|
379
|
+
choices: Iterable | None = None,
|
380
|
+
required: bool = False,
|
381
|
+
help: str = "",
|
382
|
+
dest: str | None = None,
|
383
|
+
) -> None:
|
332
384
|
"""Add an argument to the parser.
|
333
385
|
Args:
|
334
386
|
name or flags: Either a name or prefixed flags (e.g. 'faylx', '-f', '--falyx').
|
@@ -341,9 +393,10 @@ class CommandArgumentParser:
|
|
341
393
|
help: A brief description of the argument.
|
342
394
|
dest: The name of the attribute to be added to the object returned by parse_args().
|
343
395
|
"""
|
396
|
+
expected_type = type
|
344
397
|
self._validate_flags(flags)
|
345
398
|
positional = self._is_positional(flags)
|
346
|
-
dest = self._get_dest_from_flags(flags,
|
399
|
+
dest = self._get_dest_from_flags(flags, dest)
|
347
400
|
if dest in self._dest_set:
|
348
401
|
raise CommandArgumentError(
|
349
402
|
f"Destination '{dest}' is already defined.\n"
|
@@ -351,18 +404,9 @@ class CommandArgumentParser:
|
|
351
404
|
"is not supported. Define a unique 'dest' for each argument."
|
352
405
|
)
|
353
406
|
self._dest_set.add(dest)
|
354
|
-
action =
|
355
|
-
|
356
|
-
|
357
|
-
action = ArgumentAction(action)
|
358
|
-
except ValueError:
|
359
|
-
raise CommandArgumentError(
|
360
|
-
f"Invalid action '{action}' is not a valid ArgumentAction"
|
361
|
-
)
|
362
|
-
flags = list(flags)
|
363
|
-
nargs = self._validate_nargs(kwargs.get("nargs", 1))
|
364
|
-
default = self._resolve_default(action, kwargs.get("default"), nargs)
|
365
|
-
expected_type = kwargs.get("type", str)
|
407
|
+
action = self._validate_action(action, positional)
|
408
|
+
nargs = self._validate_nargs(nargs, action)
|
409
|
+
default = self._resolve_default(default, action, nargs)
|
366
410
|
if (
|
367
411
|
action in (ArgumentAction.STORE, ArgumentAction.APPEND, ArgumentAction.EXTEND)
|
368
412
|
and default is not None
|
@@ -371,14 +415,12 @@ class CommandArgumentParser:
|
|
371
415
|
self._validate_default_list_type(default, expected_type, dest)
|
372
416
|
else:
|
373
417
|
self._validate_default_type(default, expected_type, dest)
|
374
|
-
choices = self._normalize_choices(
|
418
|
+
choices = self._normalize_choices(choices, expected_type)
|
375
419
|
if default is not None and choices and default not in choices:
|
376
420
|
raise CommandArgumentError(
|
377
421
|
f"Default value '{default}' not in allowed choices: {choices}"
|
378
422
|
)
|
379
|
-
required = self._determine_required(
|
380
|
-
kwargs.get("required", False), positional, nargs
|
381
|
-
)
|
423
|
+
required = self._determine_required(required, positional, nargs)
|
382
424
|
argument = Argument(
|
383
425
|
flags=flags,
|
384
426
|
dest=dest,
|
@@ -387,7 +429,7 @@ class CommandArgumentParser:
|
|
387
429
|
default=default,
|
388
430
|
choices=choices,
|
389
431
|
required=required,
|
390
|
-
help=
|
432
|
+
help=help,
|
391
433
|
nargs=nargs,
|
392
434
|
positional=positional,
|
393
435
|
)
|
@@ -430,11 +472,11 @@ class CommandArgumentParser:
|
|
430
472
|
values = []
|
431
473
|
i = start
|
432
474
|
if isinstance(spec.nargs, int):
|
433
|
-
# assert i + spec.nargs <= len(
|
434
|
-
# args
|
435
|
-
# ), "Not enough arguments provided: shouldn't happen"
|
436
475
|
values = args[i : i + spec.nargs]
|
437
476
|
return values, i + spec.nargs
|
477
|
+
elif spec.nargs is None:
|
478
|
+
values = [args[i]]
|
479
|
+
return values, i + 1
|
438
480
|
elif spec.nargs == "+":
|
439
481
|
if i >= len(args):
|
440
482
|
raise CommandArgumentError(
|
@@ -479,6 +521,8 @@ class CommandArgumentParser:
|
|
479
521
|
for next_spec in positional_args[j + 1 :]:
|
480
522
|
if isinstance(next_spec.nargs, int):
|
481
523
|
min_required += next_spec.nargs
|
524
|
+
elif next_spec.nargs is None:
|
525
|
+
min_required += 1
|
482
526
|
elif next_spec.nargs == "+":
|
483
527
|
min_required += 1
|
484
528
|
elif next_spec.nargs == "?":
|
@@ -521,7 +565,7 @@ class CommandArgumentParser:
|
|
521
565
|
|
522
566
|
return i
|
523
567
|
|
524
|
-
def parse_args(
|
568
|
+
async def parse_args(
|
525
569
|
self, args: list[str] | None = None, from_validate: bool = False
|
526
570
|
) -> dict[str, Any]:
|
527
571
|
"""Parse Falyx Command arguments."""
|
@@ -669,7 +713,7 @@ class CommandArgumentParser:
|
|
669
713
|
result.pop("help", None)
|
670
714
|
return result
|
671
715
|
|
672
|
-
def parse_args_split(
|
716
|
+
async def parse_args_split(
|
673
717
|
self, args: list[str], from_validate: bool = False
|
674
718
|
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
675
719
|
"""
|
@@ -677,7 +721,7 @@ class CommandArgumentParser:
|
|
677
721
|
tuple[args, kwargs] - Positional arguments in defined order,
|
678
722
|
followed by keyword argument mapping.
|
679
723
|
"""
|
680
|
-
parsed = self.parse_args(args, from_validate)
|
724
|
+
parsed = await self.parse_args(args, from_validate)
|
681
725
|
args_list = []
|
682
726
|
kwargs_dict = {}
|
683
727
|
for arg in self._arguments:
|
falyx/parsers/signature.py
CHANGED
falyx/selection.py
CHANGED
@@ -271,7 +271,7 @@ async def prompt_for_index(
|
|
271
271
|
prompt_session: PromptSession | None = None,
|
272
272
|
prompt_message: str = "Select an option > ",
|
273
273
|
show_table: bool = True,
|
274
|
-
):
|
274
|
+
) -> int:
|
275
275
|
prompt_session = prompt_session or PromptSession()
|
276
276
|
console = console or Console(color_system="auto")
|
277
277
|
|
falyx/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.35"
|
@@ -1,52 +1,54 @@
|
|
1
|
+
falyx/.coverage,sha256=DNx1Ew1vSvuIcKko7httsyL62erJxVQ6CKtuJKxRVj4,53248
|
1
2
|
falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
3
|
falyx/__init__.py,sha256=MZzno-7HvIYM6pDjDP4t22aN7OaBTTlQYOb7W3Gw_7g,615
|
3
4
|
falyx/__main__.py,sha256=g_LwJieofK3DJzCYtpkAMEeOXhzSLQenb7pRVUqcf-Y,2152
|
4
5
|
falyx/action/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
falyx/action/__init__.py,sha256=
|
6
|
-
falyx/action/action.py,sha256
|
6
|
+
falyx/action/__init__.py,sha256=Vd-XNp_MEpyfbHL9PAhSOXQAlwKKsIpZFbO5pskHdOA,1047
|
7
|
+
falyx/action/action.py,sha256=-pqM8Nr8nwUTFoCtY3cZFnDLM3DRHtEEIjGX8ow-8wo,33389
|
7
8
|
falyx/action/action_factory.py,sha256=iJ7jx8WxQ9W2v1-xBoUzFD3X-3IFQjjClfGYCg5io4o,4834
|
8
9
|
falyx/action/http_action.py,sha256=aIieGHyZSkz1ZGay-fwgDYZ0QF17XypAWtKeVAYp5f4,5806
|
9
10
|
falyx/action/io_action.py,sha256=tQVonWst44ZXR_87H6-aQ62DZf7qfpbBiKE1oTp9qVA,10061
|
10
11
|
falyx/action/menu_action.py,sha256=Qv8iUqlQeXMT6lCXg99YuRGswGWS_tkU8VeuqkC_uQA,5763
|
12
|
+
falyx/action/prompt_menu_action.py,sha256=8Qh8egbqiF9agTdeU45IrlehY6fxGc9rtyhIe0nfKK4,5165
|
11
13
|
falyx/action/select_file_action.py,sha256=VZkz4H17-XbGooniBMN9ZG406GLerHOrI_BS7hZkkTI,8615
|
12
|
-
falyx/action/selection_action.py,sha256=
|
14
|
+
falyx/action/selection_action.py,sha256=8ts0WM3UF5ziMGLeg8faTVKwvzzOGPKxeZJF2z3o9BY,12918
|
13
15
|
falyx/action/signal_action.py,sha256=5UMqvzy7fBnLANGwYUWoe1VRhrr7e-yOVeLdOnCBiJo,1350
|
14
16
|
falyx/action/types.py,sha256=NfZz1ufZuvCgp-he2JIItbnjX7LjOUadjtKbjpRlSIY,1399
|
15
17
|
falyx/action/user_input_action.py,sha256=TQ7BlH9lQ5h_t28q6UXCH0hbr2b0vJP7syJmM-okwS0,3478
|
16
18
|
falyx/bottom_bar.py,sha256=iWxgOKWgn5YmREeZBuGA50FzqzEfz1-Vnqm0V_fhldc,7383
|
17
|
-
falyx/command.py,sha256=
|
19
|
+
falyx/command.py,sha256=siyTV-8Bq0bZ99qyWesQveU-qhMwbfhUpT7poNH69q4,14598
|
18
20
|
falyx/config.py,sha256=sApU53PHM8h8eV9YIuTDM1qSGp24AbNy_jJ6GAZhjEo,9597
|
19
21
|
falyx/context.py,sha256=FNF-IS7RMDxel2l3kskEqQImZ0mLO6zvGw_xC9cIzgI,10338
|
20
22
|
falyx/debug.py,sha256=oWWTLOF8elrx_RGZ1G4pbzfFr46FjB0woFXpVU2wmjU,1567
|
21
23
|
falyx/exceptions.py,sha256=kK9k1v7LVNjJSwYztRa9Krhr3ZOI-6Htq2ZjlYICPKg,922
|
22
24
|
falyx/execution_registry.py,sha256=rctsz0mrIHPToLZqylblVjDdKWdq1x_JBc8GwMP5sJ8,4710
|
23
|
-
falyx/falyx.py,sha256=
|
25
|
+
falyx/falyx.py,sha256=fNuVa6Af7E2TL_wZySL78_ydtS4JTTZTacvQctVeMm4,44422
|
24
26
|
falyx/hook_manager.py,sha256=LRRoVG3gomxI6C9HnLa24WOSZtA-UP9HmpnkT2nvBRo,2986
|
25
27
|
falyx/hooks.py,sha256=IV2nbj5FjY2m3_L7x4mYBnaRDG45E8tWQU90i4butlw,2940
|
26
28
|
falyx/init.py,sha256=abcSlPmxVeByLIHdUkNjqtO_tEkO3ApC6f9WbxsSEWg,3393
|
27
29
|
falyx/logger.py,sha256=1Mfb_vJFJ1tQwziuyU2p-cSMi2Js8N2byniFEnI6vOQ,132
|
28
|
-
falyx/menu.py,sha256=
|
30
|
+
falyx/menu.py,sha256=Dp_afAkRZoLe7s2DmNrePQelL-ZSpOELUFr-7pC_eGo,3740
|
29
31
|
falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
|
30
32
|
falyx/parsers/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
33
|
falyx/parsers/__init__.py,sha256=l0QMf89uJHhTpOqQfiV3tx7aAHvELqDFWAyjCbwEgBQ,370
|
32
|
-
falyx/parsers/argparse.py,sha256=
|
34
|
+
falyx/parsers/argparse.py,sha256=k8j0MiTwV53UqesZ-GsYSi7-T7XOOo7o1DB8UnFFZ7Q,31545
|
33
35
|
falyx/parsers/parsers.py,sha256=yGyAwNIJQz12LU_WF87aW4xbBmJtyEY7RnLRFqXAsio,5763
|
34
|
-
falyx/parsers/signature.py,sha256=
|
36
|
+
falyx/parsers/signature.py,sha256=i4iOiJxv70sxQYivKKXC_YOsShRUYfcI8Cjq8yVZvMo,2262
|
35
37
|
falyx/parsers/utils.py,sha256=_DUrqOdHDBZdeAqSLCFGnfNljbXTBS0d2T5MDqxGbWQ,888
|
36
38
|
falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
|
37
39
|
falyx/protocols.py,sha256=mesdq5CjPF_5Kyu7Evwr6qMT71tUHlw0SjjtmnggTZw,495
|
38
40
|
falyx/retry.py,sha256=UUzY6FlKobr84Afw7yJO9rj3AIQepDk2fcWs6_1gi6Q,3788
|
39
41
|
falyx/retry_utils.py,sha256=EAzc-ECTu8AxKkmlw28ioOW9y-Y9tLQ0KasvSkBRYgs,694
|
40
|
-
falyx/selection.py,sha256=
|
42
|
+
falyx/selection.py,sha256=PLfiULkJ76cn1yBFwUux9KEMt6NFF00_Nj5oHS2e498,12894
|
41
43
|
falyx/signals.py,sha256=Y_neFXpfHs7qY0syw9XcfR9WeAGRcRw1nG_2L1JJqKE,1083
|
42
44
|
falyx/tagged_table.py,sha256=4SV-SdXFrAhy1JNToeBCvyxT-iWVf6cWY7XETTys4n8,1067
|
43
45
|
falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
|
44
46
|
falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
|
45
47
|
falyx/utils.py,sha256=u3puR4Bh-unNBw9a0V9sw7PDTIzRaNLolap0oz5bVIk,6718
|
46
48
|
falyx/validators.py,sha256=t5iyzVpY8tdC4rfhr4isEfWpD5gNTzjeX_Hbi_Uq6sA,1328
|
47
|
-
falyx/version.py,sha256=
|
48
|
-
falyx-0.1.
|
49
|
-
falyx-0.1.
|
50
|
-
falyx-0.1.
|
51
|
-
falyx-0.1.
|
52
|
-
falyx-0.1.
|
49
|
+
falyx/version.py,sha256=ABqgofsnbWf7823vTBbZNQ81eKQbWwrVToAU6T3z-6s,23
|
50
|
+
falyx-0.1.35.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
|
51
|
+
falyx-0.1.35.dist-info/METADATA,sha256=PDVu81XfXOjmqytaXM-ymN7cCwfigPfE3Tgv63z30h4,5521
|
52
|
+
falyx-0.1.35.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
53
|
+
falyx-0.1.35.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
|
54
|
+
falyx-0.1.35.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|