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:
2025-06-08 12:09:16 -04:00
parent b24079ea7e
commit 53ba6a896a
36 changed files with 373 additions and 80 deletions

View File

@ -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()

View File

@ -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,
)
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:
print("Selection was cancelled.")
try:
print(asyncio.run(list_select()))
except CancelSignal:
print("Selection was cancelled.")
asyncio.run(flx.run())

View File

@ -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")

View File

@ -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:

View File

@ -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})"
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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], "

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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}:")

View File

@ -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"
)

View File

@ -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

View File

@ -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:

View File

@ -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,
)

View File

@ -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}'"
)

View File

@ -1 +1 @@
__version__ = "0.1.50"
__version__ = "0.1.51"

View File

@ -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"

View File

@ -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)"
)

View File

@ -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)')"
)

View File

@ -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

View File

@ -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():

View File

@ -1,6 +1,6 @@
import pytest
from falyx.parsers import Argument, ArgumentAction
from falyx.parser import Argument, ArgumentAction
def test_positional_text_with_choices():

View File

@ -1,4 +1,4 @@
from falyx.parsers import ArgumentAction
from falyx.parser import ArgumentAction
def test_argument_action():

View File

@ -1,7 +1,7 @@
import pytest
from falyx.exceptions import CommandArgumentError
from falyx.parsers import CommandArgumentParser
from falyx.parser import CommandArgumentParser
def test_str():

View File

@ -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 ---

View File

@ -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

View File

@ -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