Add multi selecto to SelectionAction and SelectFileAction, Allow IOActions to receive no input, Rename subpackage falyx.parsers -> falyx.parser, Add default_text to UserInputAction
This commit is contained in:
@ -6,11 +6,12 @@ from falyx.action.types import FileReturnType
|
||||
|
||||
sf = SelectFileAction(
|
||||
name="select_file",
|
||||
suffix_filter=".py",
|
||||
suffix_filter=".yaml",
|
||||
title="Select a YAML file",
|
||||
prompt_message="Choose > ",
|
||||
prompt_message="Choose 2 > ",
|
||||
return_type=FileReturnType.TEXT,
|
||||
columns=3,
|
||||
number_selections=2,
|
||||
)
|
||||
|
||||
flx = Falyx()
|
||||
|
@ -1,5 +1,7 @@
|
||||
import asyncio
|
||||
from uuid import uuid4
|
||||
|
||||
from falyx import Falyx
|
||||
from falyx.action import SelectionAction
|
||||
from falyx.selection import SelectionOption
|
||||
from falyx.signals import CancelSignal
|
||||
@ -24,7 +26,45 @@ select = SelectionAction(
|
||||
show_table=True,
|
||||
)
|
||||
|
||||
try:
|
||||
list_selections = [uuid4() for _ in range(10)]
|
||||
|
||||
list_select = SelectionAction(
|
||||
name="Select Deployments",
|
||||
selections=list_selections,
|
||||
title="Select Deployments",
|
||||
columns=3,
|
||||
prompt_message="Select 3 Deployments > ",
|
||||
return_type="value",
|
||||
show_table=True,
|
||||
number_selections=3,
|
||||
)
|
||||
|
||||
|
||||
flx = Falyx()
|
||||
|
||||
flx.add_command(
|
||||
key="S",
|
||||
description="Select a deployment",
|
||||
action=select,
|
||||
help_text="Select a deployment from the list",
|
||||
)
|
||||
flx.add_command(
|
||||
key="L",
|
||||
description="Select deployments",
|
||||
action=list_select,
|
||||
help_text="Select multiple deployments from the list",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
try:
|
||||
print(asyncio.run(select()))
|
||||
except CancelSignal:
|
||||
except CancelSignal:
|
||||
print("Selection was cancelled.")
|
||||
|
||||
try:
|
||||
print(asyncio.run(list_select()))
|
||||
except CancelSignal:
|
||||
print("Selection was cancelled.")
|
||||
|
||||
asyncio.run(flx.run())
|
||||
|
@ -2,7 +2,7 @@ import asyncio
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from falyx import Falyx
|
||||
from falyx.parsers import CommandArgumentParser
|
||||
from falyx.parser import CommandArgumentParser
|
||||
|
||||
flx = Falyx("Test Type Validation")
|
||||
|
||||
|
@ -14,7 +14,7 @@ from typing import Any
|
||||
|
||||
from falyx.config import loader
|
||||
from falyx.falyx import Falyx
|
||||
from falyx.parsers import CommandArgumentParser, get_root_parser, get_subparsers
|
||||
from falyx.parser import CommandArgumentParser, get_root_parser, get_subparsers
|
||||
|
||||
|
||||
def find_falyx_config() -> Path | None:
|
||||
|
@ -157,6 +157,6 @@ class Action(BaseAction):
|
||||
return (
|
||||
f"Action(name={self.name!r}, action="
|
||||
f"{getattr(self._action, '__name__', repr(self._action))}, "
|
||||
f"args={self.args!r}, kwargs={self.kwargs!r}, "
|
||||
f"retry={self.retry_policy.enabled})"
|
||||
f"retry={self.retry_policy.enabled}, "
|
||||
f"rollback={self.rollback is not None})"
|
||||
)
|
||||
|
@ -11,7 +11,7 @@ from falyx.context import ExecutionContext, SharedContext
|
||||
from falyx.execution_registry import ExecutionRegistry as er
|
||||
from falyx.hook_manager import Hook, HookManager, HookType
|
||||
from falyx.logger import logger
|
||||
from falyx.parsers.utils import same_argument_definitions
|
||||
from falyx.parser.utils import same_argument_definitions
|
||||
from falyx.themes.colors import OneColors
|
||||
|
||||
|
||||
|
@ -93,10 +93,7 @@ class BaseIOAction(BaseAction):
|
||||
if self.inject_last_result and self.shared_context:
|
||||
return self.shared_context.last_result()
|
||||
|
||||
logger.debug(
|
||||
"[%s] No input provided and no last result found for injection.", self.name
|
||||
)
|
||||
raise FalyxError("No input provided and no last result to inject.")
|
||||
return ""
|
||||
|
||||
def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
|
||||
return None, None
|
||||
|
@ -111,7 +111,7 @@ class MenuAction(BaseAction):
|
||||
key = effective_default
|
||||
if not self.never_prompt:
|
||||
table = self._build_table()
|
||||
key = await prompt_for_selection(
|
||||
key_ = await prompt_for_selection(
|
||||
self.menu_options.keys(),
|
||||
table,
|
||||
default_selection=self.default_selection,
|
||||
@ -120,6 +120,10 @@ class MenuAction(BaseAction):
|
||||
prompt_message=self.prompt_message,
|
||||
show_table=self.show_table,
|
||||
)
|
||||
if isinstance(key_, str):
|
||||
key = key_
|
||||
else:
|
||||
assert False, "Unreachable, MenuAction only supports single selection"
|
||||
option = self.menu_options[key]
|
||||
result = await option.action(*args, **kwargs)
|
||||
context.result = result
|
||||
|
@ -14,7 +14,7 @@ from falyx.context import ExecutionContext, SharedContext
|
||||
from falyx.execution_registry import ExecutionRegistry as er
|
||||
from falyx.hook_manager import HookManager, HookType
|
||||
from falyx.logger import logger
|
||||
from falyx.parsers.utils import same_argument_definitions
|
||||
from falyx.parser.utils import same_argument_definitions
|
||||
from falyx.themes import OneColors
|
||||
|
||||
|
||||
|
@ -66,6 +66,9 @@ class SelectFileAction(BaseAction):
|
||||
style: str = OneColors.WHITE,
|
||||
suffix_filter: str | None = None,
|
||||
return_type: FileReturnType | str = FileReturnType.PATH,
|
||||
number_selections: int | str = 1,
|
||||
separator: str = ",",
|
||||
allow_duplicates: bool = False,
|
||||
console: Console | None = None,
|
||||
prompt_session: PromptSession | None = None,
|
||||
):
|
||||
@ -76,6 +79,9 @@ class SelectFileAction(BaseAction):
|
||||
self.prompt_message = prompt_message
|
||||
self.suffix_filter = suffix_filter
|
||||
self.style = style
|
||||
self.number_selections = number_selections
|
||||
self.separator = separator
|
||||
self.allow_duplicates = allow_duplicates
|
||||
if isinstance(console, Console):
|
||||
self.console = console
|
||||
elif console:
|
||||
@ -83,6 +89,21 @@ class SelectFileAction(BaseAction):
|
||||
self.prompt_session = prompt_session or PromptSession()
|
||||
self.return_type = self._coerce_return_type(return_type)
|
||||
|
||||
@property
|
||||
def number_selections(self) -> int | str:
|
||||
return self._number_selections
|
||||
|
||||
@number_selections.setter
|
||||
def number_selections(self, value: int | str):
|
||||
if isinstance(value, int) and value > 0:
|
||||
self._number_selections: int | str = value
|
||||
elif isinstance(value, str):
|
||||
if value not in ("*"):
|
||||
raise ValueError("number_selections string must be one of '*'")
|
||||
self._number_selections = value
|
||||
else:
|
||||
raise ValueError("number_selections must be a positive integer or one of '*'")
|
||||
|
||||
def _coerce_return_type(self, return_type: FileReturnType | str) -> FileReturnType:
|
||||
if isinstance(return_type, FileReturnType):
|
||||
return return_type
|
||||
@ -163,18 +184,25 @@ class SelectFileAction(BaseAction):
|
||||
title=self.title, selections=options | cancel_option, columns=self.columns
|
||||
)
|
||||
|
||||
key = await prompt_for_selection(
|
||||
keys = await prompt_for_selection(
|
||||
(options | cancel_option).keys(),
|
||||
table,
|
||||
console=self.console,
|
||||
prompt_session=self.prompt_session,
|
||||
prompt_message=self.prompt_message,
|
||||
number_selections=self.number_selections,
|
||||
separator=self.separator,
|
||||
allow_duplicates=self.allow_duplicates,
|
||||
cancel_key=cancel_key,
|
||||
)
|
||||
|
||||
if key == cancel_key:
|
||||
if isinstance(keys, str):
|
||||
if keys == cancel_key:
|
||||
raise CancelSignal("User canceled the selection.")
|
||||
result = options[keys].value
|
||||
elif isinstance(keys, list):
|
||||
result = [options[key].value for key in keys]
|
||||
|
||||
result = options[key].value
|
||||
context.result = result
|
||||
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
||||
return result
|
||||
|
@ -48,6 +48,9 @@ class SelectionAction(BaseAction):
|
||||
columns: int = 5,
|
||||
prompt_message: str = "Select > ",
|
||||
default_selection: str = "",
|
||||
number_selections: int | str = 1,
|
||||
separator: str = ",",
|
||||
allow_duplicates: bool = False,
|
||||
inject_last_result: bool = False,
|
||||
inject_into: str = "last_result",
|
||||
return_type: SelectionReturnType | str = "value",
|
||||
@ -73,9 +76,26 @@ class SelectionAction(BaseAction):
|
||||
raise ValueError("`console` must be an instance of `rich.console.Console`")
|
||||
self.prompt_session = prompt_session or PromptSession()
|
||||
self.default_selection = default_selection
|
||||
self.number_selections = number_selections
|
||||
self.separator = separator
|
||||
self.allow_duplicates = allow_duplicates
|
||||
self.prompt_message = prompt_message
|
||||
self.show_table = show_table
|
||||
self.cancel_key = self._find_cancel_key()
|
||||
|
||||
@property
|
||||
def number_selections(self) -> int | str:
|
||||
return self._number_selections
|
||||
|
||||
@number_selections.setter
|
||||
def number_selections(self, value: int | str):
|
||||
if isinstance(value, int) and value > 0:
|
||||
self._number_selections: int | str = value
|
||||
elif isinstance(value, str):
|
||||
if value not in ("*"):
|
||||
raise ValueError("number_selections string must be '*'")
|
||||
self._number_selections = value
|
||||
else:
|
||||
raise ValueError("number_selections must be a positive integer or '*'")
|
||||
|
||||
def _coerce_return_type(
|
||||
self, return_type: SelectionReturnType | str
|
||||
@ -156,6 +176,38 @@ class SelectionAction(BaseAction):
|
||||
def get_infer_target(self) -> tuple[None, None]:
|
||||
return None, None
|
||||
|
||||
def _get_result_from_keys(self, keys: str | list[str]) -> Any:
|
||||
if not isinstance(self.selections, dict):
|
||||
raise TypeError("Selections must be a dictionary to get result by keys.")
|
||||
if self.return_type == SelectionReturnType.KEY:
|
||||
result: Any = keys
|
||||
elif self.return_type == SelectionReturnType.VALUE:
|
||||
if isinstance(keys, list):
|
||||
result = [self.selections[key].value for key in keys]
|
||||
elif isinstance(keys, str):
|
||||
result = self.selections[keys].value
|
||||
elif self.return_type == SelectionReturnType.ITEMS:
|
||||
if isinstance(keys, list):
|
||||
result = {key: self.selections[key] for key in keys}
|
||||
elif isinstance(keys, str):
|
||||
result = {keys: self.selections[keys]}
|
||||
elif self.return_type == SelectionReturnType.DESCRIPTION:
|
||||
if isinstance(keys, list):
|
||||
result = [self.selections[key].description for key in keys]
|
||||
elif isinstance(keys, str):
|
||||
result = self.selections[keys].description
|
||||
elif self.return_type == SelectionReturnType.DESCRIPTION_VALUE:
|
||||
if isinstance(keys, list):
|
||||
result = {
|
||||
self.selections[key].description: self.selections[key].value
|
||||
for key in keys
|
||||
}
|
||||
elif isinstance(keys, str):
|
||||
result = {self.selections[keys].description: self.selections[keys].value}
|
||||
else:
|
||||
raise ValueError(f"Unsupported return type: {self.return_type}")
|
||||
return result
|
||||
|
||||
async def _run(self, *args, **kwargs) -> Any:
|
||||
kwargs = self._maybe_inject_last_result(kwargs)
|
||||
context = ExecutionContext(
|
||||
@ -191,7 +243,7 @@ class SelectionAction(BaseAction):
|
||||
if self.never_prompt and not effective_default:
|
||||
raise ValueError(
|
||||
f"[{self.name}] 'never_prompt' is True but no valid default_selection "
|
||||
"was provided."
|
||||
"or usable last_result was available."
|
||||
)
|
||||
|
||||
context.start_timer()
|
||||
@ -206,7 +258,7 @@ class SelectionAction(BaseAction):
|
||||
formatter=self.cancel_formatter,
|
||||
)
|
||||
if not self.never_prompt:
|
||||
index: int | str = await prompt_for_index(
|
||||
indices: int | list[int] = await prompt_for_index(
|
||||
len(self.selections),
|
||||
table,
|
||||
default_selection=effective_default,
|
||||
@ -214,12 +266,30 @@ class SelectionAction(BaseAction):
|
||||
prompt_session=self.prompt_session,
|
||||
prompt_message=self.prompt_message,
|
||||
show_table=self.show_table,
|
||||
number_selections=self.number_selections,
|
||||
separator=self.separator,
|
||||
allow_duplicates=self.allow_duplicates,
|
||||
cancel_key=self.cancel_key,
|
||||
)
|
||||
else:
|
||||
index = effective_default
|
||||
if int(index) == int(self.cancel_key):
|
||||
if effective_default:
|
||||
indices = int(effective_default)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"[{self.name}] 'never_prompt' is True but no valid "
|
||||
"default_selection was provided."
|
||||
)
|
||||
|
||||
if indices == int(self.cancel_key):
|
||||
raise CancelSignal("User cancelled the selection.")
|
||||
result: Any = self.selections[int(index)]
|
||||
if isinstance(indices, list):
|
||||
result: str | list[str] = [
|
||||
self.selections[index] for index in indices
|
||||
]
|
||||
elif isinstance(indices, int):
|
||||
result = self.selections[indices]
|
||||
else:
|
||||
assert False, "unreachable"
|
||||
elif isinstance(self.selections, dict):
|
||||
cancel_option = {
|
||||
self.cancel_key: SelectionOption(
|
||||
@ -232,7 +302,7 @@ class SelectionAction(BaseAction):
|
||||
columns=self.columns,
|
||||
)
|
||||
if not self.never_prompt:
|
||||
key = await prompt_for_selection(
|
||||
keys = await prompt_for_selection(
|
||||
(self.selections | cancel_option).keys(),
|
||||
table,
|
||||
default_selection=effective_default,
|
||||
@ -240,25 +310,17 @@ class SelectionAction(BaseAction):
|
||||
prompt_session=self.prompt_session,
|
||||
prompt_message=self.prompt_message,
|
||||
show_table=self.show_table,
|
||||
number_selections=self.number_selections,
|
||||
separator=self.separator,
|
||||
allow_duplicates=self.allow_duplicates,
|
||||
cancel_key=self.cancel_key,
|
||||
)
|
||||
else:
|
||||
key = effective_default
|
||||
if key == self.cancel_key:
|
||||
keys = effective_default
|
||||
if keys == self.cancel_key:
|
||||
raise CancelSignal("User cancelled the selection.")
|
||||
if self.return_type == SelectionReturnType.KEY:
|
||||
result = key
|
||||
elif self.return_type == SelectionReturnType.VALUE:
|
||||
result = self.selections[key].value
|
||||
elif self.return_type == SelectionReturnType.ITEMS:
|
||||
result = {key: self.selections[key]}
|
||||
elif self.return_type == SelectionReturnType.DESCRIPTION:
|
||||
result = self.selections[key].description
|
||||
elif self.return_type == SelectionReturnType.DESCRIPTION_VALUE:
|
||||
result = {
|
||||
self.selections[key].description: self.selections[key].value
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Unsupported return type: {self.return_type}")
|
||||
|
||||
result = self._get_result_from_keys(keys)
|
||||
else:
|
||||
raise TypeError(
|
||||
"'selections' must be a list[str] or dict[str, Any], "
|
||||
|
@ -29,6 +29,7 @@ class UserInputAction(BaseAction):
|
||||
name: str,
|
||||
*,
|
||||
prompt_text: str = "Input > ",
|
||||
default_text: str = "",
|
||||
validator: Validator | None = None,
|
||||
console: Console | None = None,
|
||||
prompt_session: PromptSession | None = None,
|
||||
@ -45,6 +46,7 @@ class UserInputAction(BaseAction):
|
||||
elif console:
|
||||
raise ValueError("`console` must be an instance of `rich.console.Console`")
|
||||
self.prompt_session = prompt_session or PromptSession()
|
||||
self.default_text = default_text
|
||||
|
||||
def get_infer_target(self) -> tuple[None, None]:
|
||||
return None, None
|
||||
@ -67,6 +69,7 @@ class UserInputAction(BaseAction):
|
||||
answer = await self.prompt_session.prompt_async(
|
||||
prompt_text,
|
||||
validator=self.validator,
|
||||
default=kwargs.get("default_text", self.default_text),
|
||||
)
|
||||
context.result = answer
|
||||
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
||||
|
@ -34,8 +34,8 @@ from falyx.execution_registry import ExecutionRegistry as er
|
||||
from falyx.hook_manager import HookManager, HookType
|
||||
from falyx.logger import logger
|
||||
from falyx.options_manager import OptionsManager
|
||||
from falyx.parsers.argparse import CommandArgumentParser
|
||||
from falyx.parsers.signature import infer_args_from_func
|
||||
from falyx.parser.argparse import CommandArgumentParser
|
||||
from falyx.parser.signature import infer_args_from_func
|
||||
from falyx.prompt_utils import confirm_async, should_prompt_user
|
||||
from falyx.protocols import ArgParserProtocol
|
||||
from falyx.retry import RetryPolicy
|
||||
|
@ -129,7 +129,7 @@ class ExecutionContext(BaseModel):
|
||||
args = ", ".join(map(repr, self.args))
|
||||
kwargs = ", ".join(f"{key}={value!r}" for key, value in self.kwargs.items())
|
||||
signature = ", ".join(filter(None, [args, kwargs]))
|
||||
return f"{self.name} ({signature})"
|
||||
return f"{self.action} ({signature})"
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
return {
|
||||
|
@ -112,7 +112,7 @@ class ExecutionRegistry:
|
||||
cls,
|
||||
name: str = "",
|
||||
index: int | None = None,
|
||||
result: int | None = None,
|
||||
result_index: int | None = None,
|
||||
clear: bool = False,
|
||||
last_result: bool = False,
|
||||
status: Literal["all", "success", "error"] = "all",
|
||||
@ -138,12 +138,12 @@ class ExecutionRegistry:
|
||||
)
|
||||
return
|
||||
|
||||
if result is not None and result >= 0:
|
||||
if result_index is not None and result_index >= 0:
|
||||
try:
|
||||
result_context = cls._store_by_index[result]
|
||||
result_context = cls._store_by_index[result_index]
|
||||
except KeyError:
|
||||
cls._console.print(
|
||||
f"[{OneColors.DARK_RED}]❌ No execution found for index {index}."
|
||||
f"[{OneColors.DARK_RED}]❌ No execution found for index {result_index}."
|
||||
)
|
||||
return
|
||||
cls._console.print(f"{result_context.signature}:")
|
||||
|
@ -59,7 +59,7 @@ from falyx.execution_registry import ExecutionRegistry as er
|
||||
from falyx.hook_manager import Hook, HookManager, HookType
|
||||
from falyx.logger import logger
|
||||
from falyx.options_manager import OptionsManager
|
||||
from falyx.parsers import CommandArgumentParser, FalyxParsers, get_arg_parsers
|
||||
from falyx.parser import CommandArgumentParser, FalyxParsers, get_arg_parsers
|
||||
from falyx.protocols import ArgParserProtocol
|
||||
from falyx.retry import RetryPolicy
|
||||
from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
|
||||
@ -330,7 +330,13 @@ class Falyx:
|
||||
action="store_true",
|
||||
help="Clear the Execution History.",
|
||||
)
|
||||
parser.add_argument("-r", "--result", type=int, help="Get the result by index")
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--result",
|
||||
type=int,
|
||||
dest="result_index",
|
||||
help="Get the result by index",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l", "--last-result", action="store_true", help="Get the last result"
|
||||
)
|
||||
|
@ -12,7 +12,7 @@ from rich.text import Text
|
||||
|
||||
from falyx.action.base import BaseAction
|
||||
from falyx.exceptions import CommandArgumentError
|
||||
from falyx.parsers.utils import coerce_value
|
||||
from falyx.parser.utils import coerce_value
|
||||
from falyx.signals import HelpSignal
|
||||
|
||||
|
@ -7,7 +7,7 @@ from dateutil import parser as date_parser
|
||||
|
||||
from falyx.action.base import BaseAction
|
||||
from falyx.logger import logger
|
||||
from falyx.parsers.signature import infer_args_from_func
|
||||
from falyx.parser.signature import infer_args_from_func
|
||||
|
||||
|
||||
def coerce_bool(value: str) -> bool:
|
@ -11,7 +11,7 @@ from rich.table import Table
|
||||
|
||||
from falyx.themes import OneColors
|
||||
from falyx.utils import CaseInsensitiveDict, chunks
|
||||
from falyx.validators import int_range_validator, key_validator
|
||||
from falyx.validators import MultiIndexValidator, MultiKeyValidator
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -271,7 +271,11 @@ async def prompt_for_index(
|
||||
prompt_session: PromptSession | None = None,
|
||||
prompt_message: str = "Select an option > ",
|
||||
show_table: bool = True,
|
||||
) -> int:
|
||||
number_selections: int | str = 1,
|
||||
separator: str = ",",
|
||||
allow_duplicates: bool = False,
|
||||
cancel_key: str = "",
|
||||
) -> int | list[int]:
|
||||
prompt_session = prompt_session or PromptSession()
|
||||
console = console or Console(color_system="truecolor")
|
||||
|
||||
@ -280,10 +284,22 @@ async def prompt_for_index(
|
||||
|
||||
selection = await prompt_session.prompt_async(
|
||||
message=prompt_message,
|
||||
validator=int_range_validator(min_index, max_index),
|
||||
validator=MultiIndexValidator(
|
||||
min_index,
|
||||
max_index,
|
||||
number_selections,
|
||||
separator,
|
||||
allow_duplicates,
|
||||
cancel_key,
|
||||
),
|
||||
default=default_selection,
|
||||
)
|
||||
return int(selection)
|
||||
|
||||
if selection.strip() == cancel_key:
|
||||
return int(cancel_key)
|
||||
if isinstance(number_selections, int) and number_selections == 1:
|
||||
return int(selection.strip())
|
||||
return [int(index.strip()) for index in selection.strip().split(separator)]
|
||||
|
||||
|
||||
async def prompt_for_selection(
|
||||
@ -295,7 +311,11 @@ async def prompt_for_selection(
|
||||
prompt_session: PromptSession | None = None,
|
||||
prompt_message: str = "Select an option > ",
|
||||
show_table: bool = True,
|
||||
) -> str:
|
||||
number_selections: int | str = 1,
|
||||
separator: str = ",",
|
||||
allow_duplicates: bool = False,
|
||||
cancel_key: str = "",
|
||||
) -> str | list[str]:
|
||||
"""Prompt the user to select a key from a set of options. Return the selected key."""
|
||||
prompt_session = prompt_session or PromptSession()
|
||||
console = console or Console(color_system="truecolor")
|
||||
@ -305,11 +325,17 @@ async def prompt_for_selection(
|
||||
|
||||
selected = await prompt_session.prompt_async(
|
||||
message=prompt_message,
|
||||
validator=key_validator(keys),
|
||||
validator=MultiKeyValidator(
|
||||
keys, number_selections, separator, allow_duplicates, cancel_key
|
||||
),
|
||||
default=default_selection,
|
||||
)
|
||||
|
||||
return selected
|
||||
if selected.strip() == cancel_key:
|
||||
return cancel_key
|
||||
if isinstance(number_selections, int) and number_selections == 1:
|
||||
return selected.strip()
|
||||
return [key.strip() for key in selected.strip().split(separator)]
|
||||
|
||||
|
||||
async def select_value_from_list(
|
||||
@ -320,6 +346,10 @@ async def select_value_from_list(
|
||||
prompt_session: PromptSession | None = None,
|
||||
prompt_message: str = "Select an option > ",
|
||||
default_selection: str = "",
|
||||
number_selections: int | str = 1,
|
||||
separator: str = ",",
|
||||
allow_duplicates: bool = False,
|
||||
cancel_key: str = "",
|
||||
columns: int = 4,
|
||||
caption: str = "",
|
||||
box_style: box.Box = box.SIMPLE,
|
||||
@ -332,7 +362,7 @@ async def select_value_from_list(
|
||||
title_style: str = "",
|
||||
caption_style: str = "",
|
||||
highlight: bool = False,
|
||||
):
|
||||
) -> str | list[str]:
|
||||
"""Prompt for a selection. Return the selected item."""
|
||||
table = render_selection_indexed_table(
|
||||
title=title,
|
||||
@ -360,8 +390,14 @@ async def select_value_from_list(
|
||||
console=console,
|
||||
prompt_session=prompt_session,
|
||||
prompt_message=prompt_message,
|
||||
number_selections=number_selections,
|
||||
separator=separator,
|
||||
allow_duplicates=allow_duplicates,
|
||||
cancel_key=cancel_key,
|
||||
)
|
||||
|
||||
if isinstance(selection_index, list):
|
||||
return [selections[i] for i in selection_index]
|
||||
return selections[selection_index]
|
||||
|
||||
|
||||
@ -373,7 +409,11 @@ async def select_key_from_dict(
|
||||
prompt_session: PromptSession | None = None,
|
||||
prompt_message: str = "Select an option > ",
|
||||
default_selection: str = "",
|
||||
) -> Any:
|
||||
number_selections: int | str = 1,
|
||||
separator: str = ",",
|
||||
allow_duplicates: bool = False,
|
||||
cancel_key: str = "",
|
||||
) -> str | list[str]:
|
||||
"""Prompt for a key from a dict, returns the key."""
|
||||
prompt_session = prompt_session or PromptSession()
|
||||
console = console or Console(color_system="truecolor")
|
||||
@ -387,6 +427,10 @@ async def select_key_from_dict(
|
||||
console=console,
|
||||
prompt_session=prompt_session,
|
||||
prompt_message=prompt_message,
|
||||
number_selections=number_selections,
|
||||
separator=separator,
|
||||
allow_duplicates=allow_duplicates,
|
||||
cancel_key=cancel_key,
|
||||
)
|
||||
|
||||
|
||||
@ -398,7 +442,11 @@ async def select_value_from_dict(
|
||||
prompt_session: PromptSession | None = None,
|
||||
prompt_message: str = "Select an option > ",
|
||||
default_selection: str = "",
|
||||
) -> Any:
|
||||
number_selections: int | str = 1,
|
||||
separator: str = ",",
|
||||
allow_duplicates: bool = False,
|
||||
cancel_key: str = "",
|
||||
) -> Any | list[Any]:
|
||||
"""Prompt for a key from a dict, but return the value."""
|
||||
prompt_session = prompt_session or PromptSession()
|
||||
console = console or Console(color_system="truecolor")
|
||||
@ -412,8 +460,14 @@ async def select_value_from_dict(
|
||||
console=console,
|
||||
prompt_session=prompt_session,
|
||||
prompt_message=prompt_message,
|
||||
number_selections=number_selections,
|
||||
separator=separator,
|
||||
allow_duplicates=allow_duplicates,
|
||||
cancel_key=cancel_key,
|
||||
)
|
||||
|
||||
if isinstance(selection_key, list):
|
||||
return [selections[key].value for key in selection_key]
|
||||
return selections[selection_key].value
|
||||
|
||||
|
||||
@ -425,7 +479,11 @@ async def get_selection_from_dict_menu(
|
||||
prompt_session: PromptSession | None = None,
|
||||
prompt_message: str = "Select an option > ",
|
||||
default_selection: str = "",
|
||||
):
|
||||
number_selections: int | str = 1,
|
||||
separator: str = ",",
|
||||
allow_duplicates: bool = False,
|
||||
cancel_key: str = "",
|
||||
) -> Any | list[Any]:
|
||||
"""Prompt for a key from a dict, but return the value."""
|
||||
table = render_selection_dict_table(
|
||||
title,
|
||||
@ -439,4 +497,8 @@ async def get_selection_from_dict_menu(
|
||||
prompt_session=prompt_session,
|
||||
prompt_message=prompt_message,
|
||||
default_selection=default_selection,
|
||||
number_selections=number_selections,
|
||||
separator=separator,
|
||||
allow_duplicates=allow_duplicates,
|
||||
cancel_key=cancel_key,
|
||||
)
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""validators.py"""
|
||||
from typing import KeysView, Sequence
|
||||
|
||||
from prompt_toolkit.validation import Validator
|
||||
from prompt_toolkit.validation import ValidationError, Validator
|
||||
|
||||
|
||||
def int_range_validator(minimum: int, maximum: int) -> Validator:
|
||||
@ -45,3 +45,91 @@ def yes_no_validator() -> Validator:
|
||||
return True
|
||||
|
||||
return Validator.from_callable(validate, error_message="Enter 'Y' or 'n'.")
|
||||
|
||||
|
||||
class MultiIndexValidator(Validator):
|
||||
def __init__(
|
||||
self,
|
||||
minimum: int,
|
||||
maximum: int,
|
||||
number_selections: int | str,
|
||||
separator: str,
|
||||
allow_duplicates: bool,
|
||||
cancel_key: str,
|
||||
) -> None:
|
||||
self.minimum = minimum
|
||||
self.maximum = maximum
|
||||
self.number_selections = number_selections
|
||||
self.separator = separator
|
||||
self.allow_duplicates = allow_duplicates
|
||||
self.cancel_key = cancel_key
|
||||
super().__init__()
|
||||
|
||||
def validate(self, document):
|
||||
selections = [
|
||||
index.strip() for index in document.text.strip().split(self.separator)
|
||||
]
|
||||
if not selections or selections == [""]:
|
||||
raise ValidationError(message="Select at least 1 item.")
|
||||
if self.cancel_key in selections and len(selections) == 1:
|
||||
return
|
||||
elif self.cancel_key in selections:
|
||||
raise ValidationError(message="Cancel key must be selected alone.")
|
||||
for selection in selections:
|
||||
try:
|
||||
index = int(selection)
|
||||
if not self.minimum <= index <= self.maximum:
|
||||
raise ValidationError(
|
||||
message=f"Invalid selection: {selection}. Select a number between {self.minimum} and {self.maximum}."
|
||||
)
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
message=f"Invalid selection: {selection}. Select a number between {self.minimum} and {self.maximum}."
|
||||
)
|
||||
if not self.allow_duplicates and selections.count(selection) > 1:
|
||||
raise ValidationError(message=f"Duplicate selection: {selection}")
|
||||
if isinstance(self.number_selections, int):
|
||||
if self.number_selections == 1 and len(selections) > 1:
|
||||
raise ValidationError(message="Invalid selection. Select only 1 item.")
|
||||
if len(selections) != self.number_selections:
|
||||
raise ValidationError(
|
||||
message=f"Select exactly {self.number_selections} items separated by '{self.separator}'"
|
||||
)
|
||||
|
||||
|
||||
class MultiKeyValidator(Validator):
|
||||
def __init__(
|
||||
self,
|
||||
keys: Sequence[str] | KeysView[str],
|
||||
number_selections: int | str,
|
||||
separator: str,
|
||||
allow_duplicates: bool,
|
||||
cancel_key: str,
|
||||
) -> None:
|
||||
self.keys = keys
|
||||
self.separator = separator
|
||||
self.number_selections = number_selections
|
||||
self.allow_duplicates = allow_duplicates
|
||||
self.cancel_key = cancel_key
|
||||
super().__init__()
|
||||
|
||||
def validate(self, document):
|
||||
selections = [key.strip() for key in document.text.strip().split(self.separator)]
|
||||
if not selections or selections == [""]:
|
||||
raise ValidationError(message="Select at least 1 item.")
|
||||
if self.cancel_key in selections and len(selections) == 1:
|
||||
return
|
||||
elif self.cancel_key in selections:
|
||||
raise ValidationError(message="Cancel key must be selected alone.")
|
||||
for selection in selections:
|
||||
if selection.upper() not in [key.upper() for key in self.keys]:
|
||||
raise ValidationError(message=f"Invalid selection: {selection}")
|
||||
if not self.allow_duplicates and selections.count(selection) > 1:
|
||||
raise ValidationError(message=f"Duplicate selection: {selection}")
|
||||
if isinstance(self.number_selections, int):
|
||||
if self.number_selections == 1 and len(selections) > 1:
|
||||
raise ValidationError(message="Invalid selection. Select only 1 item.")
|
||||
if len(selections) != self.number_selections:
|
||||
raise ValidationError(
|
||||
message=f"Select exactly {self.number_selections} items separated by '{self.separator}'"
|
||||
)
|
||||
|
@ -1 +1 @@
|
||||
__version__ = "0.1.50"
|
||||
__version__ = "0.1.51"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "falyx"
|
||||
version = "0.1.50"
|
||||
version = "0.1.51"
|
||||
description = "Reliable and introspectable async CLI action framework."
|
||||
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
||||
license = "MIT"
|
||||
|
@ -38,13 +38,14 @@ async def test_action_async_callable():
|
||||
action = Action("test_action", async_callable)
|
||||
result = await action()
|
||||
assert result == "Hello, World!"
|
||||
print(action)
|
||||
assert (
|
||||
str(action)
|
||||
== "Action(name='test_action', action=async_callable, args=(), kwargs={}, retry=False)"
|
||||
== "Action(name='test_action', action=async_callable, retry=False, rollback=False)"
|
||||
)
|
||||
assert (
|
||||
repr(action)
|
||||
== "Action(name='test_action', action=async_callable, args=(), kwargs={}, retry=False)"
|
||||
== "Action(name='test_action', action=async_callable, retry=False, rollback=False)"
|
||||
)
|
||||
|
||||
|
||||
|
@ -50,9 +50,10 @@ def test_command_str():
|
||||
"""Test if Command string representation is correct."""
|
||||
action = Action("test_action", dummy_action)
|
||||
cmd = Command(key="TEST", description="Test Command", action=action)
|
||||
print(cmd)
|
||||
assert (
|
||||
str(cmd)
|
||||
== "Command(key='TEST', description='Test Command' action='Action(name='test_action', action=dummy_action, args=(), kwargs={}, retry=False)')"
|
||||
== "Command(key='TEST', description='Test Command' action='Action(name='test_action', action=dummy_action, retry=False, rollback=False)')"
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from falyx.exceptions import CommandArgumentError
|
||||
from falyx.parsers import ArgumentAction, CommandArgumentParser
|
||||
from falyx.parser import ArgumentAction, CommandArgumentParser
|
||||
from falyx.signals import HelpSignal
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
from falyx.action import Action, SelectionAction
|
||||
from falyx.exceptions import CommandArgumentError
|
||||
from falyx.parsers import ArgumentAction, CommandArgumentParser
|
||||
from falyx.parser import ArgumentAction, CommandArgumentParser
|
||||
|
||||
|
||||
def test_add_argument():
|
||||
|
@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from falyx.parsers import Argument, ArgumentAction
|
||||
from falyx.parser import Argument, ArgumentAction
|
||||
|
||||
|
||||
def test_positional_text_with_choices():
|
||||
|
@ -1,4 +1,4 @@
|
||||
from falyx.parsers import ArgumentAction
|
||||
from falyx.parser import ArgumentAction
|
||||
|
||||
|
||||
def test_argument_action():
|
||||
|
@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from falyx.exceptions import CommandArgumentError
|
||||
from falyx.parsers import CommandArgumentParser
|
||||
from falyx.parser import CommandArgumentParser
|
||||
|
||||
|
||||
def test_str():
|
||||
|
@ -5,7 +5,7 @@ from typing import Literal
|
||||
|
||||
import pytest
|
||||
|
||||
from falyx.parsers.utils import coerce_value
|
||||
from falyx.parser.utils import coerce_value
|
||||
|
||||
|
||||
# --- Tests ---
|
||||
|
@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from falyx.exceptions import CommandArgumentError
|
||||
from falyx.parsers import ArgumentAction, CommandArgumentParser
|
||||
from falyx.parser import ArgumentAction, CommandArgumentParser
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from falyx.exceptions import CommandArgumentError
|
||||
from falyx.parsers import ArgumentAction, CommandArgumentParser
|
||||
from falyx.parser import ArgumentAction, CommandArgumentParser
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
Reference in New Issue
Block a user