falyx 0.1.27__py3-none-any.whl → 0.1.29__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/.pytyped +0 -0
- falyx/action/io_action.py +4 -1
- falyx/command.py +100 -2
- falyx/exceptions.py +4 -0
- falyx/falyx.py +123 -38
- falyx/parsers/.pytyped +0 -0
- falyx/parsers/__init__.py +21 -0
- falyx/parsers/argparse.py +756 -0
- falyx/{parsers.py → parsers/parsers.py} +7 -1
- falyx/parsers/signature.py +71 -0
- falyx/parsers/utils.py +33 -0
- falyx/protocols.py +7 -1
- falyx/signals.py +7 -0
- falyx/version.py +1 -1
- {falyx-0.1.27.dist-info → falyx-0.1.29.dist-info}/METADATA +1 -1
- {falyx-0.1.27.dist-info → falyx-0.1.29.dist-info}/RECORD +19 -14
- falyx/config_schema.py +0 -76
- {falyx-0.1.27.dist-info → falyx-0.1.29.dist-info}/LICENSE +0 -0
- {falyx-0.1.27.dist-info → falyx-0.1.29.dist-info}/WHEEL +0 -0
- {falyx-0.1.27.dist-info → falyx-0.1.29.dist-info}/entry_points.txt +0 -0
falyx/action/.pytyped
ADDED
File without changes
|
falyx/action/io_action.py
CHANGED
@@ -224,7 +224,10 @@ class ShellAction(BaseIOAction):
|
|
224
224
|
# Replace placeholder in template, or use raw input as full command
|
225
225
|
command = self.command_template.format(parsed_input)
|
226
226
|
if self.safe_mode:
|
227
|
-
|
227
|
+
try:
|
228
|
+
args = shlex.split(command)
|
229
|
+
except ValueError as error:
|
230
|
+
raise FalyxError(f"Invalid command template: {error}")
|
228
231
|
result = subprocess.run(args, capture_output=True, text=True, check=True)
|
229
232
|
else:
|
230
233
|
result = subprocess.run(
|
falyx/command.py
CHANGED
@@ -18,6 +18,7 @@ in building robust interactive menus.
|
|
18
18
|
"""
|
19
19
|
from __future__ import annotations
|
20
20
|
|
21
|
+
import shlex
|
21
22
|
from functools import cached_property
|
22
23
|
from typing import Any, Callable
|
23
24
|
|
@@ -26,7 +27,13 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
|
26
27
|
from rich.console import Console
|
27
28
|
from rich.tree import Tree
|
28
29
|
|
29
|
-
from falyx.action.action import
|
30
|
+
from falyx.action.action import (
|
31
|
+
Action,
|
32
|
+
ActionGroup,
|
33
|
+
BaseAction,
|
34
|
+
ChainedAction,
|
35
|
+
ProcessAction,
|
36
|
+
)
|
30
37
|
from falyx.action.io_action import BaseIOAction
|
31
38
|
from falyx.context import ExecutionContext
|
32
39
|
from falyx.debug import register_debug_hooks
|
@@ -34,7 +41,13 @@ from falyx.execution_registry import ExecutionRegistry as er
|
|
34
41
|
from falyx.hook_manager import HookManager, HookType
|
35
42
|
from falyx.logger import logger
|
36
43
|
from falyx.options_manager import OptionsManager
|
44
|
+
from falyx.parsers import (
|
45
|
+
CommandArgumentParser,
|
46
|
+
infer_args_from_func,
|
47
|
+
same_argument_definitions,
|
48
|
+
)
|
37
49
|
from falyx.prompt_utils import confirm_async, should_prompt_user
|
50
|
+
from falyx.protocols import ArgParserProtocol
|
38
51
|
from falyx.retry import RetryPolicy
|
39
52
|
from falyx.retry_utils import enable_retries_recursively
|
40
53
|
from falyx.signals import CancelSignal
|
@@ -87,6 +100,11 @@ class Command(BaseModel):
|
|
87
100
|
tags (list[str]): Organizational tags for the command.
|
88
101
|
logging_hooks (bool): Whether to attach logging hooks automatically.
|
89
102
|
requires_input (bool | None): Indicates if the action needs input.
|
103
|
+
options_manager (OptionsManager): Manages global command-line options.
|
104
|
+
arg_parser (CommandArgumentParser): Parses command arguments.
|
105
|
+
custom_parser (ArgParserProtocol | None): Custom argument parser.
|
106
|
+
custom_help (Callable[[], str | None] | None): Custom help message generator.
|
107
|
+
auto_args (bool): Automatically infer arguments from the action.
|
90
108
|
|
91
109
|
Methods:
|
92
110
|
__call__(): Executes the command, respecting hooks and retries.
|
@@ -98,12 +116,13 @@ class Command(BaseModel):
|
|
98
116
|
|
99
117
|
key: str
|
100
118
|
description: str
|
101
|
-
action: BaseAction | Callable[[], Any]
|
119
|
+
action: BaseAction | Callable[[Any], Any]
|
102
120
|
args: tuple = ()
|
103
121
|
kwargs: dict[str, Any] = Field(default_factory=dict)
|
104
122
|
hidden: bool = False
|
105
123
|
aliases: list[str] = Field(default_factory=list)
|
106
124
|
help_text: str = ""
|
125
|
+
help_epilogue: str = ""
|
107
126
|
style: str = OneColors.WHITE
|
108
127
|
confirm: bool = False
|
109
128
|
confirm_message: str = "Are you sure?"
|
@@ -121,11 +140,46 @@ class Command(BaseModel):
|
|
121
140
|
logging_hooks: bool = False
|
122
141
|
requires_input: bool | None = None
|
123
142
|
options_manager: OptionsManager = Field(default_factory=OptionsManager)
|
143
|
+
arg_parser: CommandArgumentParser = Field(default_factory=CommandArgumentParser)
|
144
|
+
arguments: list[dict[str, Any]] = Field(default_factory=list)
|
145
|
+
argument_config: Callable[[CommandArgumentParser], None] | None = None
|
146
|
+
custom_parser: ArgParserProtocol | None = None
|
147
|
+
custom_help: Callable[[], str | None] | None = None
|
148
|
+
auto_args: bool = False
|
149
|
+
arg_metadata: dict[str, str | dict[str, Any]] = Field(default_factory=dict)
|
124
150
|
|
125
151
|
_context: ExecutionContext | None = PrivateAttr(default=None)
|
126
152
|
|
127
153
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
128
154
|
|
155
|
+
def parse_args(
|
156
|
+
self, raw_args: list[str] | str, from_validate: bool = False
|
157
|
+
) -> tuple[tuple, dict]:
|
158
|
+
if self.custom_parser:
|
159
|
+
if isinstance(raw_args, str):
|
160
|
+
try:
|
161
|
+
raw_args = shlex.split(raw_args)
|
162
|
+
except ValueError:
|
163
|
+
logger.warning(
|
164
|
+
"[Command:%s] Failed to split arguments: %s",
|
165
|
+
self.key,
|
166
|
+
raw_args,
|
167
|
+
)
|
168
|
+
return ((), {})
|
169
|
+
return self.custom_parser(raw_args)
|
170
|
+
|
171
|
+
if isinstance(raw_args, str):
|
172
|
+
try:
|
173
|
+
raw_args = shlex.split(raw_args)
|
174
|
+
except ValueError:
|
175
|
+
logger.warning(
|
176
|
+
"[Command:%s] Failed to split arguments: %s",
|
177
|
+
self.key,
|
178
|
+
raw_args,
|
179
|
+
)
|
180
|
+
return ((), {})
|
181
|
+
return self.arg_parser.parse_args_split(raw_args, from_validate=from_validate)
|
182
|
+
|
129
183
|
@field_validator("action", mode="before")
|
130
184
|
@classmethod
|
131
185
|
def wrap_callable_as_async(cls, action: Any) -> Any:
|
@@ -135,6 +189,35 @@ class Command(BaseModel):
|
|
135
189
|
return ensure_async(action)
|
136
190
|
raise TypeError("Action must be a callable or an instance of BaseAction")
|
137
191
|
|
192
|
+
def get_argument_definitions(self) -> list[dict[str, Any]]:
|
193
|
+
if self.arguments:
|
194
|
+
return self.arguments
|
195
|
+
elif self.argument_config:
|
196
|
+
self.argument_config(self.arg_parser)
|
197
|
+
elif self.auto_args:
|
198
|
+
if isinstance(self.action, (Action, ProcessAction)):
|
199
|
+
return infer_args_from_func(self.action.action, self.arg_metadata)
|
200
|
+
elif isinstance(self.action, ChainedAction):
|
201
|
+
if self.action.actions:
|
202
|
+
action = self.action.actions[0]
|
203
|
+
if isinstance(action, Action):
|
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
|
+
)
|
217
|
+
elif callable(self.action):
|
218
|
+
return infer_args_from_func(self.action, self.arg_metadata)
|
219
|
+
return []
|
220
|
+
|
138
221
|
def model_post_init(self, _: Any) -> None:
|
139
222
|
"""Post-initialization to set up the action and hooks."""
|
140
223
|
if self.retry and isinstance(self.action, Action):
|
@@ -164,6 +247,9 @@ class Command(BaseModel):
|
|
164
247
|
elif self.requires_input is None:
|
165
248
|
self.requires_input = False
|
166
249
|
|
250
|
+
for arg_def in self.get_argument_definitions():
|
251
|
+
self.arg_parser.add_argument(*arg_def.pop("flags"), **arg_def)
|
252
|
+
|
167
253
|
@cached_property
|
168
254
|
def detect_requires_input(self) -> bool:
|
169
255
|
"""Detect if the action requires input based on its type."""
|
@@ -269,6 +355,18 @@ class Command(BaseModel):
|
|
269
355
|
if self._context:
|
270
356
|
self._context.log_summary()
|
271
357
|
|
358
|
+
def show_help(self) -> bool:
|
359
|
+
"""Display the help message for the command."""
|
360
|
+
if self.custom_help:
|
361
|
+
output = self.custom_help()
|
362
|
+
if output:
|
363
|
+
console.print(output)
|
364
|
+
return True
|
365
|
+
if isinstance(self.arg_parser, CommandArgumentParser):
|
366
|
+
self.arg_parser.render_help()
|
367
|
+
return True
|
368
|
+
return False
|
369
|
+
|
272
370
|
async def preview(self) -> None:
|
273
371
|
label = f"[{OneColors.GREEN_b}]Command:[/] '{self.key}' — {self.description}"
|
274
372
|
|
falyx/exceptions.py
CHANGED
falyx/falyx.py
CHANGED
@@ -23,6 +23,7 @@ from __future__ import annotations
|
|
23
23
|
|
24
24
|
import asyncio
|
25
25
|
import logging
|
26
|
+
import shlex
|
26
27
|
import sys
|
27
28
|
from argparse import Namespace
|
28
29
|
from difflib import get_close_matches
|
@@ -34,7 +35,8 @@ from prompt_toolkit import PromptSession
|
|
34
35
|
from prompt_toolkit.completion import WordCompleter
|
35
36
|
from prompt_toolkit.formatted_text import AnyFormattedText
|
36
37
|
from prompt_toolkit.key_binding import KeyBindings
|
37
|
-
from prompt_toolkit.
|
38
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
39
|
+
from prompt_toolkit.validation import ValidationError, Validator
|
38
40
|
from rich import box
|
39
41
|
from rich.console import Console
|
40
42
|
from rich.markdown import Markdown
|
@@ -47,6 +49,7 @@ from falyx.context import ExecutionContext
|
|
47
49
|
from falyx.debug import log_after, log_before, log_error, log_success
|
48
50
|
from falyx.exceptions import (
|
49
51
|
CommandAlreadyExistsError,
|
52
|
+
CommandArgumentError,
|
50
53
|
FalyxError,
|
51
54
|
InvalidActionError,
|
52
55
|
NotAFalyxError,
|
@@ -55,21 +58,42 @@ from falyx.execution_registry import ExecutionRegistry as er
|
|
55
58
|
from falyx.hook_manager import Hook, HookManager, HookType
|
56
59
|
from falyx.logger import logger
|
57
60
|
from falyx.options_manager import OptionsManager
|
58
|
-
from falyx.parsers import get_arg_parsers
|
61
|
+
from falyx.parsers import CommandArgumentParser, get_arg_parsers
|
62
|
+
from falyx.protocols import ArgParserProtocol
|
59
63
|
from falyx.retry import RetryPolicy
|
60
|
-
from falyx.signals import BackSignal, CancelSignal, QuitSignal
|
64
|
+
from falyx.signals import BackSignal, CancelSignal, FlowSignal, HelpSignal, QuitSignal
|
61
65
|
from falyx.themes import OneColors, get_nord_theme
|
62
66
|
from falyx.utils import CaseInsensitiveDict, _noop, chunks, get_program_invocation
|
63
67
|
from falyx.version import __version__
|
64
68
|
|
65
69
|
|
66
|
-
class FalyxMode(
|
70
|
+
class FalyxMode(Enum):
|
67
71
|
MENU = "menu"
|
68
72
|
RUN = "run"
|
69
73
|
PREVIEW = "preview"
|
70
74
|
RUN_ALL = "run-all"
|
71
75
|
|
72
76
|
|
77
|
+
class CommandValidator(Validator):
|
78
|
+
"""Validator to check if the input is a valid command or toggle key."""
|
79
|
+
|
80
|
+
def __init__(self, falyx: Falyx, error_message: str) -> None:
|
81
|
+
super().__init__()
|
82
|
+
self.falyx = falyx
|
83
|
+
self.error_message = error_message
|
84
|
+
|
85
|
+
def validate(self, document) -> None:
|
86
|
+
text = document.text
|
87
|
+
is_preview, choice, _, __ = self.falyx.get_command(text, from_validate=True)
|
88
|
+
if is_preview:
|
89
|
+
return None
|
90
|
+
if not choice:
|
91
|
+
raise ValidationError(
|
92
|
+
message=self.error_message,
|
93
|
+
cursor_position=document.get_end_of_document_position(),
|
94
|
+
)
|
95
|
+
|
96
|
+
|
73
97
|
class Falyx:
|
74
98
|
"""
|
75
99
|
Main menu controller for Falyx CLI applications.
|
@@ -325,7 +349,7 @@ class Falyx:
|
|
325
349
|
keys.extend(cmd.aliases)
|
326
350
|
return WordCompleter(keys, ignore_case=True)
|
327
351
|
|
328
|
-
def
|
352
|
+
def _get_validator_error_message(self) -> str:
|
329
353
|
"""Validator to check if the input is a valid command or toggle key."""
|
330
354
|
keys = {self.exit_command.key.upper()}
|
331
355
|
keys.update({alias.upper() for alias in self.exit_command.aliases})
|
@@ -354,18 +378,7 @@ class Falyx:
|
|
354
378
|
if toggle_keys:
|
355
379
|
message_lines.append(f" Toggles: {toggles_str}")
|
356
380
|
error_message = " ".join(message_lines)
|
357
|
-
|
358
|
-
def validator(text):
|
359
|
-
is_preview, choice = self.get_command(text, from_validate=True)
|
360
|
-
if is_preview and choice is None:
|
361
|
-
return True
|
362
|
-
return bool(choice)
|
363
|
-
|
364
|
-
return Validator.from_callable(
|
365
|
-
validator,
|
366
|
-
error_message=error_message,
|
367
|
-
move_cursor_to_end=True,
|
368
|
-
)
|
381
|
+
return error_message
|
369
382
|
|
370
383
|
def _invalidate_prompt_session_cache(self):
|
371
384
|
"""Forces the prompt session to be recreated on the next access."""
|
@@ -428,9 +441,11 @@ class Falyx:
|
|
428
441
|
multiline=False,
|
429
442
|
completer=self._get_completer(),
|
430
443
|
reserve_space_for_menu=1,
|
431
|
-
validator=self.
|
444
|
+
validator=CommandValidator(self, self._get_validator_error_message()),
|
432
445
|
bottom_toolbar=self._get_bottom_bar_render(),
|
433
446
|
key_bindings=self.key_bindings,
|
447
|
+
validate_while_typing=False,
|
448
|
+
interrupt_exception=FlowSignal,
|
434
449
|
)
|
435
450
|
return self._prompt_session
|
436
451
|
|
@@ -511,7 +526,7 @@ class Falyx:
|
|
511
526
|
key: str = "X",
|
512
527
|
description: str = "Exit",
|
513
528
|
aliases: list[str] | None = None,
|
514
|
-
action: Callable[[], Any] | None = None,
|
529
|
+
action: Callable[[Any], Any] | None = None,
|
515
530
|
style: str = OneColors.DARK_RED,
|
516
531
|
confirm: bool = False,
|
517
532
|
confirm_message: str = "Are you sure?",
|
@@ -565,13 +580,14 @@ class Falyx:
|
|
565
580
|
self,
|
566
581
|
key: str,
|
567
582
|
description: str,
|
568
|
-
action: BaseAction | Callable[[], Any],
|
583
|
+
action: BaseAction | Callable[[Any], Any],
|
569
584
|
*,
|
570
585
|
args: tuple = (),
|
571
586
|
kwargs: dict[str, Any] | None = None,
|
572
587
|
hidden: bool = False,
|
573
588
|
aliases: list[str] | None = None,
|
574
589
|
help_text: str = "",
|
590
|
+
help_epilogue: str = "",
|
575
591
|
style: str = OneColors.WHITE,
|
576
592
|
confirm: bool = False,
|
577
593
|
confirm_message: str = "Are you sure?",
|
@@ -593,9 +609,33 @@ class Falyx:
|
|
593
609
|
retry_all: bool = False,
|
594
610
|
retry_policy: RetryPolicy | None = None,
|
595
611
|
requires_input: bool | None = None,
|
612
|
+
arg_parser: CommandArgumentParser | None = None,
|
613
|
+
arguments: list[dict[str, Any]] | None = None,
|
614
|
+
argument_config: Callable[[CommandArgumentParser], None] | None = None,
|
615
|
+
custom_parser: ArgParserProtocol | None = None,
|
616
|
+
custom_help: Callable[[], str | None] | None = None,
|
617
|
+
auto_args: bool = False,
|
618
|
+
arg_metadata: dict[str, str | dict[str, Any]] | None = None,
|
596
619
|
) -> Command:
|
597
620
|
"""Adds an command to the menu, preventing duplicates."""
|
598
621
|
self._validate_command_key(key)
|
622
|
+
|
623
|
+
if arg_parser:
|
624
|
+
if not isinstance(arg_parser, CommandArgumentParser):
|
625
|
+
raise NotAFalyxError(
|
626
|
+
"arg_parser must be an instance of CommandArgumentParser."
|
627
|
+
)
|
628
|
+
arg_parser = arg_parser
|
629
|
+
else:
|
630
|
+
arg_parser = CommandArgumentParser(
|
631
|
+
command_key=key,
|
632
|
+
command_description=description,
|
633
|
+
command_style=style,
|
634
|
+
help_text=help_text,
|
635
|
+
help_epilogue=help_epilogue,
|
636
|
+
aliases=aliases,
|
637
|
+
)
|
638
|
+
|
599
639
|
command = Command(
|
600
640
|
key=key,
|
601
641
|
description=description,
|
@@ -605,6 +645,7 @@ class Falyx:
|
|
605
645
|
hidden=hidden,
|
606
646
|
aliases=aliases if aliases else [],
|
607
647
|
help_text=help_text,
|
648
|
+
help_epilogue=help_epilogue,
|
608
649
|
style=style,
|
609
650
|
confirm=confirm,
|
610
651
|
confirm_message=confirm_message,
|
@@ -621,6 +662,13 @@ class Falyx:
|
|
621
662
|
retry_policy=retry_policy or RetryPolicy(),
|
622
663
|
requires_input=requires_input,
|
623
664
|
options_manager=self.options,
|
665
|
+
arg_parser=arg_parser,
|
666
|
+
arguments=arguments or [],
|
667
|
+
argument_config=argument_config,
|
668
|
+
custom_parser=custom_parser,
|
669
|
+
custom_help=custom_help,
|
670
|
+
auto_args=auto_args,
|
671
|
+
arg_metadata=arg_metadata or {},
|
624
672
|
)
|
625
673
|
|
626
674
|
if hooks:
|
@@ -694,32 +742,57 @@ class Falyx:
|
|
694
742
|
return False, input_str.strip()
|
695
743
|
|
696
744
|
def get_command(
|
697
|
-
self,
|
698
|
-
) -> tuple[bool, Command | None]:
|
745
|
+
self, raw_choices: str, from_validate=False
|
746
|
+
) -> tuple[bool, Command | None, tuple, dict[str, Any]]:
|
699
747
|
"""
|
700
748
|
Returns the selected command based on user input.
|
701
749
|
Supports keys, aliases, and abbreviations.
|
702
750
|
"""
|
751
|
+
args = ()
|
752
|
+
kwargs: dict[str, Any] = {}
|
753
|
+
try:
|
754
|
+
choice, *input_args = shlex.split(raw_choices)
|
755
|
+
except ValueError:
|
756
|
+
return False, None, args, kwargs
|
703
757
|
is_preview, choice = self.parse_preview_command(choice)
|
704
758
|
if is_preview and not choice and self.help_command:
|
705
759
|
is_preview = False
|
706
760
|
choice = "?"
|
707
761
|
elif is_preview and not choice:
|
762
|
+
# No help command enabled
|
708
763
|
if not from_validate:
|
709
764
|
self.console.print(
|
710
765
|
f"[{OneColors.DARK_RED}]❌ You must enter a command for preview mode."
|
711
766
|
)
|
712
|
-
return is_preview, None
|
767
|
+
return is_preview, None, args, kwargs
|
713
768
|
|
714
769
|
choice = choice.upper()
|
715
770
|
name_map = self._name_map
|
716
|
-
|
717
771
|
if choice in name_map:
|
718
|
-
|
772
|
+
if not from_validate:
|
773
|
+
logger.info("Command '%s' selected.", choice)
|
774
|
+
if input_args and name_map[choice].arg_parser:
|
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}"
|
782
|
+
)
|
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
|
+
return is_preview, name_map[choice], args, kwargs
|
719
792
|
|
720
793
|
prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)]
|
721
794
|
if len(prefix_matches) == 1:
|
722
|
-
return is_preview, prefix_matches[0]
|
795
|
+
return is_preview, prefix_matches[0], args, kwargs
|
723
796
|
|
724
797
|
fuzzy_matches = get_close_matches(choice, list(name_map.keys()), n=3, cutoff=0.7)
|
725
798
|
if fuzzy_matches:
|
@@ -736,7 +809,7 @@ class Falyx:
|
|
736
809
|
self.console.print(
|
737
810
|
f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command '{choice}'[/]"
|
738
811
|
)
|
739
|
-
return is_preview, None
|
812
|
+
return is_preview, None, args, kwargs
|
740
813
|
|
741
814
|
def _create_context(self, selected_command: Command) -> ExecutionContext:
|
742
815
|
"""Creates a context dictionary for the selected command."""
|
@@ -759,8 +832,9 @@ class Falyx:
|
|
759
832
|
|
760
833
|
async def process_command(self) -> bool:
|
761
834
|
"""Processes the action of the selected command."""
|
762
|
-
|
763
|
-
|
835
|
+
with patch_stdout(raw=True):
|
836
|
+
choice = await self.prompt_session.prompt_async()
|
837
|
+
is_preview, selected_command, args, kwargs = self.get_command(choice)
|
764
838
|
if not selected_command:
|
765
839
|
logger.info("Invalid command '%s'.", choice)
|
766
840
|
return True
|
@@ -789,8 +863,7 @@ class Falyx:
|
|
789
863
|
context.start_timer()
|
790
864
|
try:
|
791
865
|
await self.hooks.trigger(HookType.BEFORE, context)
|
792
|
-
|
793
|
-
result = await selected_command()
|
866
|
+
result = await selected_command(*args, **kwargs)
|
794
867
|
context.result = result
|
795
868
|
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
796
869
|
except Exception as error:
|
@@ -803,10 +876,18 @@ class Falyx:
|
|
803
876
|
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
|
804
877
|
return True
|
805
878
|
|
806
|
-
async def run_key(
|
879
|
+
async def run_key(
|
880
|
+
self,
|
881
|
+
command_key: str,
|
882
|
+
return_context: bool = False,
|
883
|
+
args: tuple = (),
|
884
|
+
kwargs: dict[str, Any] | None = None,
|
885
|
+
) -> Any:
|
807
886
|
"""Run a command by key without displaying the menu (non-interactive mode)."""
|
808
887
|
self.debug_hooks()
|
809
|
-
is_preview, selected_command = self.get_command(command_key)
|
888
|
+
is_preview, selected_command, _, __ = self.get_command(command_key)
|
889
|
+
kwargs = kwargs or {}
|
890
|
+
|
810
891
|
self.last_run_command = selected_command
|
811
892
|
|
812
893
|
if not selected_command:
|
@@ -827,7 +908,7 @@ class Falyx:
|
|
827
908
|
context.start_timer()
|
828
909
|
try:
|
829
910
|
await self.hooks.trigger(HookType.BEFORE, context)
|
830
|
-
result = await selected_command()
|
911
|
+
result = await selected_command(*args, **kwargs)
|
831
912
|
context.result = result
|
832
913
|
|
833
914
|
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
@@ -951,12 +1032,12 @@ class Falyx:
|
|
951
1032
|
sys.exit(0)
|
952
1033
|
|
953
1034
|
if self.cli_args.command == "version" or self.cli_args.version:
|
954
|
-
self.console.print(f"[{OneColors.
|
1035
|
+
self.console.print(f"[{OneColors.BLUE_b}]Falyx CLI v{__version__}[/]")
|
955
1036
|
sys.exit(0)
|
956
1037
|
|
957
1038
|
if self.cli_args.command == "preview":
|
958
1039
|
self.mode = FalyxMode.PREVIEW
|
959
|
-
_, command = self.get_command(self.cli_args.name)
|
1040
|
+
_, command, args, kwargs = self.get_command(self.cli_args.name)
|
960
1041
|
if not command:
|
961
1042
|
self.console.print(
|
962
1043
|
f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found."
|
@@ -970,7 +1051,7 @@ class Falyx:
|
|
970
1051
|
|
971
1052
|
if self.cli_args.command == "run":
|
972
1053
|
self.mode = FalyxMode.RUN
|
973
|
-
is_preview, command = self.get_command(self.cli_args.name)
|
1054
|
+
is_preview, command, _, __ = self.get_command(self.cli_args.name)
|
974
1055
|
if is_preview:
|
975
1056
|
if command is None:
|
976
1057
|
sys.exit(1)
|
@@ -981,7 +1062,11 @@ class Falyx:
|
|
981
1062
|
sys.exit(1)
|
982
1063
|
self._set_retry_policy(command)
|
983
1064
|
try:
|
984
|
-
|
1065
|
+
args, kwargs = command.parse_args(self.cli_args.command_args)
|
1066
|
+
except HelpSignal:
|
1067
|
+
sys.exit(0)
|
1068
|
+
try:
|
1069
|
+
await self.run_key(self.cli_args.name, args=args, kwargs=kwargs)
|
985
1070
|
except FalyxError as error:
|
986
1071
|
self.console.print(f"[{OneColors.DARK_RED}]❌ Error: {error}[/]")
|
987
1072
|
sys.exit(1)
|
falyx/parsers/.pytyped
ADDED
File without changes
|
@@ -0,0 +1,21 @@
|
|
1
|
+
"""
|
2
|
+
Falyx CLI Framework
|
3
|
+
|
4
|
+
Copyright (c) 2025 rtj.dev LLC.
|
5
|
+
Licensed under the MIT License. See LICENSE file for details.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .argparse import Argument, ArgumentAction, CommandArgumentParser
|
9
|
+
from .parsers import FalyxParsers, get_arg_parsers
|
10
|
+
from .signature import infer_args_from_func
|
11
|
+
from .utils import same_argument_definitions
|
12
|
+
|
13
|
+
__all__ = [
|
14
|
+
"Argument",
|
15
|
+
"ArgumentAction",
|
16
|
+
"CommandArgumentParser",
|
17
|
+
"get_arg_parsers",
|
18
|
+
"FalyxParsers",
|
19
|
+
"infer_args_from_func",
|
20
|
+
"same_argument_definitions",
|
21
|
+
]
|