falyx 0.1.55__py3-none-any.whl → 0.1.57__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.
@@ -11,7 +11,6 @@ from typing import Any
11
11
  import toml
12
12
  import yaml
13
13
  from prompt_toolkit import PromptSession
14
- from rich.console import Console
15
14
  from rich.tree import Tree
16
15
 
17
16
  from falyx.action.action_types import FileType
@@ -51,7 +50,6 @@ class SelectFileAction(BaseAction):
51
50
  style (str): Style for the selection options.
52
51
  suffix_filter (str | None): Restrict to certain file types.
53
52
  return_type (FileType): What to return (path, content, parsed).
54
- console (Console | None): Console instance for output.
55
53
  prompt_session (PromptSession | None): Prompt session for user input.
56
54
  """
57
55
 
@@ -69,7 +67,6 @@ class SelectFileAction(BaseAction):
69
67
  number_selections: int | str = 1,
70
68
  separator: str = ",",
71
69
  allow_duplicates: bool = False,
72
- console: Console | None = None,
73
70
  prompt_session: PromptSession | None = None,
74
71
  ):
75
72
  super().__init__(name)
@@ -82,10 +79,6 @@ class SelectFileAction(BaseAction):
82
79
  self.number_selections = number_selections
83
80
  self.separator = separator
84
81
  self.allow_duplicates = allow_duplicates
85
- if isinstance(console, Console):
86
- self.console = console
87
- elif console:
88
- raise ValueError("`console` must be an instance of `rich.console.Console`")
89
82
  self.prompt_session = prompt_session or PromptSession()
90
83
  self.return_type = self._coerce_return_type(return_type)
91
84
 
@@ -165,6 +158,11 @@ class SelectFileAction(BaseAction):
165
158
  try:
166
159
  await self.hooks.trigger(HookType.BEFORE, context)
167
160
 
161
+ if not self.directory.exists():
162
+ raise FileNotFoundError(f"Directory {self.directory} does not exist.")
163
+ elif not self.directory.is_dir():
164
+ raise NotADirectoryError(f"{self.directory} is not a directory.")
165
+
168
166
  files = [
169
167
  file
170
168
  for file in self.directory.iterdir()
@@ -190,7 +188,6 @@ class SelectFileAction(BaseAction):
190
188
  keys = await prompt_for_selection(
191
189
  (options | cancel_option).keys(),
192
190
  table,
193
- console=self.console,
194
191
  prompt_session=self.prompt_session,
195
192
  prompt_message=self.prompt_message,
196
193
  number_selections=self.number_selections,
@@ -3,7 +3,6 @@
3
3
  from typing import Any
4
4
 
5
5
  from prompt_toolkit import PromptSession
6
- from rich.console import Console
7
6
  from rich.tree import Tree
8
7
 
9
8
  from falyx.action.action_types import SelectionReturnType
@@ -54,7 +53,6 @@ class SelectionAction(BaseAction):
54
53
  inject_last_result: bool = False,
55
54
  inject_into: str = "last_result",
56
55
  return_type: SelectionReturnType | str = "value",
57
- console: Console | None = None,
58
56
  prompt_session: PromptSession | None = None,
59
57
  never_prompt: bool = False,
60
58
  show_table: bool = True,
@@ -70,10 +68,6 @@ class SelectionAction(BaseAction):
70
68
  self.return_type: SelectionReturnType = self._coerce_return_type(return_type)
71
69
  self.title = title
72
70
  self.columns = columns
73
- if isinstance(console, Console):
74
- self.console = console
75
- elif console:
76
- raise ValueError("`console` must be an instance of `rich.console.Console`")
77
71
  self.prompt_session = prompt_session or PromptSession()
78
72
  self.default_selection = default_selection
79
73
  self.number_selections = number_selections
@@ -262,7 +256,6 @@ class SelectionAction(BaseAction):
262
256
  len(self.selections),
263
257
  table,
264
258
  default_selection=effective_default,
265
- console=self.console,
266
259
  prompt_session=self.prompt_session,
267
260
  prompt_message=self.prompt_message,
268
261
  show_table=self.show_table,
@@ -306,7 +299,6 @@ class SelectionAction(BaseAction):
306
299
  (self.selections | cancel_option).keys(),
307
300
  table,
308
301
  default_selection=effective_default,
309
- console=self.console,
310
302
  prompt_session=self.prompt_session,
311
303
  prompt_message=self.prompt_message,
312
304
  show_table=self.show_table,
@@ -2,7 +2,6 @@
2
2
  """user_input_action.py"""
3
3
  from prompt_toolkit import PromptSession
4
4
  from prompt_toolkit.validation import Validator
5
- from rich.console import Console
6
5
  from rich.tree import Tree
7
6
 
8
7
  from falyx.action.base_action import BaseAction
@@ -20,7 +19,6 @@ class UserInputAction(BaseAction):
20
19
  name (str): Action name.
21
20
  prompt_text (str): Prompt text (can include '{last_result}' for interpolation).
22
21
  validator (Validator, optional): Prompt Toolkit validator.
23
- console (Console, optional): Rich console for rendering.
24
22
  prompt_session (PromptSession, optional): Reusable prompt session.
25
23
  inject_last_result (bool): Whether to inject last_result into prompt.
26
24
  inject_into (str): Key to use for injection (default: 'last_result').
@@ -33,7 +31,6 @@ class UserInputAction(BaseAction):
33
31
  prompt_text: str = "Input > ",
34
32
  default_text: str = "",
35
33
  validator: Validator | None = None,
36
- console: Console | None = None,
37
34
  prompt_session: PromptSession | None = None,
38
35
  inject_last_result: bool = False,
39
36
  ):
@@ -43,10 +40,6 @@ class UserInputAction(BaseAction):
43
40
  )
44
41
  self.prompt_text = prompt_text
45
42
  self.validator = validator
46
- if isinstance(console, Console):
47
- self.console = console
48
- elif console:
49
- raise ValueError("`console` must be an instance of `rich.console.Console`")
50
43
  self.prompt_session = prompt_session or PromptSession()
51
44
  self.default_text = default_text
52
45
 
falyx/bottom_bar.py CHANGED
@@ -5,8 +5,8 @@ from typing import Any, Callable
5
5
 
6
6
  from prompt_toolkit.formatted_text import HTML, merge_formatted_text
7
7
  from prompt_toolkit.key_binding import KeyBindings
8
- from rich.console import Console
9
8
 
9
+ from falyx.console import console
10
10
  from falyx.options_manager import OptionsManager
11
11
  from falyx.themes import OneColors
12
12
  from falyx.utils import CaseInsensitiveDict, chunks
@@ -30,7 +30,7 @@ class BottomBar:
30
30
  key_validator: Callable[[str], bool] | None = None,
31
31
  ) -> None:
32
32
  self.columns = columns
33
- self.console = Console(color_system="truecolor")
33
+ self.console: Console = console
34
34
  self._named_items: dict[str, Callable[[], HTML]] = {}
35
35
  self._value_getters: dict[str, Callable[[], Any]] = CaseInsensitiveDict()
36
36
  self.toggle_keys: list[str] = []
falyx/command.py CHANGED
@@ -23,11 +23,11 @@ from typing import Any, Awaitable, Callable
23
23
 
24
24
  from prompt_toolkit.formatted_text import FormattedText
25
25
  from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
26
- from rich.console import Console
27
26
  from rich.tree import Tree
28
27
 
29
28
  from falyx.action.action import Action
30
29
  from falyx.action.base_action import BaseAction
30
+ from falyx.console import console
31
31
  from falyx.context import ExecutionContext
32
32
  from falyx.debug import register_debug_hooks
33
33
  from falyx.execution_registry import ExecutionRegistry as er
@@ -44,8 +44,6 @@ from falyx.signals import CancelSignal
44
44
  from falyx.themes import OneColors
45
45
  from falyx.utils import ensure_async
46
46
 
47
- console = Console(color_system="truecolor")
48
-
49
47
 
50
48
  class Command(BaseModel):
51
49
  """
@@ -91,6 +89,12 @@ class Command(BaseModel):
91
89
  logging_hooks (bool): Whether to attach logging hooks automatically.
92
90
  options_manager (OptionsManager): Manages global command-line options.
93
91
  arg_parser (CommandArgumentParser): Parses command arguments.
92
+ arguments (list[dict[str, Any]]): Argument definitions for the command.
93
+ argument_config (Callable[[CommandArgumentParser], None] | None): Function to configure arguments
94
+ for the command parser.
95
+ arg_metadata (dict[str, str | dict[str, Any]]): Metadata for arguments,
96
+ such as help text or choices.
97
+ simple_help_signature (bool): Whether to use a simplified help signature.
94
98
  custom_parser (ArgParserProtocol | None): Custom argument parser.
95
99
  custom_help (Callable[[], str | None] | None): Custom help message generator.
96
100
  auto_args (bool): Automatically infer arguments from the action.
@@ -227,7 +231,7 @@ class Command(BaseModel):
227
231
  if self.logging_hooks and isinstance(self.action, BaseAction):
228
232
  register_debug_hooks(self.action.hooks)
229
233
 
230
- if self.arg_parser is None:
234
+ if self.arg_parser is None and not self.custom_parser:
231
235
  self.arg_parser = CommandArgumentParser(
232
236
  command_key=self.key,
233
237
  command_description=self.description,
falyx/completer.py ADDED
@@ -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))
falyx/config.py CHANGED
@@ -11,18 +11,16 @@ from typing import Any, Callable
11
11
  import toml
12
12
  import yaml
13
13
  from pydantic import BaseModel, Field, field_validator, model_validator
14
- from rich.console import Console
15
14
 
16
15
  from falyx.action.action import Action
17
16
  from falyx.action.base_action import BaseAction
18
17
  from falyx.command import Command
18
+ from falyx.console import console
19
19
  from falyx.falyx import Falyx
20
20
  from falyx.logger import logger
21
21
  from falyx.retry import RetryPolicy
22
22
  from falyx.themes import OneColors
23
23
 
24
- console = Console(color_system="truecolor")
25
-
26
24
 
27
25
  def wrap_if_needed(obj: Any, name=None) -> BaseAction | Command:
28
26
  if isinstance(obj, (BaseAction, Command)):
falyx/console.py ADDED
@@ -0,0 +1,5 @@
1
+ from rich.console import Console
2
+
3
+ from falyx.themes import get_nord_theme
4
+
5
+ console = Console(color_system="truecolor", theme=get_nord_theme())
falyx/context.py CHANGED
@@ -24,6 +24,8 @@ from typing import Any
24
24
  from pydantic import BaseModel, ConfigDict, Field
25
25
  from rich.console import Console
26
26
 
27
+ from falyx.console import console
28
+
27
29
 
28
30
  class ExecutionContext(BaseModel):
29
31
  """
@@ -83,7 +85,7 @@ class ExecutionContext(BaseModel):
83
85
  index: int | None = None
84
86
 
85
87
  extra: dict[str, Any] = Field(default_factory=dict)
86
- console: Console = Field(default_factory=lambda: Console(color_system="truecolor"))
88
+ console: Console = console
87
89
 
88
90
  shared_context: SharedContext | None = None
89
91
 
@@ -36,6 +36,7 @@ from rich import box
36
36
  from rich.console import Console
37
37
  from rich.table import Table
38
38
 
39
+ from falyx.console import console
39
40
  from falyx.context import ExecutionContext
40
41
  from falyx.logger import logger
41
42
  from falyx.themes import OneColors
falyx/falyx.py CHANGED
@@ -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,8 @@ 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
+ from falyx.console import console
49
50
  from falyx.context import ExecutionContext
50
51
  from falyx.debug import log_after, log_before, log_error, log_success
51
52
  from falyx.exceptions import (
@@ -63,7 +64,7 @@ from falyx.parser import CommandArgumentParser, FalyxParsers, get_arg_parsers
63
64
  from falyx.protocols import ArgParserProtocol
64
65
  from falyx.retry import RetryPolicy
65
66
  from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
66
- from falyx.themes import OneColors, get_nord_theme
67
+ from falyx.themes import OneColors
67
68
  from falyx.utils import CaseInsensitiveDict, _noop, chunks
68
69
  from falyx.version import __version__
69
70
 
@@ -201,7 +202,7 @@ class Falyx:
201
202
  self.help_command: Command | None = (
202
203
  self._get_help_command() if include_help_command else None
203
204
  )
204
- self.console: Console = Console(color_system="truecolor", theme=get_nord_theme())
205
+ self.console: Console = console
205
206
  self.welcome_message: str | Markdown | dict[str, Any] = welcome_message
206
207
  self.exit_message: str | Markdown | dict[str, Any] = exit_message
207
208
  self.hooks: HookManager = HookManager()
@@ -413,20 +414,9 @@ class Falyx:
413
414
  arg_parser=parser,
414
415
  )
415
416
 
416
- def _get_completer(self) -> WordCompleter:
417
+ def _get_completer(self) -> FalyxCompleter:
417
418
  """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)
419
+ return FalyxCompleter(self)
430
420
 
431
421
  def _get_validator_error_message(self) -> str:
432
422
  """Validator to check if the input is a valid command or toggle key."""
@@ -524,6 +514,8 @@ class Falyx:
524
514
  bottom_toolbar=self._get_bottom_bar_render(),
525
515
  key_bindings=self.key_bindings,
526
516
  validate_while_typing=True,
517
+ interrupt_exception=QuitSignal,
518
+ eof_exception=QuitSignal,
527
519
  )
528
520
  return self._prompt_session
529
521
 
falyx/falyx_completer.py CHANGED
@@ -1,53 +1,128 @@
1
- import shlex
2
- from typing import Iterable
3
-
1
+ from collections import Counter
4
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
5
6
 
7
+ from falyx.command import Command
6
8
  from falyx.parser.command_argument_parser import CommandArgumentParser
7
-
9
+ from falyx.parser.argument import Argument
10
+ from falyx.parser.argument_action import ArgumentAction
8
11
 
9
12
  class FalyxCompleter(Completer):
10
- def __init__(self, falyx: "Falyx") -> None:
13
+ """Completer for Falyx commands and their arguments."""
14
+ def __init__(self, falyx: "Falyx"):
11
15
  self.falyx = falyx
16
+ self._used_args: Set[str] = set()
17
+ self._used_args_counter: Counter = Counter()
12
18
 
13
- def get_completions(self, document, complete_event) -> Iterable[Completion]:
14
- text = document.text_before_cursor.strip()
15
- if not text:
16
- yield from self._complete_command("")
17
- return
18
-
19
+ def get_completions(self, document: Document, complete_event) -> Iterable[Completion]:
20
+ text = document.text_before_cursor
19
21
  try:
20
22
  tokens = shlex.split(text)
23
+ cursor_at_end_of_token = document.text_before_cursor.endswith((' ', '\t'))
21
24
  except ValueError:
22
- return # unmatched quotes or syntax error
25
+ return
23
26
 
24
- if not tokens:
25
- yield from self._complete_command("")
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 "")
26
30
  return
27
31
 
28
- command_token = tokens[0]
29
- command_key = command_token.lstrip("?").upper()
30
- command = self.falyx._name_map.get(command_key)
32
+ command = self._match_command(tokens[0])
33
+ if not command:
34
+ return
31
35
 
32
- if command is None:
33
- yield from self._complete_command(command_token)
36
+ if command.arg_parser is None:
34
37
  return
35
38
 
36
- used_flags = set(tokens[1:]) # simplistic
37
- parser: CommandArgumentParser = command.arg_parser or CommandArgumentParser()
38
- for arg in parser._keyword_list:
39
- for flag in arg.flags:
40
- if flag not in used_flags:
41
- yield Completion(flag, start_position=0)
39
+ self._set_used_args(tokens, command)
40
+
41
+ next_arg = self._next_expected_argument(tokens, command.arg_parser)
42
42
 
43
- for dest, arg in parser._positional.items():
44
- if dest not in used_flags:
45
- yield Completion(arg.dest, start_position=0)
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)
46
51
 
47
- def _complete_command(self, prefix: str) -> Iterable[Completion]:
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()
48
79
  seen = set()
49
80
  for cmd in self.falyx.commands.values():
50
81
  for key in [cmd.key] + cmd.aliases:
51
- if key not in seen and key.upper().startswith(prefix.upper()):
82
+ if key.upper().startswith(prefix) and key not in seen:
52
83
  yield Completion(key, start_position=-len(prefix))
53
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
+ )
falyx/init.py CHANGED
@@ -2,7 +2,7 @@
2
2
  """init.py"""
3
3
  from pathlib import Path
4
4
 
5
- from rich.console import Console
5
+ from falyx.console import console
6
6
 
7
7
  TEMPLATE_TASKS = """\
8
8
  # This file is used by falyx.yaml to define CLI actions.
@@ -98,8 +98,6 @@ commands:
98
98
  aliases: [clean, cleanup]
99
99
  """
100
100
 
101
- console = Console(color_system="truecolor")
102
-
103
101
 
104
102
  def init_project(name: str) -> None:
105
103
  target = Path(name).resolve()
falyx/parser/argument.py CHANGED
@@ -24,6 +24,7 @@ class Argument:
24
24
  nargs: int | str | None = None # int, '?', '*', '+', None
25
25
  positional: bool = False # True if no leading - or -- in flags
26
26
  resolver: BaseAction | None = None # Action object for the argument
27
+ lazy_resolver: bool = False # True if resolver should be called lazily
27
28
 
28
29
  def get_positional_text(self) -> str:
29
30
  """Get the positional text for the argument."""