falyx 0.1.54__tar.gz → 0.1.56__tar.gz

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.
Files changed (69) hide show
  1. {falyx-0.1.54 → falyx-0.1.56}/PKG-INFO +1 -1
  2. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/select_file_action.py +5 -0
  3. {falyx-0.1.54 → falyx-0.1.56}/falyx/command.py +7 -1
  4. falyx-0.1.56/falyx/completer.py +47 -0
  5. {falyx-0.1.54 → falyx-0.1.56}/falyx/falyx.py +3 -14
  6. falyx-0.1.56/falyx/falyx_completer.py +128 -0
  7. falyx-0.1.56/falyx/parser/.pytyped +0 -0
  8. {falyx-0.1.54 → falyx-0.1.56}/falyx/parser/command_argument_parser.py +73 -21
  9. falyx-0.1.56/falyx/version.py +1 -0
  10. {falyx-0.1.54 → falyx-0.1.56}/pyproject.toml +1 -1
  11. falyx-0.1.54/falyx/version.py +0 -1
  12. {falyx-0.1.54 → falyx-0.1.56}/LICENSE +0 -0
  13. {falyx-0.1.54 → falyx-0.1.56}/README.md +0 -0
  14. {falyx-0.1.54 → falyx-0.1.56}/falyx/.pytyped +0 -0
  15. {falyx-0.1.54 → falyx-0.1.56}/falyx/__init__.py +0 -0
  16. {falyx-0.1.54 → falyx-0.1.56}/falyx/__main__.py +0 -0
  17. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/.pytyped +0 -0
  18. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/__init__.py +0 -0
  19. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/action.py +0 -0
  20. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/action_factory.py +0 -0
  21. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/action_group.py +0 -0
  22. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/action_mixins.py +0 -0
  23. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/action_types.py +0 -0
  24. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/base_action.py +0 -0
  25. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/chained_action.py +0 -0
  26. /falyx-0.1.54/falyx/parser/.pytyped → /falyx-0.1.56/falyx/action/confirm_action.py +0 -0
  27. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/fallback_action.py +0 -0
  28. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/http_action.py +0 -0
  29. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/io_action.py +0 -0
  30. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/literal_input_action.py +0 -0
  31. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/load_file_action.py +0 -0
  32. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/menu_action.py +0 -0
  33. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/process_action.py +0 -0
  34. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/process_pool_action.py +0 -0
  35. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/prompt_menu_action.py +0 -0
  36. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/save_file_action.py +0 -0
  37. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/selection_action.py +0 -0
  38. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/shell_action.py +0 -0
  39. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/signal_action.py +0 -0
  40. {falyx-0.1.54 → falyx-0.1.56}/falyx/action/user_input_action.py +0 -0
  41. {falyx-0.1.54 → falyx-0.1.56}/falyx/bottom_bar.py +0 -0
  42. {falyx-0.1.54 → falyx-0.1.56}/falyx/config.py +0 -0
  43. {falyx-0.1.54 → falyx-0.1.56}/falyx/context.py +0 -0
  44. {falyx-0.1.54 → falyx-0.1.56}/falyx/debug.py +0 -0
  45. {falyx-0.1.54 → falyx-0.1.56}/falyx/exceptions.py +0 -0
  46. {falyx-0.1.54 → falyx-0.1.56}/falyx/execution_registry.py +0 -0
  47. {falyx-0.1.54 → falyx-0.1.56}/falyx/hook_manager.py +0 -0
  48. {falyx-0.1.54 → falyx-0.1.56}/falyx/hooks.py +0 -0
  49. {falyx-0.1.54 → falyx-0.1.56}/falyx/init.py +0 -0
  50. {falyx-0.1.54 → falyx-0.1.56}/falyx/logger.py +0 -0
  51. {falyx-0.1.54 → falyx-0.1.56}/falyx/menu.py +0 -0
  52. {falyx-0.1.54 → falyx-0.1.56}/falyx/options_manager.py +0 -0
  53. {falyx-0.1.54 → falyx-0.1.56}/falyx/parser/__init__.py +0 -0
  54. {falyx-0.1.54 → falyx-0.1.56}/falyx/parser/argument.py +0 -0
  55. {falyx-0.1.54 → falyx-0.1.56}/falyx/parser/argument_action.py +0 -0
  56. {falyx-0.1.54 → falyx-0.1.56}/falyx/parser/parsers.py +0 -0
  57. {falyx-0.1.54 → falyx-0.1.56}/falyx/parser/signature.py +0 -0
  58. {falyx-0.1.54 → falyx-0.1.56}/falyx/parser/utils.py +0 -0
  59. {falyx-0.1.54 → falyx-0.1.56}/falyx/prompt_utils.py +0 -0
  60. {falyx-0.1.54 → falyx-0.1.56}/falyx/protocols.py +0 -0
  61. {falyx-0.1.54 → falyx-0.1.56}/falyx/retry.py +0 -0
  62. {falyx-0.1.54 → falyx-0.1.56}/falyx/retry_utils.py +0 -0
  63. {falyx-0.1.54 → falyx-0.1.56}/falyx/selection.py +0 -0
  64. {falyx-0.1.54 → falyx-0.1.56}/falyx/signals.py +0 -0
  65. {falyx-0.1.54 → falyx-0.1.56}/falyx/tagged_table.py +0 -0
  66. {falyx-0.1.54 → falyx-0.1.56}/falyx/themes/__init__.py +0 -0
  67. {falyx-0.1.54 → falyx-0.1.56}/falyx/themes/colors.py +0 -0
  68. {falyx-0.1.54 → falyx-0.1.56}/falyx/utils.py +0 -0
  69. {falyx-0.1.54 → falyx-0.1.56}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.54
3
+ Version: 0.1.56
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -165,6 +165,11 @@ class SelectFileAction(BaseAction):
165
165
  try:
166
166
  await self.hooks.trigger(HookType.BEFORE, context)
167
167
 
168
+ if not self.directory.exists():
169
+ raise FileNotFoundError(f"Directory {self.directory} does not exist.")
170
+ elif not self.directory.is_dir():
171
+ raise NotADirectoryError(f"{self.directory} is not a directory.")
172
+
168
173
  files = [
169
174
  file
170
175
  for file in self.directory.iterdir()
@@ -91,6 +91,12 @@ class Command(BaseModel):
91
91
  logging_hooks (bool): Whether to attach logging hooks automatically.
92
92
  options_manager (OptionsManager): Manages global command-line options.
93
93
  arg_parser (CommandArgumentParser): Parses command arguments.
94
+ arguments (list[dict[str, Any]]): Argument definitions for the command.
95
+ argument_config (Callable[[CommandArgumentParser], None] | None): Function to configure arguments
96
+ for the command parser.
97
+ arg_metadata (dict[str, str | dict[str, Any]]): Metadata for arguments,
98
+ such as help text or choices.
99
+ simple_help_signature (bool): Whether to use a simplified help signature.
94
100
  custom_parser (ArgParserProtocol | None): Custom argument parser.
95
101
  custom_help (Callable[[], str | None] | None): Custom help message generator.
96
102
  auto_args (bool): Automatically infer arguments from the action.
@@ -227,7 +233,7 @@ class Command(BaseModel):
227
233
  if self.logging_hooks and isinstance(self.action, BaseAction):
228
234
  register_debug_hooks(self.action.hooks)
229
235
 
230
- if self.arg_parser is None:
236
+ if self.arg_parser is None and not self.custom_parser:
231
237
  self.arg_parser = CommandArgumentParser(
232
238
  command_key=self.key,
233
239
  command_description=self.description,
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+ from typing import TYPE_CHECKING, Iterable
5
+
6
+ from prompt_toolkit.completion import Completer, Completion
7
+ from prompt_toolkit.document import Document
8
+
9
+ if TYPE_CHECKING:
10
+ from falyx import Falyx
11
+
12
+
13
+ class FalyxCompleter(Completer):
14
+ """Completer for Falyx commands."""
15
+
16
+ def __init__(self, falyx: "Falyx"):
17
+ self.falyx = falyx
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
+ def _suggest_commands(self, prefix: str) -> Iterable[Completion]:
33
+ prefix = prefix.upper()
34
+ keys = [self.falyx.exit_command.key]
35
+ keys.extend(self.falyx.exit_command.aliases)
36
+ if self.falyx.history_command:
37
+ keys.append(self.falyx.history_command.key)
38
+ keys.extend(self.falyx.history_command.aliases)
39
+ if self.falyx.help_command:
40
+ keys.append(self.falyx.help_command.key)
41
+ keys.extend(self.falyx.help_command.aliases)
42
+ for cmd in self.falyx.commands.values():
43
+ keys.append(cmd.key)
44
+ keys.extend(cmd.aliases)
45
+ for key in keys:
46
+ if key.upper().startswith(prefix):
47
+ yield Completion(key, start_position=-len(prefix))
@@ -32,7 +32,6 @@ from functools import cached_property
32
32
  from typing import Any, Callable
33
33
 
34
34
  from prompt_toolkit import PromptSession
35
- from prompt_toolkit.completion import WordCompleter
36
35
  from prompt_toolkit.formatted_text import AnyFormattedText
37
36
  from prompt_toolkit.key_binding import KeyBindings
38
37
  from prompt_toolkit.patch_stdout import patch_stdout
@@ -46,6 +45,7 @@ from falyx.action.action import Action
46
45
  from falyx.action.base_action import BaseAction
47
46
  from falyx.bottom_bar import BottomBar
48
47
  from falyx.command import Command
48
+ from falyx.completer import FalyxCompleter
49
49
  from falyx.context import ExecutionContext
50
50
  from falyx.debug import log_after, log_before, log_error, log_success
51
51
  from falyx.exceptions import (
@@ -413,20 +413,9 @@ class Falyx:
413
413
  arg_parser=parser,
414
414
  )
415
415
 
416
- def _get_completer(self) -> WordCompleter:
416
+ def _get_completer(self) -> FalyxCompleter:
417
417
  """Completer to provide auto-completion for the menu commands."""
418
- keys = [self.exit_command.key]
419
- keys.extend(self.exit_command.aliases)
420
- if self.history_command:
421
- keys.append(self.history_command.key)
422
- keys.extend(self.history_command.aliases)
423
- if self.help_command:
424
- keys.append(self.help_command.key)
425
- keys.extend(self.help_command.aliases)
426
- for cmd in self.commands.values():
427
- keys.append(cmd.key)
428
- keys.extend(cmd.aliases)
429
- return WordCompleter(keys, ignore_case=True)
418
+ return FalyxCompleter(self)
430
419
 
431
420
  def _get_validator_error_message(self) -> str:
432
421
  """Validator to check if the input is a valid command or toggle key."""
@@ -0,0 +1,128 @@
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
@@ -7,7 +7,6 @@ from typing import Any, Iterable
7
7
 
8
8
  from rich.console import Console
9
9
  from rich.markup import escape
10
- from rich.text import Text
11
10
 
12
11
  from falyx.action.base_action import BaseAction
13
12
  from falyx.exceptions import CommandArgumentError
@@ -466,6 +465,10 @@ class CommandArgumentParser:
466
465
  or isinstance(next_spec.nargs, str)
467
466
  and next_spec.nargs in ("+", "*", "?")
468
467
  ), f"Invalid nargs value: {spec.nargs}"
468
+
469
+ if next_spec.default:
470
+ continue
471
+
469
472
  if next_spec.nargs is None:
470
473
  min_required += 1
471
474
  elif isinstance(next_spec.nargs, int):
@@ -473,9 +476,9 @@ class CommandArgumentParser:
473
476
  elif next_spec.nargs == "+":
474
477
  min_required += 1
475
478
  elif next_spec.nargs == "?":
476
- min_required += 0
479
+ continue
477
480
  elif next_spec.nargs == "*":
478
- min_required += 0
481
+ continue
479
482
 
480
483
  slice_args = args[i:] if is_last else args[i : i + (remaining - min_required)]
481
484
  values, new_i = self._consume_nargs(slice_args, 0, spec)
@@ -484,9 +487,23 @@ class CommandArgumentParser:
484
487
  try:
485
488
  typed = [coerce_value(value, spec.type) for value in values]
486
489
  except Exception as error:
487
- raise CommandArgumentError(
488
- f"Invalid value for '{spec.dest}': {error}"
489
- ) from error
490
+ if len(args[i - new_i :]) == 1 and args[i - new_i].startswith("-"):
491
+ token = args[i - new_i]
492
+ valid_flags = [
493
+ flag for flag in self._flag_map if flag.startswith(token)
494
+ ]
495
+ if valid_flags:
496
+ raise CommandArgumentError(
497
+ f"Unrecognized option '{token}'. Did you mean one of: {', '.join(valid_flags)}?"
498
+ ) from error
499
+ else:
500
+ raise CommandArgumentError(
501
+ f"Unrecognized option '{token}'. Use --help to see available options."
502
+ ) from error
503
+ else:
504
+ raise CommandArgumentError(
505
+ f"Invalid value for '{spec.dest}': {error}"
506
+ ) from error
490
507
  if spec.action == ArgumentAction.ACTION:
491
508
  assert isinstance(
492
509
  spec.resolver, BaseAction
@@ -497,6 +514,8 @@ class CommandArgumentParser:
497
514
  raise CommandArgumentError(
498
515
  f"[{spec.dest}] Action failed: {error}"
499
516
  ) from error
517
+ elif not typed and spec.default:
518
+ result[spec.dest] = spec.default
500
519
  elif spec.action == ArgumentAction.APPEND:
501
520
  assert result.get(spec.dest) is not None, "dest should not be None"
502
521
  if spec.nargs is None:
@@ -515,10 +534,22 @@ class CommandArgumentParser:
515
534
  consumed_positional_indicies.add(j)
516
535
 
517
536
  if i < len(args):
518
- plural = "s" if len(args[i:]) > 1 else ""
519
- raise CommandArgumentError(
520
- f"Unexpected positional argument{plural}: {', '.join(args[i:])}"
521
- )
537
+ if len(args[i:]) == 1 and args[i].startswith("-"):
538
+ token = args[i]
539
+ valid_flags = [flag for flag in self._flag_map if flag.startswith(token)]
540
+ if valid_flags:
541
+ raise CommandArgumentError(
542
+ f"Unrecognized option '{token}'. Did you mean one of: {', '.join(valid_flags)}?"
543
+ )
544
+ else:
545
+ raise CommandArgumentError(
546
+ f"Unrecognized option '{token}'. Use --help to see available options."
547
+ )
548
+ else:
549
+ plural = "s" if len(args[i:]) > 1 else ""
550
+ raise CommandArgumentError(
551
+ f"Unexpected positional argument{plural}: {', '.join(args[i:])}"
552
+ )
522
553
 
523
554
  return i
524
555
 
@@ -624,8 +655,22 @@ class CommandArgumentParser:
624
655
  f"Invalid value for '{spec.dest}': {error}"
625
656
  ) from error
626
657
  if not typed_values and spec.nargs not in ("*", "?"):
658
+ choices = []
659
+ if spec.default:
660
+ choices.append(f"default={spec.default!r}")
661
+ if spec.choices:
662
+ choices.append(f"choices={spec.choices!r}")
663
+ if choices:
664
+ choices_text = ", ".join(choices)
665
+ raise CommandArgumentError(
666
+ f"Argument '{spec.dest}' requires a value. {choices_text}"
667
+ )
668
+ if spec.nargs is None:
669
+ raise CommandArgumentError(
670
+ f"Enter a {spec.type.__name__} value for '{spec.dest}'"
671
+ )
627
672
  raise CommandArgumentError(
628
- f"Expected at least one value for '{spec.dest}'"
673
+ f"Argument '{spec.dest}' requires a value. Expected {spec.nargs} values."
629
674
  )
630
675
  if spec.nargs in (None, 1, "?") and spec.action != ArgumentAction.APPEND:
631
676
  result[spec.dest] = (
@@ -637,7 +682,15 @@ class CommandArgumentParser:
637
682
  i = new_i
638
683
  elif token.startswith("-"):
639
684
  # Handle unrecognized option
640
- raise CommandArgumentError(f"Unrecognized flag: {token}")
685
+ valid_flags = [flag for flag in self._flag_map if flag.startswith(token)]
686
+ if valid_flags:
687
+ raise CommandArgumentError(
688
+ f"Unrecognized option '{token}'. Did you mean one of: {', '.join(valid_flags)}?"
689
+ )
690
+ else:
691
+ raise CommandArgumentError(
692
+ f"Unrecognized option '{token}'. Use --help to see available options."
693
+ )
641
694
  else:
642
695
  # Get the next flagged argument index if it exists
643
696
  next_flagged_index = -1
@@ -645,8 +698,6 @@ class CommandArgumentParser:
645
698
  if arg in self._keyword:
646
699
  next_flagged_index = index
647
700
  break
648
- print(f"next_flagged_index: {next_flagged_index}")
649
- print(f"{self._keyword_list=}")
650
701
  if next_flagged_index == -1:
651
702
  next_flagged_index = len(args)
652
703
  args_consumed = await self._consume_all_positional_args(
@@ -694,7 +745,10 @@ class CommandArgumentParser:
694
745
  if spec.dest == "help":
695
746
  continue
696
747
  if spec.required and not result.get(spec.dest):
697
- raise CommandArgumentError(f"Missing required argument: {spec.dest}")
748
+ help_text = f" help: {spec.help}" if spec.help else ""
749
+ raise CommandArgumentError(
750
+ f"Missing required argument {spec.dest}: {spec.get_choice_text()}{help_text}"
751
+ )
698
752
 
699
753
  if spec.choices and result.get(spec.dest) not in spec.choices:
700
754
  raise CommandArgumentError(
@@ -809,22 +863,20 @@ class CommandArgumentParser:
809
863
  self.console.print("[bold]positional:[/bold]")
810
864
  for arg in self._positional.values():
811
865
  flags = arg.get_positional_text()
812
- arg_line = Text(f" {flags:<30} ")
866
+ arg_line = f" {flags:<30} "
813
867
  help_text = arg.help or ""
814
868
  if help_text and len(flags) > 30:
815
869
  help_text = f"\n{'':<33}{help_text}"
816
- arg_line.append(help_text)
817
- self.console.print(arg_line)
870
+ self.console.print(f"{arg_line}{help_text}")
818
871
  self.console.print("[bold]options:[/bold]")
819
872
  for arg in self._keyword_list:
820
873
  flags = ", ".join(arg.flags)
821
874
  flags_choice = f"{flags} {arg.get_choice_text()}"
822
- arg_line = Text(f" {flags_choice:<30} ")
875
+ arg_line = f" {flags_choice:<30} "
823
876
  help_text = arg.help or ""
824
877
  if help_text and len(flags_choice) > 30:
825
878
  help_text = f"\n{'':<33}{help_text}"
826
- arg_line.append(help_text)
827
- self.console.print(arg_line)
879
+ self.console.print(f"{arg_line}{help_text}")
828
880
 
829
881
  # Epilog
830
882
  if self.help_epilog:
@@ -0,0 +1 @@
1
+ __version__ = "0.1.56"
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "falyx"
3
- version = "0.1.54"
3
+ version = "0.1.56"
4
4
  description = "Reliable and introspectable async CLI action framework."
5
5
  authors = ["Roland Thomas Jr <roland@rtj.dev>"]
6
6
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "0.1.54"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes