falyx 0.1.29__py3-none-any.whl → 0.1.31__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/__init__.py +0 -1
- falyx/action/action.py +42 -14
- falyx/action/action_factory.py +8 -1
- falyx/action/io_action.py +16 -10
- falyx/action/menu_action.py +3 -0
- falyx/action/select_file_action.py +30 -9
- falyx/action/selection_action.py +83 -19
- falyx/action/types.py +15 -0
- falyx/action/user_input_action.py +3 -0
- falyx/command.py +14 -61
- falyx/config.py +0 -1
- falyx/falyx.py +38 -46
- falyx/hook_manager.py +8 -7
- falyx/menu.py +20 -8
- falyx/parsers/__init__.py +0 -4
- falyx/parsers/argparse.py +11 -0
- falyx/parsers/signature.py +5 -2
- falyx/parsers/utils.py +5 -10
- falyx/selection.py +57 -1
- falyx/version.py +1 -1
- {falyx-0.1.29.dist-info → falyx-0.1.31.dist-info}/METADATA +1 -1
- {falyx-0.1.29.dist-info → falyx-0.1.31.dist-info}/RECORD +25 -25
- {falyx-0.1.29.dist-info → falyx-0.1.31.dist-info}/LICENSE +0 -0
- {falyx-0.1.29.dist-info → falyx-0.1.31.dist-info}/WHEEL +0 -0
- {falyx-0.1.29.dist-info → falyx-0.1.31.dist-info}/entry_points.txt +0 -0
falyx/command.py
CHANGED
@@ -19,7 +19,6 @@ in building robust interactive menus.
|
|
19
19
|
from __future__ import annotations
|
20
20
|
|
21
21
|
import shlex
|
22
|
-
from functools import cached_property
|
23
22
|
from typing import Any, Callable
|
24
23
|
|
25
24
|
from prompt_toolkit.formatted_text import FormattedText
|
@@ -27,25 +26,15 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
|
27
26
|
from rich.console import Console
|
28
27
|
from rich.tree import Tree
|
29
28
|
|
30
|
-
from falyx.action.action import
|
31
|
-
Action,
|
32
|
-
ActionGroup,
|
33
|
-
BaseAction,
|
34
|
-
ChainedAction,
|
35
|
-
ProcessAction,
|
36
|
-
)
|
37
|
-
from falyx.action.io_action import BaseIOAction
|
29
|
+
from falyx.action.action import Action, BaseAction
|
38
30
|
from falyx.context import ExecutionContext
|
39
31
|
from falyx.debug import register_debug_hooks
|
40
32
|
from falyx.execution_registry import ExecutionRegistry as er
|
41
33
|
from falyx.hook_manager import HookManager, HookType
|
42
34
|
from falyx.logger import logger
|
43
35
|
from falyx.options_manager import OptionsManager
|
44
|
-
from falyx.parsers import
|
45
|
-
|
46
|
-
infer_args_from_func,
|
47
|
-
same_argument_definitions,
|
48
|
-
)
|
36
|
+
from falyx.parsers.argparse import CommandArgumentParser
|
37
|
+
from falyx.parsers.signature import infer_args_from_func
|
49
38
|
from falyx.prompt_utils import confirm_async, should_prompt_user
|
50
39
|
from falyx.protocols import ArgParserProtocol
|
51
40
|
from falyx.retry import RetryPolicy
|
@@ -99,7 +88,6 @@ class Command(BaseModel):
|
|
99
88
|
retry_policy (RetryPolicy): Retry behavior configuration.
|
100
89
|
tags (list[str]): Organizational tags for the command.
|
101
90
|
logging_hooks (bool): Whether to attach logging hooks automatically.
|
102
|
-
requires_input (bool | None): Indicates if the action needs input.
|
103
91
|
options_manager (OptionsManager): Manages global command-line options.
|
104
92
|
arg_parser (CommandArgumentParser): Parses command arguments.
|
105
93
|
custom_parser (ArgParserProtocol | None): Custom argument parser.
|
@@ -116,7 +104,7 @@ class Command(BaseModel):
|
|
116
104
|
|
117
105
|
key: str
|
118
106
|
description: str
|
119
|
-
action: BaseAction | Callable[
|
107
|
+
action: BaseAction | Callable[..., Any]
|
120
108
|
args: tuple = ()
|
121
109
|
kwargs: dict[str, Any] = Field(default_factory=dict)
|
122
110
|
hidden: bool = False
|
@@ -138,14 +126,13 @@ class Command(BaseModel):
|
|
138
126
|
retry_policy: RetryPolicy = Field(default_factory=RetryPolicy)
|
139
127
|
tags: list[str] = Field(default_factory=list)
|
140
128
|
logging_hooks: bool = False
|
141
|
-
requires_input: bool | None = None
|
142
129
|
options_manager: OptionsManager = Field(default_factory=OptionsManager)
|
143
130
|
arg_parser: CommandArgumentParser = Field(default_factory=CommandArgumentParser)
|
144
131
|
arguments: list[dict[str, Any]] = Field(default_factory=list)
|
145
132
|
argument_config: Callable[[CommandArgumentParser], None] | None = None
|
146
133
|
custom_parser: ArgParserProtocol | None = None
|
147
134
|
custom_help: Callable[[], str | None] | None = None
|
148
|
-
auto_args: bool =
|
135
|
+
auto_args: bool = True
|
149
136
|
arg_metadata: dict[str, str | dict[str, Any]] = Field(default_factory=dict)
|
150
137
|
|
151
138
|
_context: ExecutionContext | None = PrivateAttr(default=None)
|
@@ -155,7 +142,7 @@ class Command(BaseModel):
|
|
155
142
|
def parse_args(
|
156
143
|
self, raw_args: list[str] | str, from_validate: bool = False
|
157
144
|
) -> tuple[tuple, dict]:
|
158
|
-
if self.custom_parser:
|
145
|
+
if callable(self.custom_parser):
|
159
146
|
if isinstance(raw_args, str):
|
160
147
|
try:
|
161
148
|
raw_args = shlex.split(raw_args)
|
@@ -192,28 +179,15 @@ class Command(BaseModel):
|
|
192
179
|
def get_argument_definitions(self) -> list[dict[str, Any]]:
|
193
180
|
if self.arguments:
|
194
181
|
return self.arguments
|
195
|
-
elif self.argument_config:
|
182
|
+
elif callable(self.argument_config):
|
196
183
|
self.argument_config(self.arg_parser)
|
197
184
|
elif self.auto_args:
|
198
|
-
if isinstance(self.action,
|
199
|
-
|
200
|
-
|
201
|
-
if
|
202
|
-
|
203
|
-
|
204
|
-
return infer_args_from_func(action.action, self.arg_metadata)
|
205
|
-
elif callable(action):
|
206
|
-
return infer_args_from_func(action, self.arg_metadata)
|
207
|
-
elif isinstance(self.action, ActionGroup):
|
208
|
-
arg_defs = same_argument_definitions(
|
209
|
-
self.action.actions, self.arg_metadata
|
210
|
-
)
|
211
|
-
if arg_defs:
|
212
|
-
return arg_defs
|
213
|
-
logger.debug(
|
214
|
-
"[Command:%s] auto_args disabled: mismatched ActionGroup arguments",
|
215
|
-
self.key,
|
216
|
-
)
|
185
|
+
if isinstance(self.action, BaseAction):
|
186
|
+
infer_target, maybe_metadata = self.action.get_infer_target()
|
187
|
+
# merge metadata with the action's metadata if not already in self.arg_metadata
|
188
|
+
if maybe_metadata:
|
189
|
+
self.arg_metadata = {**maybe_metadata, **self.arg_metadata}
|
190
|
+
return infer_args_from_func(infer_target, self.arg_metadata)
|
217
191
|
elif callable(self.action):
|
218
192
|
return infer_args_from_func(self.action, self.arg_metadata)
|
219
193
|
return []
|
@@ -241,30 +215,9 @@ class Command(BaseModel):
|
|
241
215
|
if self.logging_hooks and isinstance(self.action, BaseAction):
|
242
216
|
register_debug_hooks(self.action.hooks)
|
243
217
|
|
244
|
-
if self.requires_input is None and self.detect_requires_input:
|
245
|
-
self.requires_input = True
|
246
|
-
self.hidden = True
|
247
|
-
elif self.requires_input is None:
|
248
|
-
self.requires_input = False
|
249
|
-
|
250
218
|
for arg_def in self.get_argument_definitions():
|
251
219
|
self.arg_parser.add_argument(*arg_def.pop("flags"), **arg_def)
|
252
220
|
|
253
|
-
@cached_property
|
254
|
-
def detect_requires_input(self) -> bool:
|
255
|
-
"""Detect if the action requires input based on its type."""
|
256
|
-
if isinstance(self.action, BaseIOAction):
|
257
|
-
return True
|
258
|
-
elif isinstance(self.action, ChainedAction):
|
259
|
-
return (
|
260
|
-
isinstance(self.action.actions[0], BaseIOAction)
|
261
|
-
if self.action.actions
|
262
|
-
else False
|
263
|
-
)
|
264
|
-
elif isinstance(self.action, ActionGroup):
|
265
|
-
return any(isinstance(action, BaseIOAction) for action in self.action.actions)
|
266
|
-
return False
|
267
|
-
|
268
221
|
def _inject_options_manager(self) -> None:
|
269
222
|
"""Inject the options manager into the action if applicable."""
|
270
223
|
if isinstance(self.action, BaseAction):
|
@@ -357,7 +310,7 @@ class Command(BaseModel):
|
|
357
310
|
|
358
311
|
def show_help(self) -> bool:
|
359
312
|
"""Display the help message for the command."""
|
360
|
-
if self.custom_help:
|
313
|
+
if callable(self.custom_help):
|
361
314
|
output = self.custom_help()
|
362
315
|
if output:
|
363
316
|
console.print(output)
|
falyx/config.py
CHANGED
falyx/falyx.py
CHANGED
@@ -61,9 +61,9 @@ from falyx.options_manager import OptionsManager
|
|
61
61
|
from falyx.parsers import CommandArgumentParser, get_arg_parsers
|
62
62
|
from falyx.protocols import ArgParserProtocol
|
63
63
|
from falyx.retry import RetryPolicy
|
64
|
-
from falyx.signals import BackSignal, CancelSignal,
|
64
|
+
from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
|
65
65
|
from falyx.themes import OneColors, get_nord_theme
|
66
|
-
from falyx.utils import CaseInsensitiveDict, _noop, chunks
|
66
|
+
from falyx.utils import CaseInsensitiveDict, _noop, chunks
|
67
67
|
from falyx.version import __version__
|
68
68
|
|
69
69
|
|
@@ -90,7 +90,7 @@ class CommandValidator(Validator):
|
|
90
90
|
if not choice:
|
91
91
|
raise ValidationError(
|
92
92
|
message=self.error_message,
|
93
|
-
cursor_position=
|
93
|
+
cursor_position=len(text),
|
94
94
|
)
|
95
95
|
|
96
96
|
|
@@ -111,6 +111,8 @@ class Falyx:
|
|
111
111
|
- Submenu nesting and action chaining
|
112
112
|
- History tracking, help generation, and run key execution modes
|
113
113
|
- Seamless CLI argument parsing and integration via argparse
|
114
|
+
- Declarative option management with OptionsManager
|
115
|
+
- Command level argument parsing and validation
|
114
116
|
- Extensible with user-defined hooks, bottom bars, and custom layouts
|
115
117
|
|
116
118
|
Args:
|
@@ -126,7 +128,7 @@ class Falyx:
|
|
126
128
|
never_prompt (bool): Seed default for `OptionsManager["never_prompt"]`
|
127
129
|
force_confirm (bool): Seed default for `OptionsManager["force_confirm"]`
|
128
130
|
cli_args (Namespace | None): Parsed CLI arguments, usually from argparse.
|
129
|
-
options (OptionsManager | None): Declarative option mappings.
|
131
|
+
options (OptionsManager | None): Declarative option mappings for global state.
|
130
132
|
custom_table (Callable[[Falyx], Table] | Table | None): Custom menu table
|
131
133
|
generator.
|
132
134
|
|
@@ -158,8 +160,9 @@ class Falyx:
|
|
158
160
|
force_confirm: bool = False,
|
159
161
|
cli_args: Namespace | None = None,
|
160
162
|
options: OptionsManager | None = None,
|
161
|
-
render_menu: Callable[[
|
162
|
-
custom_table: Callable[[
|
163
|
+
render_menu: Callable[[Falyx], None] | None = None,
|
164
|
+
custom_table: Callable[[Falyx], Table] | Table | None = None,
|
165
|
+
hide_menu_table: bool = False,
|
163
166
|
) -> None:
|
164
167
|
"""Initializes the Falyx object."""
|
165
168
|
self.title: str | Markdown = title
|
@@ -183,8 +186,9 @@ class Falyx:
|
|
183
186
|
self._never_prompt: bool = never_prompt
|
184
187
|
self._force_confirm: bool = force_confirm
|
185
188
|
self.cli_args: Namespace | None = cli_args
|
186
|
-
self.render_menu: Callable[[
|
187
|
-
self.custom_table: Callable[[
|
189
|
+
self.render_menu: Callable[[Falyx], None] | None = render_menu
|
190
|
+
self.custom_table: Callable[[Falyx], Table] | Table | None = custom_table
|
191
|
+
self.hide_menu_table: bool = hide_menu_table
|
188
192
|
self.validate_options(cli_args, options)
|
189
193
|
self._prompt_session: PromptSession | None = None
|
190
194
|
self.mode = FalyxMode.MENU
|
@@ -287,8 +291,6 @@ class Falyx:
|
|
287
291
|
|
288
292
|
for command in self.commands.values():
|
289
293
|
help_text = command.help_text or command.description
|
290
|
-
if command.requires_input:
|
291
|
-
help_text += " [dim](requires input)[/dim]"
|
292
294
|
table.add_row(
|
293
295
|
f"[{command.style}]{command.key}[/]",
|
294
296
|
", ".join(command.aliases) if command.aliases else "",
|
@@ -445,7 +447,6 @@ class Falyx:
|
|
445
447
|
bottom_toolbar=self._get_bottom_bar_render(),
|
446
448
|
key_bindings=self.key_bindings,
|
447
449
|
validate_while_typing=False,
|
448
|
-
interrupt_exception=FlowSignal,
|
449
450
|
)
|
450
451
|
return self._prompt_session
|
451
452
|
|
@@ -526,7 +527,7 @@ class Falyx:
|
|
526
527
|
key: str = "X",
|
527
528
|
description: str = "Exit",
|
528
529
|
aliases: list[str] | None = None,
|
529
|
-
action: Callable[
|
530
|
+
action: Callable[..., Any] | None = None,
|
530
531
|
style: str = OneColors.DARK_RED,
|
531
532
|
confirm: bool = False,
|
532
533
|
confirm_message: str = "Are you sure?",
|
@@ -580,7 +581,7 @@ class Falyx:
|
|
580
581
|
self,
|
581
582
|
key: str,
|
582
583
|
description: str,
|
583
|
-
action: BaseAction | Callable[
|
584
|
+
action: BaseAction | Callable[..., Any],
|
584
585
|
*,
|
585
586
|
args: tuple = (),
|
586
587
|
kwargs: dict[str, Any] | None = None,
|
@@ -608,13 +609,12 @@ class Falyx:
|
|
608
609
|
retry: bool = False,
|
609
610
|
retry_all: bool = False,
|
610
611
|
retry_policy: RetryPolicy | None = None,
|
611
|
-
requires_input: bool | None = None,
|
612
612
|
arg_parser: CommandArgumentParser | None = None,
|
613
613
|
arguments: list[dict[str, Any]] | None = None,
|
614
614
|
argument_config: Callable[[CommandArgumentParser], None] | None = None,
|
615
615
|
custom_parser: ArgParserProtocol | None = None,
|
616
616
|
custom_help: Callable[[], str | None] | None = None,
|
617
|
-
auto_args: bool =
|
617
|
+
auto_args: bool = True,
|
618
618
|
arg_metadata: dict[str, str | dict[str, Any]] | None = None,
|
619
619
|
) -> Command:
|
620
620
|
"""Adds an command to the menu, preventing duplicates."""
|
@@ -660,7 +660,6 @@ class Falyx:
|
|
660
660
|
retry=retry,
|
661
661
|
retry_all=retry_all,
|
662
662
|
retry_policy=retry_policy or RetryPolicy(),
|
663
|
-
requires_input=requires_input,
|
664
663
|
options_manager=self.options,
|
665
664
|
arg_parser=arg_parser,
|
666
665
|
arguments=arguments or [],
|
@@ -768,26 +767,27 @@ class Falyx:
|
|
768
767
|
|
769
768
|
choice = choice.upper()
|
770
769
|
name_map = self._name_map
|
771
|
-
if choice
|
770
|
+
if name_map.get(choice):
|
772
771
|
if not from_validate:
|
773
772
|
logger.info("Command '%s' selected.", choice)
|
774
|
-
if
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
else:
|
784
|
-
name_map[choice].show_help()
|
785
|
-
raise ValidationError(
|
786
|
-
message=str(error), cursor_position=len(raw_choices)
|
773
|
+
if is_preview:
|
774
|
+
return True, name_map[choice], args, kwargs
|
775
|
+
try:
|
776
|
+
args, kwargs = name_map[choice].parse_args(input_args, from_validate)
|
777
|
+
except CommandArgumentError as error:
|
778
|
+
if not from_validate:
|
779
|
+
if not name_map[choice].show_help():
|
780
|
+
self.console.print(
|
781
|
+
f"[{OneColors.DARK_RED}]❌ Invalid arguments for '{choice}': {error}"
|
787
782
|
)
|
788
|
-
|
789
|
-
|
790
|
-
|
783
|
+
else:
|
784
|
+
name_map[choice].show_help()
|
785
|
+
raise ValidationError(
|
786
|
+
message=str(error), cursor_position=len(raw_choices)
|
787
|
+
)
|
788
|
+
return is_preview, None, args, kwargs
|
789
|
+
except HelpSignal:
|
790
|
+
return True, None, args, kwargs
|
791
791
|
return is_preview, name_map[choice], args, kwargs
|
792
792
|
|
793
793
|
prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)]
|
@@ -844,15 +844,6 @@ class Falyx:
|
|
844
844
|
await selected_command.preview()
|
845
845
|
return True
|
846
846
|
|
847
|
-
if selected_command.requires_input:
|
848
|
-
program = get_program_invocation()
|
849
|
-
self.console.print(
|
850
|
-
f"[{OneColors.LIGHT_YELLOW}]⚠️ Command '{selected_command.key}' requires"
|
851
|
-
f" input and must be run via [{OneColors.MAGENTA}]'{program} run"
|
852
|
-
f"'[{OneColors.LIGHT_YELLOW}] with proper piping or arguments.[/]"
|
853
|
-
)
|
854
|
-
return True
|
855
|
-
|
856
847
|
self.last_run_command = selected_command
|
857
848
|
|
858
849
|
if selected_command == self.exit_command:
|
@@ -984,10 +975,11 @@ class Falyx:
|
|
984
975
|
self.print_message(self.welcome_message)
|
985
976
|
try:
|
986
977
|
while True:
|
987
|
-
if
|
988
|
-
self.render_menu
|
989
|
-
|
990
|
-
|
978
|
+
if not self.hide_menu_table:
|
979
|
+
if callable(self.render_menu):
|
980
|
+
self.render_menu(self)
|
981
|
+
else:
|
982
|
+
self.console.print(self.table, justify="center")
|
991
983
|
try:
|
992
984
|
task = asyncio.create_task(self.process_command())
|
993
985
|
should_continue = await task
|
falyx/hook_manager.py
CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import inspect
|
6
6
|
from enum import Enum
|
7
|
-
from typing import Awaitable, Callable,
|
7
|
+
from typing import Awaitable, Callable, Union
|
8
8
|
|
9
9
|
from falyx.context import ExecutionContext
|
10
10
|
from falyx.logger import logger
|
@@ -24,7 +24,7 @@ class HookType(Enum):
|
|
24
24
|
ON_TEARDOWN = "on_teardown"
|
25
25
|
|
26
26
|
@classmethod
|
27
|
-
def choices(cls) ->
|
27
|
+
def choices(cls) -> list[HookType]:
|
28
28
|
"""Return a list of all hook type choices."""
|
29
29
|
return list(cls)
|
30
30
|
|
@@ -37,16 +37,17 @@ class HookManager:
|
|
37
37
|
"""HookManager"""
|
38
38
|
|
39
39
|
def __init__(self) -> None:
|
40
|
-
self._hooks:
|
40
|
+
self._hooks: dict[HookType, list[Hook]] = {
|
41
41
|
hook_type: [] for hook_type in HookType
|
42
42
|
}
|
43
43
|
|
44
|
-
def register(self, hook_type: HookType, hook: Hook):
|
45
|
-
if
|
46
|
-
|
44
|
+
def register(self, hook_type: HookType | str, hook: Hook):
|
45
|
+
"""Raises ValueError if the hook type is not supported."""
|
46
|
+
if not isinstance(hook_type, HookType):
|
47
|
+
hook_type = HookType(hook_type)
|
47
48
|
self._hooks[hook_type].append(hook)
|
48
49
|
|
49
|
-
def clear(self, hook_type:
|
50
|
+
def clear(self, hook_type: HookType | None = None):
|
50
51
|
if hook_type:
|
51
52
|
self._hooks[hook_type] = []
|
52
53
|
else:
|
falyx/menu.py
CHANGED
@@ -33,7 +33,7 @@ class MenuOptionMap(CaseInsensitiveDict):
|
|
33
33
|
and special signal entries like Quit and Back.
|
34
34
|
"""
|
35
35
|
|
36
|
-
RESERVED_KEYS = {"
|
36
|
+
RESERVED_KEYS = {"B", "X"}
|
37
37
|
|
38
38
|
def __init__(
|
39
39
|
self,
|
@@ -49,14 +49,14 @@ class MenuOptionMap(CaseInsensitiveDict):
|
|
49
49
|
def _inject_reserved_defaults(self):
|
50
50
|
from falyx.action import SignalAction
|
51
51
|
|
52
|
-
self._add_reserved(
|
53
|
-
"Q",
|
54
|
-
MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
|
55
|
-
)
|
56
52
|
self._add_reserved(
|
57
53
|
"B",
|
58
54
|
MenuOption("Back", SignalAction("Back", BackSignal()), OneColors.DARK_YELLOW),
|
59
55
|
)
|
56
|
+
self._add_reserved(
|
57
|
+
"X",
|
58
|
+
MenuOption("Exit", SignalAction("Quit", QuitSignal()), OneColors.DARK_RED),
|
59
|
+
)
|
60
60
|
|
61
61
|
def _add_reserved(self, key: str, option: MenuOption) -> None:
|
62
62
|
"""Add a reserved key, bypassing validation."""
|
@@ -78,8 +78,20 @@ class MenuOptionMap(CaseInsensitiveDict):
|
|
78
78
|
raise ValueError(f"Cannot delete reserved option '{key}'.")
|
79
79
|
super().__delitem__(key)
|
80
80
|
|
81
|
+
def update(self, other=None, **kwargs):
|
82
|
+
"""Update the selection options with another dictionary."""
|
83
|
+
if other:
|
84
|
+
for key, option in other.items():
|
85
|
+
if not isinstance(option, MenuOption):
|
86
|
+
raise TypeError(f"Value for key '{key}' must be a SelectionOption.")
|
87
|
+
self[key] = option
|
88
|
+
for key, option in kwargs.items():
|
89
|
+
if not isinstance(option, MenuOption):
|
90
|
+
raise TypeError(f"Value for key '{key}' must be a SelectionOption.")
|
91
|
+
self[key] = option
|
92
|
+
|
81
93
|
def items(self, include_reserved: bool = True):
|
82
|
-
for
|
83
|
-
if not include_reserved and
|
94
|
+
for key, option in super().items():
|
95
|
+
if not include_reserved and key in self.RESERVED_KEYS:
|
84
96
|
continue
|
85
|
-
yield
|
97
|
+
yield key, option
|
falyx/parsers/__init__.py
CHANGED
@@ -7,8 +7,6 @@ Licensed under the MIT License. See LICENSE file for details.
|
|
7
7
|
|
8
8
|
from .argparse import Argument, ArgumentAction, CommandArgumentParser
|
9
9
|
from .parsers import FalyxParsers, get_arg_parsers
|
10
|
-
from .signature import infer_args_from_func
|
11
|
-
from .utils import same_argument_definitions
|
12
10
|
|
13
11
|
__all__ = [
|
14
12
|
"Argument",
|
@@ -16,6 +14,4 @@ __all__ = [
|
|
16
14
|
"CommandArgumentParser",
|
17
15
|
"get_arg_parsers",
|
18
16
|
"FalyxParsers",
|
19
|
-
"infer_args_from_func",
|
20
|
-
"same_argument_definitions",
|
21
17
|
]
|
falyx/parsers/argparse.py
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
from __future__ import annotations
|
3
|
+
|
2
4
|
from copy import deepcopy
|
3
5
|
from dataclasses import dataclass
|
4
6
|
from enum import Enum
|
@@ -23,6 +25,15 @@ class ArgumentAction(Enum):
|
|
23
25
|
COUNT = "count"
|
24
26
|
HELP = "help"
|
25
27
|
|
28
|
+
@classmethod
|
29
|
+
def choices(cls) -> list[ArgumentAction]:
|
30
|
+
"""Return a list of all argument actions."""
|
31
|
+
return list(cls)
|
32
|
+
|
33
|
+
def __str__(self) -> str:
|
34
|
+
"""Return the string representation of the argument action."""
|
35
|
+
return self.value
|
36
|
+
|
26
37
|
|
27
38
|
@dataclass
|
28
39
|
class Argument:
|
falyx/parsers/signature.py
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
import inspect
|
2
2
|
from typing import Any, Callable
|
3
3
|
|
4
|
-
from falyx import logger
|
4
|
+
from falyx.logger import logger
|
5
5
|
|
6
6
|
|
7
7
|
def infer_args_from_func(
|
8
|
-
func: Callable[[Any], Any],
|
8
|
+
func: Callable[[Any], Any] | None,
|
9
9
|
arg_metadata: dict[str, str | dict[str, Any]] | None = None,
|
10
10
|
) -> list[dict[str, Any]]:
|
11
11
|
"""
|
12
12
|
Infer argument definitions from a callable's signature.
|
13
13
|
Returns a list of kwargs suitable for CommandArgumentParser.add_argument.
|
14
14
|
"""
|
15
|
+
if not callable(func):
|
16
|
+
logger.debug("Provided argument is not callable: %s", func)
|
17
|
+
return []
|
15
18
|
arg_metadata = arg_metadata or {}
|
16
19
|
signature = inspect.signature(func)
|
17
20
|
arg_defs = []
|
falyx/parsers/utils.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
from typing import Any
|
2
2
|
|
3
3
|
from falyx import logger
|
4
|
-
from falyx.action.action import Action, ChainedAction, ProcessAction
|
5
4
|
from falyx.parsers.signature import infer_args_from_func
|
6
5
|
|
7
6
|
|
@@ -9,17 +8,13 @@ def same_argument_definitions(
|
|
9
8
|
actions: list[Any],
|
10
9
|
arg_metadata: dict[str, str | dict[str, Any]] | None = None,
|
11
10
|
) -> list[dict[str, Any]] | None:
|
11
|
+
from falyx.action.action import BaseAction
|
12
|
+
|
12
13
|
arg_sets = []
|
13
14
|
for action in actions:
|
14
|
-
if isinstance(action,
|
15
|
-
|
16
|
-
|
17
|
-
if action.actions:
|
18
|
-
action = action.actions[0]
|
19
|
-
if isinstance(action, Action):
|
20
|
-
arg_defs = infer_args_from_func(action.action, arg_metadata)
|
21
|
-
elif callable(action):
|
22
|
-
arg_defs = infer_args_from_func(action, arg_metadata)
|
15
|
+
if isinstance(action, BaseAction):
|
16
|
+
infer_target, _ = action.get_infer_target()
|
17
|
+
arg_defs = infer_args_from_func(infer_target, arg_metadata)
|
23
18
|
elif callable(action):
|
24
19
|
arg_defs = infer_args_from_func(action, arg_metadata)
|
25
20
|
else:
|
falyx/selection.py
CHANGED
@@ -10,7 +10,7 @@ from rich.markup import escape
|
|
10
10
|
from rich.table import Table
|
11
11
|
|
12
12
|
from falyx.themes import OneColors
|
13
|
-
from falyx.utils import chunks
|
13
|
+
from falyx.utils import CaseInsensitiveDict, chunks
|
14
14
|
from falyx.validators import int_range_validator, key_validator
|
15
15
|
|
16
16
|
|
@@ -32,6 +32,62 @@ class SelectionOption:
|
|
32
32
|
return f"[{OneColors.WHITE}]{key}[/] [{self.style}]{self.description}[/]"
|
33
33
|
|
34
34
|
|
35
|
+
class SelectionOptionMap(CaseInsensitiveDict):
|
36
|
+
"""
|
37
|
+
Manages selection options including validation and reserved key protection.
|
38
|
+
"""
|
39
|
+
|
40
|
+
RESERVED_KEYS: set[str] = set()
|
41
|
+
|
42
|
+
def __init__(
|
43
|
+
self,
|
44
|
+
options: dict[str, SelectionOption] | None = None,
|
45
|
+
allow_reserved: bool = False,
|
46
|
+
):
|
47
|
+
super().__init__()
|
48
|
+
self.allow_reserved = allow_reserved
|
49
|
+
if options:
|
50
|
+
self.update(options)
|
51
|
+
|
52
|
+
def _add_reserved(self, key: str, option: SelectionOption) -> None:
|
53
|
+
"""Add a reserved key, bypassing validation."""
|
54
|
+
norm_key = key.upper()
|
55
|
+
super().__setitem__(norm_key, option)
|
56
|
+
|
57
|
+
def __setitem__(self, key: str, option: SelectionOption) -> None:
|
58
|
+
if not isinstance(option, SelectionOption):
|
59
|
+
raise TypeError(f"Value for key '{key}' must be a SelectionOption.")
|
60
|
+
norm_key = key.upper()
|
61
|
+
if norm_key in self.RESERVED_KEYS and not self.allow_reserved:
|
62
|
+
raise ValueError(
|
63
|
+
f"Key '{key}' is reserved and cannot be used in SelectionOptionMap."
|
64
|
+
)
|
65
|
+
super().__setitem__(norm_key, option)
|
66
|
+
|
67
|
+
def __delitem__(self, key: str) -> None:
|
68
|
+
if key.upper() in self.RESERVED_KEYS and not self.allow_reserved:
|
69
|
+
raise ValueError(f"Cannot delete reserved option '{key}'.")
|
70
|
+
super().__delitem__(key)
|
71
|
+
|
72
|
+
def update(self, other=None, **kwargs):
|
73
|
+
"""Update the selection options with another dictionary."""
|
74
|
+
if other:
|
75
|
+
for key, option in other.items():
|
76
|
+
if not isinstance(option, SelectionOption):
|
77
|
+
raise TypeError(f"Value for key '{key}' must be a SelectionOption.")
|
78
|
+
self[key] = option
|
79
|
+
for key, option in kwargs.items():
|
80
|
+
if not isinstance(option, SelectionOption):
|
81
|
+
raise TypeError(f"Value for key '{key}' must be a SelectionOption.")
|
82
|
+
self[key] = option
|
83
|
+
|
84
|
+
def items(self, include_reserved: bool = True):
|
85
|
+
for k, v in super().items():
|
86
|
+
if not include_reserved and k in self.RESERVED_KEYS:
|
87
|
+
continue
|
88
|
+
yield k, v
|
89
|
+
|
90
|
+
|
35
91
|
def render_table_base(
|
36
92
|
title: str,
|
37
93
|
*,
|
falyx/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.31"
|