Add PromptMenuAction, add cancel button to SelectionAction, make get_command async, add Action validation and defauilt nargs to None.
This commit is contained in:
parent
b0c0e7dc16
commit
ddb78bd5a7
|
@ -6,7 +6,7 @@ from falyx.action import ActionFactoryAction, ChainedAction, HTTPAction, Selecti
|
|||
# Selection of a post ID to fetch (just an example set)
|
||||
post_selector = SelectionAction(
|
||||
name="Pick Post ID",
|
||||
selections=["1", "2", "3", "4", "5"],
|
||||
selections=["15", "25", "35", "45", "55"],
|
||||
title="Choose a Post ID to submit",
|
||||
prompt_message="Post ID > ",
|
||||
show_table=True,
|
||||
|
@ -14,7 +14,7 @@ post_selector = SelectionAction(
|
|||
|
||||
|
||||
# Factory that builds and executes the actual HTTP POST request
|
||||
def build_post_action(post_id) -> HTTPAction:
|
||||
async def build_post_action(post_id) -> HTTPAction:
|
||||
print(f"Building HTTPAction for Post ID: {post_id}")
|
||||
return HTTPAction(
|
||||
name=f"POST to /posts (id={post_id})",
|
||||
|
|
|
@ -2,8 +2,16 @@ import asyncio
|
|||
import time
|
||||
|
||||
from falyx import Falyx
|
||||
from falyx.action import Action, ActionGroup, ChainedAction, MenuAction, ProcessAction
|
||||
from falyx.action import (
|
||||
Action,
|
||||
ActionGroup,
|
||||
ChainedAction,
|
||||
MenuAction,
|
||||
ProcessAction,
|
||||
PromptMenuAction,
|
||||
)
|
||||
from falyx.menu import MenuOption, MenuOptionMap
|
||||
from falyx.themes import OneColors
|
||||
|
||||
|
||||
# Basic coroutine for Action
|
||||
|
@ -77,20 +85,28 @@ parallel = ActionGroup(
|
|||
|
||||
process = ProcessAction(name="compute", action=heavy_computation)
|
||||
|
||||
menu_options = MenuOptionMap(
|
||||
{
|
||||
"A": MenuOption("Run basic Action", basic_action, style=OneColors.LIGHT_YELLOW),
|
||||
"C": MenuOption("Run ChainedAction", chained, style=OneColors.MAGENTA),
|
||||
"P": MenuOption("Run ActionGroup (parallel)", parallel, style=OneColors.CYAN),
|
||||
"H": MenuOption("Run ProcessAction (heavy task)", process, style=OneColors.GREEN),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Menu setup
|
||||
|
||||
menu = MenuAction(
|
||||
name="main-menu",
|
||||
title="Choose a task to run",
|
||||
menu_options=MenuOptionMap(
|
||||
{
|
||||
"1": MenuOption("Run basic Action", basic_action),
|
||||
"2": MenuOption("Run ChainedAction", chained),
|
||||
"3": MenuOption("Run ActionGroup (parallel)", parallel),
|
||||
"4": MenuOption("Run ProcessAction (heavy task)", process),
|
||||
}
|
||||
),
|
||||
menu_options=menu_options,
|
||||
)
|
||||
|
||||
|
||||
prompt_menu = PromptMenuAction(
|
||||
name="select-user",
|
||||
menu_options=menu_options,
|
||||
)
|
||||
|
||||
flx = Falyx(
|
||||
|
@ -108,6 +124,13 @@ flx.add_command(
|
|||
logging_hooks=True,
|
||||
)
|
||||
|
||||
flx.add_command(
|
||||
key="P",
|
||||
description="Show Prompt Menu",
|
||||
action=prompt_menu,
|
||||
logging_hooks=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(flx.run())
|
||||
|
|
|
@ -2,6 +2,7 @@ import asyncio
|
|||
|
||||
from falyx.action import SelectionAction
|
||||
from falyx.selection import SelectionOption
|
||||
from falyx.signals import CancelSignal
|
||||
|
||||
selections = {
|
||||
"1": SelectionOption(
|
||||
|
@ -23,4 +24,7 @@ select = SelectionAction(
|
|||
show_table=True,
|
||||
)
|
||||
|
||||
try:
|
||||
print(asyncio.run(select()))
|
||||
except CancelSignal:
|
||||
print("Selection was cancelled.")
|
||||
|
|
|
@ -18,6 +18,7 @@ from .action_factory import ActionFactoryAction
|
|||
from .http_action import HTTPAction
|
||||
from .io_action import BaseIOAction, ShellAction
|
||||
from .menu_action import MenuAction
|
||||
from .prompt_menu_action import PromptMenuAction
|
||||
from .select_file_action import SelectFileAction
|
||||
from .selection_action import SelectionAction
|
||||
from .signal_action import SignalAction
|
||||
|
@ -40,4 +41,5 @@ __all__ = [
|
|||
"FallbackAction",
|
||||
"LiteralInputAction",
|
||||
"UserInputAction",
|
||||
"PromptMenuAction",
|
||||
]
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||
"""prompt_menu_action.py"""
|
||||
from typing import Any
|
||||
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.formatted_text import FormattedText, merge_formatted_text
|
||||
from rich.console import Console
|
||||
from rich.tree import Tree
|
||||
|
||||
from falyx.action.action import BaseAction
|
||||
from falyx.context import ExecutionContext
|
||||
from falyx.execution_registry import ExecutionRegistry as er
|
||||
from falyx.hook_manager import HookType
|
||||
from falyx.logger import logger
|
||||
from falyx.menu import MenuOptionMap
|
||||
from falyx.signals import BackSignal, QuitSignal
|
||||
from falyx.themes import OneColors
|
||||
|
||||
|
||||
class PromptMenuAction(BaseAction):
|
||||
"""PromptMenuAction class for creating prompt -> actions."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
menu_options: MenuOptionMap,
|
||||
*,
|
||||
prompt_message: str = "Select > ",
|
||||
default_selection: str = "",
|
||||
inject_last_result: bool = False,
|
||||
inject_into: str = "last_result",
|
||||
console: Console | None = None,
|
||||
prompt_session: PromptSession | None = None,
|
||||
never_prompt: bool = False,
|
||||
include_reserved: bool = True,
|
||||
):
|
||||
super().__init__(
|
||||
name,
|
||||
inject_last_result=inject_last_result,
|
||||
inject_into=inject_into,
|
||||
never_prompt=never_prompt,
|
||||
)
|
||||
self.menu_options = menu_options
|
||||
self.prompt_message = prompt_message
|
||||
self.default_selection = default_selection
|
||||
self.console = console or Console(color_system="auto")
|
||||
self.prompt_session = prompt_session or PromptSession()
|
||||
self.include_reserved = include_reserved
|
||||
|
||||
def get_infer_target(self) -> tuple[None, None]:
|
||||
return None, None
|
||||
|
||||
async def _run(self, *args, **kwargs) -> Any:
|
||||
kwargs = self._maybe_inject_last_result(kwargs)
|
||||
context = ExecutionContext(
|
||||
name=self.name,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
action=self,
|
||||
)
|
||||
|
||||
effective_default = self.default_selection
|
||||
maybe_result = str(self.last_result)
|
||||
if maybe_result in self.menu_options:
|
||||
effective_default = maybe_result
|
||||
elif self.inject_last_result:
|
||||
logger.warning(
|
||||
"[%s] Injected last result '%s' not found in menu options",
|
||||
self.name,
|
||||
maybe_result,
|
||||
)
|
||||
|
||||
if self.never_prompt and not effective_default:
|
||||
raise ValueError(
|
||||
f"[{self.name}] 'never_prompt' is True but no valid default_selection"
|
||||
" was provided."
|
||||
)
|
||||
|
||||
context.start_timer()
|
||||
try:
|
||||
await self.hooks.trigger(HookType.BEFORE, context)
|
||||
key = effective_default
|
||||
if not self.never_prompt:
|
||||
placeholder_formatted_text = []
|
||||
for index, (key, option) in enumerate(self.menu_options.items()):
|
||||
placeholder_formatted_text.append(option.render_prompt(key))
|
||||
if index < len(self.menu_options) - 1:
|
||||
placeholder_formatted_text.append(
|
||||
FormattedText([(OneColors.WHITE, " | ")])
|
||||
)
|
||||
placeholder = merge_formatted_text(placeholder_formatted_text)
|
||||
key = await self.prompt_session.prompt_async(
|
||||
message=self.prompt_message, placeholder=placeholder
|
||||
)
|
||||
option = self.menu_options[key]
|
||||
result = await option.action(*args, **kwargs)
|
||||
context.result = result
|
||||
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
||||
return result
|
||||
|
||||
except BackSignal:
|
||||
logger.debug("[%s][BackSignal] ← Returning to previous menu", self.name)
|
||||
return None
|
||||
except QuitSignal:
|
||||
logger.debug("[%s][QuitSignal] ← Exiting application", self.name)
|
||||
raise
|
||||
except Exception as error:
|
||||
context.exception = error
|
||||
await self.hooks.trigger(HookType.ON_ERROR, context)
|
||||
raise
|
||||
finally:
|
||||
context.stop_timer()
|
||||
await self.hooks.trigger(HookType.AFTER, context)
|
||||
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
|
||||
er.record(context)
|
||||
|
||||
async def preview(self, parent: Tree | None = None):
|
||||
label = f"[{OneColors.LIGHT_YELLOW_b}]📋 PromptMenuAction[/] '{self.name}'"
|
||||
tree = parent.add(label) if parent else Tree(label)
|
||||
for key, option in self.menu_options.items():
|
||||
tree.add(
|
||||
f"[dim]{key}[/]: {option.description} → [italic]{option.action.name}[/]"
|
||||
)
|
||||
await option.action.preview(parent=tree)
|
||||
if not parent:
|
||||
self.console.print(tree)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"PromptMenuAction(name={self.name!r}, options={list(self.menu_options.keys())!r}, "
|
||||
f"default_selection={self.default_selection!r}, "
|
||||
f"include_reserved={self.include_reserved}, "
|
||||
f"prompt={'off' if self.never_prompt else 'on'})"
|
||||
)
|
|
@ -1,5 +1,6 @@
|
|||
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||
"""selection_action.py"""
|
||||
from copy import copy
|
||||
from typing import Any
|
||||
|
||||
from prompt_toolkit import PromptSession
|
||||
|
@ -72,6 +73,7 @@ class SelectionAction(BaseAction):
|
|||
self.default_selection = default_selection
|
||||
self.prompt_message = prompt_message
|
||||
self.show_table = show_table
|
||||
self.cancel_key = self._find_cancel_key()
|
||||
|
||||
def _coerce_return_type(
|
||||
self, return_type: SelectionReturnType | str
|
||||
|
@ -115,12 +117,40 @@ class SelectionAction(BaseAction):
|
|||
)
|
||||
|
||||
def _find_cancel_key(self) -> str:
|
||||
"""Return first numeric value not already used in the selection dict."""
|
||||
for index in range(len(self.selections)):
|
||||
"""Find the cancel key in the selections."""
|
||||
if isinstance(self.selections, dict):
|
||||
for index in range(len(self.selections) + 1):
|
||||
if str(index) not in self.selections:
|
||||
return str(index)
|
||||
return str(len(self.selections))
|
||||
|
||||
@property
|
||||
def cancel_key(self) -> str:
|
||||
return self._cancel_key
|
||||
|
||||
@cancel_key.setter
|
||||
def cancel_key(self, value: str) -> None:
|
||||
"""Set the cancel key for the selection."""
|
||||
if not isinstance(value, str):
|
||||
raise TypeError("Cancel key must be a string.")
|
||||
if isinstance(self.selections, dict) and value in self.selections:
|
||||
raise ValueError(
|
||||
"Cancel key cannot be one of the selection keys. "
|
||||
f"Current selections: {self.selections}"
|
||||
)
|
||||
if isinstance(self.selections, list):
|
||||
if not value.isdigit() or int(value) > len(self.selections):
|
||||
raise ValueError(
|
||||
"cancel_key must be a digit and not greater than the number of selections."
|
||||
)
|
||||
self._cancel_key = value
|
||||
|
||||
def cancel_formatter(self, index: int, selection: str) -> str:
|
||||
"""Format the cancel option for display."""
|
||||
if self.cancel_key == str(index):
|
||||
return f"[{index}] [{OneColors.DARK_RED}]Cancel[/]"
|
||||
return f"[{index}] {selection}"
|
||||
|
||||
def get_infer_target(self) -> tuple[None, None]:
|
||||
return None, None
|
||||
|
||||
|
@ -164,16 +194,17 @@ class SelectionAction(BaseAction):
|
|||
|
||||
context.start_timer()
|
||||
try:
|
||||
cancel_key = self._find_cancel_key()
|
||||
self.cancel_key = self._find_cancel_key()
|
||||
await self.hooks.trigger(HookType.BEFORE, context)
|
||||
if isinstance(self.selections, list):
|
||||
table = render_selection_indexed_table(
|
||||
title=self.title,
|
||||
selections=self.selections + ["Cancel"],
|
||||
columns=self.columns,
|
||||
formatter=self.cancel_formatter,
|
||||
)
|
||||
if not self.never_prompt:
|
||||
index = await prompt_for_index(
|
||||
index: int | str = await prompt_for_index(
|
||||
len(self.selections),
|
||||
table,
|
||||
default_selection=effective_default,
|
||||
|
@ -184,12 +215,12 @@ class SelectionAction(BaseAction):
|
|||
)
|
||||
else:
|
||||
index = effective_default
|
||||
if index == cancel_key:
|
||||
if int(index) == int(self.cancel_key):
|
||||
raise CancelSignal("User cancelled the selection.")
|
||||
result: Any = self.selections[int(index)]
|
||||
elif isinstance(self.selections, dict):
|
||||
cancel_option = {
|
||||
cancel_key: SelectionOption(
|
||||
self.cancel_key: SelectionOption(
|
||||
description="Cancel", value=CancelSignal, style=OneColors.DARK_RED
|
||||
)
|
||||
}
|
||||
|
@ -210,7 +241,7 @@ class SelectionAction(BaseAction):
|
|||
)
|
||||
else:
|
||||
key = effective_default
|
||||
if key == cancel_key:
|
||||
if key == self.cancel_key:
|
||||
raise CancelSignal("User cancelled the selection.")
|
||||
if self.return_type == SelectionReturnType.KEY:
|
||||
result = key
|
||||
|
|
|
@ -139,7 +139,7 @@ class Command(BaseModel):
|
|||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
def parse_args(
|
||||
async def parse_args(
|
||||
self, raw_args: list[str] | str, from_validate: bool = False
|
||||
) -> tuple[tuple, dict]:
|
||||
if callable(self.custom_parser):
|
||||
|
@ -165,7 +165,9 @@ class Command(BaseModel):
|
|||
raw_args,
|
||||
)
|
||||
return ((), {})
|
||||
return self.arg_parser.parse_args_split(raw_args, from_validate=from_validate)
|
||||
return await self.arg_parser.parse_args_split(
|
||||
raw_args, from_validate=from_validate
|
||||
)
|
||||
|
||||
@field_validator("action", mode="before")
|
||||
@classmethod
|
||||
|
|
|
@ -83,8 +83,11 @@ class CommandValidator(Validator):
|
|||
self.error_message = error_message
|
||||
|
||||
def validate(self, document) -> None:
|
||||
pass
|
||||
|
||||
async def validate_async(self, document) -> None:
|
||||
text = document.text
|
||||
is_preview, choice, _, __ = self.falyx.get_command(text, from_validate=True)
|
||||
is_preview, choice, _, __ = await self.falyx.get_command(text, from_validate=True)
|
||||
if is_preview:
|
||||
return None
|
||||
if not choice:
|
||||
|
@ -188,7 +191,7 @@ class Falyx:
|
|||
self.cli_args: Namespace | None = cli_args
|
||||
self.render_menu: Callable[[Falyx], None] | None = render_menu
|
||||
self.custom_table: Callable[[Falyx], Table] | Table | None = custom_table
|
||||
self.hide_menu_table: bool = hide_menu_table
|
||||
self._hide_menu_table: bool = hide_menu_table
|
||||
self.validate_options(cli_args, options)
|
||||
self._prompt_session: PromptSession | None = None
|
||||
self.mode = FalyxMode.MENU
|
||||
|
@ -740,7 +743,7 @@ class Falyx:
|
|||
return True, input_str[1:].strip()
|
||||
return False, input_str.strip()
|
||||
|
||||
def get_command(
|
||||
async def get_command(
|
||||
self, raw_choices: str, from_validate=False
|
||||
) -> tuple[bool, Command | None, tuple, dict[str, Any]]:
|
||||
"""
|
||||
|
@ -773,7 +776,9 @@ class Falyx:
|
|||
if is_preview:
|
||||
return True, name_map[choice], args, kwargs
|
||||
try:
|
||||
args, kwargs = name_map[choice].parse_args(input_args, from_validate)
|
||||
args, kwargs = await name_map[choice].parse_args(
|
||||
input_args, from_validate
|
||||
)
|
||||
except CommandArgumentError as error:
|
||||
if not from_validate:
|
||||
if not name_map[choice].show_help():
|
||||
|
@ -834,7 +839,7 @@ class Falyx:
|
|||
"""Processes the action of the selected command."""
|
||||
with patch_stdout(raw=True):
|
||||
choice = await self.prompt_session.prompt_async()
|
||||
is_preview, selected_command, args, kwargs = self.get_command(choice)
|
||||
is_preview, selected_command, args, kwargs = await self.get_command(choice)
|
||||
if not selected_command:
|
||||
logger.info("Invalid command '%s'.", choice)
|
||||
return True
|
||||
|
@ -876,7 +881,7 @@ class Falyx:
|
|||
) -> Any:
|
||||
"""Run a command by key without displaying the menu (non-interactive mode)."""
|
||||
self.debug_hooks()
|
||||
is_preview, selected_command, _, __ = self.get_command(command_key)
|
||||
is_preview, selected_command, _, __ = await self.get_command(command_key)
|
||||
kwargs = kwargs or {}
|
||||
|
||||
self.last_run_command = selected_command
|
||||
|
@ -975,7 +980,7 @@ class Falyx:
|
|||
self.print_message(self.welcome_message)
|
||||
try:
|
||||
while True:
|
||||
if not self.hide_menu_table:
|
||||
if not self.options.get("hide_menu_table", self._hide_menu_table):
|
||||
if callable(self.render_menu):
|
||||
self.render_menu(self)
|
||||
else:
|
||||
|
@ -1012,6 +1017,9 @@ class Falyx:
|
|||
if not self.options.get("force_confirm"):
|
||||
self.options.set("force_confirm", self._force_confirm)
|
||||
|
||||
if not self.options.get("hide_menu_table"):
|
||||
self.options.set("hide_menu_table", self._hide_menu_table)
|
||||
|
||||
if self.cli_args.verbose:
|
||||
logging.getLogger("falyx").setLevel(logging.DEBUG)
|
||||
|
||||
|
@ -1029,7 +1037,7 @@ class Falyx:
|
|||
|
||||
if self.cli_args.command == "preview":
|
||||
self.mode = FalyxMode.PREVIEW
|
||||
_, command, args, kwargs = self.get_command(self.cli_args.name)
|
||||
_, command, args, kwargs = await self.get_command(self.cli_args.name)
|
||||
if not command:
|
||||
self.console.print(
|
||||
f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found."
|
||||
|
@ -1043,7 +1051,7 @@ class Falyx:
|
|||
|
||||
if self.cli_args.command == "run":
|
||||
self.mode = FalyxMode.RUN
|
||||
is_preview, command, _, __ = self.get_command(self.cli_args.name)
|
||||
is_preview, command, _, __ = await self.get_command(self.cli_args.name)
|
||||
if is_preview:
|
||||
if command is None:
|
||||
sys.exit(1)
|
||||
|
@ -1054,7 +1062,7 @@ class Falyx:
|
|||
sys.exit(1)
|
||||
self._set_retry_policy(command)
|
||||
try:
|
||||
args, kwargs = command.parse_args(self.cli_args.command_args)
|
||||
args, kwargs = await command.parse_args(self.cli_args.command_args)
|
||||
except HelpSignal:
|
||||
sys.exit(0)
|
||||
try:
|
||||
|
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
|||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
|
||||
from falyx.action import BaseAction
|
||||
from falyx.signals import BackSignal, QuitSignal
|
||||
from falyx.themes import OneColors
|
||||
|
@ -26,6 +28,12 @@ class MenuOption:
|
|||
"""Render the menu option for display."""
|
||||
return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
|
||||
|
||||
def render_prompt(self, key: str) -> FormattedText:
|
||||
"""Render the menu option for prompt display."""
|
||||
return FormattedText(
|
||||
[(OneColors.WHITE, f"[{key}] "), (self.style, self.description)]
|
||||
)
|
||||
|
||||
|
||||
class MenuOptionMap(CaseInsensitiveDict):
|
||||
"""
|
||||
|
|
|
@ -39,7 +39,7 @@ class ArgumentAction(Enum):
|
|||
class Argument:
|
||||
"""Represents a command-line argument."""
|
||||
|
||||
flags: list[str]
|
||||
flags: tuple[str, ...]
|
||||
dest: str # Destination name for the argument
|
||||
action: ArgumentAction = (
|
||||
ArgumentAction.STORE
|
||||
|
@ -49,7 +49,7 @@ class Argument:
|
|||
choices: list[str] | None = None # List of valid choices for the argument
|
||||
required: bool = False # True if the argument is required
|
||||
help: str = "" # Help text for the argument
|
||||
nargs: int | str = 1 # int, '?', '*', '+'
|
||||
nargs: int | str | None = None # int, '?', '*', '+', None
|
||||
positional: bool = False # True if no leading - or -- in flags
|
||||
|
||||
def get_positional_text(self) -> str:
|
||||
|
@ -151,6 +151,7 @@ class CommandArgumentParser:
|
|||
aliases: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the CommandArgumentParser."""
|
||||
self.console = Console(color_system="auto")
|
||||
self.command_key: str = command_key
|
||||
self.command_description: str = command_description
|
||||
self.command_style: str = command_style
|
||||
|
@ -163,7 +164,6 @@ class CommandArgumentParser:
|
|||
self._flag_map: dict[str, Argument] = {}
|
||||
self._dest_set: set[str] = set()
|
||||
self._add_help()
|
||||
self.console = Console(color_system="auto")
|
||||
|
||||
def _add_help(self):
|
||||
"""Add help argument to the parser."""
|
||||
|
@ -185,9 +185,7 @@ class CommandArgumentParser:
|
|||
raise CommandArgumentError("Positional arguments cannot have multiple flags")
|
||||
return positional
|
||||
|
||||
def _get_dest_from_flags(
|
||||
self, flags: tuple[str, ...], dest: str | None
|
||||
) -> str | None:
|
||||
def _get_dest_from_flags(self, flags: tuple[str, ...], dest: str | None) -> str:
|
||||
"""Convert flags to a destination name."""
|
||||
if dest:
|
||||
if not dest.replace("_", "").isalnum():
|
||||
|
@ -216,7 +214,7 @@ class CommandArgumentParser:
|
|||
return dest
|
||||
|
||||
def _determine_required(
|
||||
self, required: bool, positional: bool, nargs: int | str
|
||||
self, required: bool, positional: bool, nargs: int | str | None
|
||||
) -> bool:
|
||||
"""Determine if the argument is required."""
|
||||
if required:
|
||||
|
@ -234,7 +232,22 @@ class CommandArgumentParser:
|
|||
|
||||
return required
|
||||
|
||||
def _validate_nargs(self, nargs: int | str) -> int | str:
|
||||
def _validate_nargs(
|
||||
self, nargs: int | str | None, action: ArgumentAction
|
||||
) -> int | str | None:
|
||||
if action in (
|
||||
ArgumentAction.STORE_FALSE,
|
||||
ArgumentAction.STORE_TRUE,
|
||||
ArgumentAction.COUNT,
|
||||
ArgumentAction.HELP,
|
||||
):
|
||||
if nargs is not None:
|
||||
raise CommandArgumentError(
|
||||
f"nargs cannot be specified for {action} actions"
|
||||
)
|
||||
return None
|
||||
if nargs is None:
|
||||
nargs = 1
|
||||
allowed_nargs = ("?", "*", "+")
|
||||
if isinstance(nargs, int):
|
||||
if nargs <= 0:
|
||||
|
@ -246,7 +259,9 @@ class CommandArgumentParser:
|
|||
raise CommandArgumentError(f"nargs must be an int or one of {allowed_nargs}")
|
||||
return nargs
|
||||
|
||||
def _normalize_choices(self, choices: Iterable, expected_type: Any) -> list[Any]:
|
||||
def _normalize_choices(
|
||||
self, choices: Iterable | None, expected_type: Any
|
||||
) -> list[Any]:
|
||||
if choices is not None:
|
||||
if isinstance(choices, dict):
|
||||
raise CommandArgumentError("choices cannot be a dict")
|
||||
|
@ -293,8 +308,34 @@ class CommandArgumentParser:
|
|||
f"Default list value {default!r} for '{dest}' cannot be coerced to {expected_type.__name__}"
|
||||
)
|
||||
|
||||
def _validate_action(
|
||||
self, action: ArgumentAction | str, positional: bool
|
||||
) -> ArgumentAction:
|
||||
if not isinstance(action, ArgumentAction):
|
||||
try:
|
||||
action = ArgumentAction(action)
|
||||
except ValueError:
|
||||
raise CommandArgumentError(
|
||||
f"Invalid action '{action}' is not a valid ArgumentAction"
|
||||
)
|
||||
if action in (
|
||||
ArgumentAction.STORE_TRUE,
|
||||
ArgumentAction.STORE_FALSE,
|
||||
ArgumentAction.COUNT,
|
||||
ArgumentAction.HELP,
|
||||
):
|
||||
if positional:
|
||||
raise CommandArgumentError(
|
||||
f"Action '{action}' cannot be used with positional arguments"
|
||||
)
|
||||
|
||||
return action
|
||||
|
||||
def _resolve_default(
|
||||
self, action: ArgumentAction, default: Any, nargs: str | int
|
||||
self,
|
||||
default: Any,
|
||||
action: ArgumentAction,
|
||||
nargs: str | int | None,
|
||||
) -> Any:
|
||||
"""Get the default value for the argument."""
|
||||
if default is None:
|
||||
|
@ -328,7 +369,18 @@ class CommandArgumentParser:
|
|||
f"Flag '{flag}' must be a single character or start with '--'"
|
||||
)
|
||||
|
||||
def add_argument(self, *flags, **kwargs):
|
||||
def add_argument(
|
||||
self,
|
||||
*flags,
|
||||
action: str | ArgumentAction = "store",
|
||||
nargs: int | str | None = None,
|
||||
default: Any = None,
|
||||
type: Any = str,
|
||||
choices: Iterable | None = None,
|
||||
required: bool = False,
|
||||
help: str = "",
|
||||
dest: str | None = None,
|
||||
) -> None:
|
||||
"""Add an argument to the parser.
|
||||
Args:
|
||||
name or flags: Either a name or prefixed flags (e.g. 'faylx', '-f', '--falyx').
|
||||
|
@ -341,9 +393,10 @@ class CommandArgumentParser:
|
|||
help: A brief description of the argument.
|
||||
dest: The name of the attribute to be added to the object returned by parse_args().
|
||||
"""
|
||||
expected_type = type
|
||||
self._validate_flags(flags)
|
||||
positional = self._is_positional(flags)
|
||||
dest = self._get_dest_from_flags(flags, kwargs.get("dest"))
|
||||
dest = self._get_dest_from_flags(flags, dest)
|
||||
if dest in self._dest_set:
|
||||
raise CommandArgumentError(
|
||||
f"Destination '{dest}' is already defined.\n"
|
||||
|
@ -351,18 +404,9 @@ class CommandArgumentParser:
|
|||
"is not supported. Define a unique 'dest' for each argument."
|
||||
)
|
||||
self._dest_set.add(dest)
|
||||
action = kwargs.get("action", ArgumentAction.STORE)
|
||||
if not isinstance(action, ArgumentAction):
|
||||
try:
|
||||
action = ArgumentAction(action)
|
||||
except ValueError:
|
||||
raise CommandArgumentError(
|
||||
f"Invalid action '{action}' is not a valid ArgumentAction"
|
||||
)
|
||||
flags = list(flags)
|
||||
nargs = self._validate_nargs(kwargs.get("nargs", 1))
|
||||
default = self._resolve_default(action, kwargs.get("default"), nargs)
|
||||
expected_type = kwargs.get("type", str)
|
||||
action = self._validate_action(action, positional)
|
||||
nargs = self._validate_nargs(nargs, action)
|
||||
default = self._resolve_default(default, action, nargs)
|
||||
if (
|
||||
action in (ArgumentAction.STORE, ArgumentAction.APPEND, ArgumentAction.EXTEND)
|
||||
and default is not None
|
||||
|
@ -371,14 +415,12 @@ class CommandArgumentParser:
|
|||
self._validate_default_list_type(default, expected_type, dest)
|
||||
else:
|
||||
self._validate_default_type(default, expected_type, dest)
|
||||
choices = self._normalize_choices(kwargs.get("choices"), expected_type)
|
||||
choices = self._normalize_choices(choices, expected_type)
|
||||
if default is not None and choices and default not in choices:
|
||||
raise CommandArgumentError(
|
||||
f"Default value '{default}' not in allowed choices: {choices}"
|
||||
)
|
||||
required = self._determine_required(
|
||||
kwargs.get("required", False), positional, nargs
|
||||
)
|
||||
required = self._determine_required(required, positional, nargs)
|
||||
argument = Argument(
|
||||
flags=flags,
|
||||
dest=dest,
|
||||
|
@ -387,7 +429,7 @@ class CommandArgumentParser:
|
|||
default=default,
|
||||
choices=choices,
|
||||
required=required,
|
||||
help=kwargs.get("help", ""),
|
||||
help=help,
|
||||
nargs=nargs,
|
||||
positional=positional,
|
||||
)
|
||||
|
@ -430,11 +472,11 @@ class CommandArgumentParser:
|
|||
values = []
|
||||
i = start
|
||||
if isinstance(spec.nargs, int):
|
||||
# assert i + spec.nargs <= len(
|
||||
# args
|
||||
# ), "Not enough arguments provided: shouldn't happen"
|
||||
values = args[i : i + spec.nargs]
|
||||
return values, i + spec.nargs
|
||||
elif spec.nargs is None:
|
||||
values = [args[i]]
|
||||
return values, i + 1
|
||||
elif spec.nargs == "+":
|
||||
if i >= len(args):
|
||||
raise CommandArgumentError(
|
||||
|
@ -479,6 +521,8 @@ class CommandArgumentParser:
|
|||
for next_spec in positional_args[j + 1 :]:
|
||||
if isinstance(next_spec.nargs, int):
|
||||
min_required += next_spec.nargs
|
||||
elif next_spec.nargs is None:
|
||||
min_required += 1
|
||||
elif next_spec.nargs == "+":
|
||||
min_required += 1
|
||||
elif next_spec.nargs == "?":
|
||||
|
@ -521,7 +565,7 @@ class CommandArgumentParser:
|
|||
|
||||
return i
|
||||
|
||||
def parse_args(
|
||||
async def parse_args(
|
||||
self, args: list[str] | None = None, from_validate: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""Parse Falyx Command arguments."""
|
||||
|
@ -669,7 +713,7 @@ class CommandArgumentParser:
|
|||
result.pop("help", None)
|
||||
return result
|
||||
|
||||
def parse_args_split(
|
||||
async def parse_args_split(
|
||||
self, args: list[str], from_validate: bool = False
|
||||
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||
"""
|
||||
|
@ -677,7 +721,7 @@ class CommandArgumentParser:
|
|||
tuple[args, kwargs] - Positional arguments in defined order,
|
||||
followed by keyword argument mapping.
|
||||
"""
|
||||
parsed = self.parse_args(args, from_validate)
|
||||
parsed = await self.parse_args(args, from_validate)
|
||||
args_list = []
|
||||
kwargs_dict = {}
|
||||
for arg in self._arguments:
|
||||
|
|
|
@ -42,7 +42,7 @@ def infer_args_from_func(
|
|||
else:
|
||||
flags = [f"--{name.replace('_', '-')}"]
|
||||
action = "store"
|
||||
nargs: int | str = 1
|
||||
nargs: int | str | None = None
|
||||
|
||||
if arg_type is bool:
|
||||
if param.default is False:
|
||||
|
|
|
@ -271,7 +271,7 @@ async def prompt_for_index(
|
|||
prompt_session: PromptSession | None = None,
|
||||
prompt_message: str = "Select an option > ",
|
||||
show_table: bool = True,
|
||||
):
|
||||
) -> int:
|
||||
prompt_session = prompt_session or PromptSession()
|
||||
console = console or Console(color_system="auto")
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.33"
|
||||
__version__ = "0.1.34"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "falyx"
|
||||
version = "0.1.33"
|
||||
version = "0.1.34"
|
||||
description = "Reliable and introspectable async CLI action framework."
|
||||
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
||||
license = "MIT"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# test_command.py
|
||||
import pytest
|
||||
|
||||
from falyx.action import Action, ActionGroup, BaseIOAction, ChainedAction
|
||||
from falyx.action import Action, BaseIOAction, ChainedAction
|
||||
from falyx.command import Command
|
||||
from falyx.execution_registry import ExecutionRegistry as er
|
||||
from falyx.retry import RetryPolicy
|
||||
|
|
|
@ -5,98 +5,109 @@ from falyx.parsers import ArgumentAction, CommandArgumentParser
|
|||
from falyx.signals import HelpSignal
|
||||
|
||||
|
||||
def build_parser_and_parse(args, config):
|
||||
async def build_parser_and_parse(args, config):
|
||||
cap = CommandArgumentParser()
|
||||
config(cap)
|
||||
return cap.parse_args(args)
|
||||
return await cap.parse_args(args)
|
||||
|
||||
|
||||
def test_none():
|
||||
@pytest.mark.asyncio
|
||||
async def test_none():
|
||||
def config(parser):
|
||||
parser.add_argument("--foo", type=str)
|
||||
|
||||
parsed = build_parser_and_parse(None, config)
|
||||
parsed = await build_parser_and_parse(None, config)
|
||||
assert parsed["foo"] is None
|
||||
|
||||
|
||||
def test_append_multiple_flags():
|
||||
@pytest.mark.asyncio
|
||||
async def test_append_multiple_flags():
|
||||
def config(parser):
|
||||
parser.add_argument("--tag", action=ArgumentAction.APPEND, type=str)
|
||||
|
||||
parsed = build_parser_and_parse(["--tag", "a", "--tag", "b", "--tag", "c"], config)
|
||||
parsed = await build_parser_and_parse(
|
||||
["--tag", "a", "--tag", "b", "--tag", "c"], config
|
||||
)
|
||||
assert parsed["tag"] == ["a", "b", "c"]
|
||||
|
||||
|
||||
def test_positional_nargs_plus_and_single():
|
||||
@pytest.mark.asyncio
|
||||
async def test_positional_nargs_plus_and_single():
|
||||
def config(parser):
|
||||
parser.add_argument("files", nargs="+", type=str)
|
||||
parser.add_argument("mode", nargs=1)
|
||||
|
||||
parsed = build_parser_and_parse(["a", "b", "c", "prod"], config)
|
||||
parsed = await build_parser_and_parse(["a", "b", "c", "prod"], config)
|
||||
assert parsed["files"] == ["a", "b", "c"]
|
||||
assert parsed["mode"] == "prod"
|
||||
|
||||
|
||||
def test_type_validation_failure():
|
||||
@pytest.mark.asyncio
|
||||
async def test_type_validation_failure():
|
||||
def config(parser):
|
||||
parser.add_argument("--count", type=int)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
build_parser_and_parse(["--count", "abc"], config)
|
||||
await build_parser_and_parse(["--count", "abc"], config)
|
||||
|
||||
|
||||
def test_required_field_missing():
|
||||
@pytest.mark.asyncio
|
||||
async def test_required_field_missing():
|
||||
def config(parser):
|
||||
parser.add_argument("--env", type=str, required=True)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
build_parser_and_parse([], config)
|
||||
await build_parser_and_parse([], config)
|
||||
|
||||
|
||||
def test_choices_enforced():
|
||||
@pytest.mark.asyncio
|
||||
async def test_choices_enforced():
|
||||
def config(parser):
|
||||
parser.add_argument("--mode", choices=["dev", "prod"])
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
build_parser_and_parse(["--mode", "staging"], config)
|
||||
await build_parser_and_parse(["--mode", "staging"], config)
|
||||
|
||||
|
||||
def test_boolean_flags():
|
||||
@pytest.mark.asyncio
|
||||
async def test_boolean_flags():
|
||||
def config(parser):
|
||||
parser.add_argument("--debug", action=ArgumentAction.STORE_TRUE)
|
||||
parser.add_argument("--no-debug", action=ArgumentAction.STORE_FALSE)
|
||||
|
||||
parsed = build_parser_and_parse(["--debug", "--no-debug"], config)
|
||||
parsed = await build_parser_and_parse(["--debug", "--no-debug"], config)
|
||||
assert parsed["debug"] is True
|
||||
assert parsed["no_debug"] is False
|
||||
parsed = build_parser_and_parse([], config)
|
||||
print(parsed)
|
||||
parsed = await build_parser_and_parse([], config)
|
||||
assert parsed["debug"] is False
|
||||
assert parsed["no_debug"] is True
|
||||
|
||||
|
||||
def test_count_action():
|
||||
@pytest.mark.asyncio
|
||||
async def test_count_action():
|
||||
def config(parser):
|
||||
parser.add_argument("-v", action=ArgumentAction.COUNT)
|
||||
|
||||
parsed = build_parser_and_parse(["-v", "-v", "-v"], config)
|
||||
parsed = await build_parser_and_parse(["-v", "-v", "-v"], config)
|
||||
assert parsed["v"] == 3
|
||||
|
||||
|
||||
def test_nargs_star():
|
||||
@pytest.mark.asyncio
|
||||
async def test_nargs_star():
|
||||
def config(parser):
|
||||
parser.add_argument("args", nargs="*", type=str)
|
||||
|
||||
parsed = build_parser_and_parse(["one", "two", "three"], config)
|
||||
parsed = await build_parser_and_parse(["one", "two", "three"], config)
|
||||
assert parsed["args"] == ["one", "two", "three"]
|
||||
|
||||
|
||||
def test_flag_and_positional_mix():
|
||||
@pytest.mark.asyncio
|
||||
async def test_flag_and_positional_mix():
|
||||
def config(parser):
|
||||
parser.add_argument("--env", type=str)
|
||||
parser.add_argument("tasks", nargs="+")
|
||||
|
||||
parsed = build_parser_and_parse(["--env", "prod", "build", "test"], config)
|
||||
parsed = await build_parser_and_parse(["--env", "prod", "build", "test"], config)
|
||||
assert parsed["env"] == "prod"
|
||||
assert parsed["tasks"] == ["build", "test"]
|
||||
|
||||
|
@ -134,7 +145,7 @@ def test_add_argument_multiple_optional_flags_same_dest():
|
|||
parser.add_argument("-f", "--falyx")
|
||||
arg = parser._arguments[-1]
|
||||
assert arg.dest == "falyx"
|
||||
assert arg.flags == ["-f", "--falyx"]
|
||||
assert arg.flags == ("-f", "--falyx")
|
||||
|
||||
|
||||
def test_add_argument_flag_dest_conflict():
|
||||
|
@ -165,7 +176,7 @@ def test_add_argument_multiple_flags_custom_dest():
|
|||
parser.add_argument("-f", "--falyx", "--test", dest="falyx")
|
||||
arg = parser._arguments[-1]
|
||||
assert arg.dest == "falyx"
|
||||
assert arg.flags == ["-f", "--falyx", "--test"]
|
||||
assert arg.flags == ("-f", "--falyx", "--test")
|
||||
|
||||
|
||||
def test_add_argument_multiple_flags_dest():
|
||||
|
@ -175,7 +186,7 @@ def test_add_argument_multiple_flags_dest():
|
|||
parser.add_argument("-f", "--falyx", "--test")
|
||||
arg = parser._arguments[-1]
|
||||
assert arg.dest == "falyx"
|
||||
assert arg.flags == ["-f", "--falyx", "--test"]
|
||||
assert arg.flags == ("-f", "--falyx", "--test")
|
||||
|
||||
|
||||
def test_add_argument_single_flag_dest():
|
||||
|
@ -185,7 +196,7 @@ def test_add_argument_single_flag_dest():
|
|||
parser.add_argument("-f")
|
||||
arg = parser._arguments[-1]
|
||||
assert arg.dest == "f"
|
||||
assert arg.flags == ["-f"]
|
||||
assert arg.flags == ("-f",)
|
||||
|
||||
|
||||
def test_add_argument_bad_dest():
|
||||
|
@ -257,7 +268,7 @@ def test_add_argument_default_value():
|
|||
parser.add_argument("--falyx", default="default_value")
|
||||
arg = parser._arguments[-1]
|
||||
assert arg.dest == "falyx"
|
||||
assert arg.flags == ["--falyx"]
|
||||
assert arg.flags == ("--falyx",)
|
||||
assert arg.default == "default_value"
|
||||
|
||||
|
||||
|
@ -297,20 +308,21 @@ def test_add_argument_default_not_in_choices():
|
|||
parser.add_argument("--falyx", choices=["a", "b"], default="c")
|
||||
|
||||
|
||||
def test_add_argument_choices():
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_argument_choices():
|
||||
parser = CommandArgumentParser()
|
||||
|
||||
# ✅ Choices provided
|
||||
parser.add_argument("--falyx", choices=["a", "b", "c"])
|
||||
arg = parser._arguments[-1]
|
||||
assert arg.dest == "falyx"
|
||||
assert arg.flags == ["--falyx"]
|
||||
assert arg.flags == ("--falyx",)
|
||||
assert arg.choices == ["a", "b", "c"]
|
||||
|
||||
args = parser.parse_args(["--falyx", "a"])
|
||||
args = await parser.parse_args(["--falyx", "a"])
|
||||
assert args["falyx"] == "a"
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.parse_args(["--falyx", "d"])
|
||||
await parser.parse_args(["--falyx", "d"])
|
||||
|
||||
|
||||
def test_add_argument_choices_invalid():
|
||||
|
@ -352,7 +364,7 @@ def test_add_argument_nargs():
|
|||
parser.add_argument("--falyx", nargs=2)
|
||||
arg = parser._arguments[-1]
|
||||
assert arg.dest == "falyx"
|
||||
assert arg.flags == ["--falyx"]
|
||||
assert arg.flags == ("--falyx",)
|
||||
assert arg.nargs == 2
|
||||
|
||||
|
||||
|
@ -377,56 +389,60 @@ def test_get_argument():
|
|||
parser.add_argument("--falyx", type=str, default="default_value")
|
||||
arg = parser.get_argument("falyx")
|
||||
assert arg.dest == "falyx"
|
||||
assert arg.flags == ["--falyx"]
|
||||
assert arg.flags == ("--falyx",)
|
||||
assert arg.default == "default_value"
|
||||
|
||||
|
||||
def test_parse_args_nargs():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_nargs():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("files", nargs="+", type=str)
|
||||
parser.add_argument("mode", nargs=1)
|
||||
|
||||
args = parser.parse_args(["a", "b", "c"])
|
||||
args = await parser.parse_args(["a", "b", "c"])
|
||||
|
||||
assert args["files"] == ["a", "b"]
|
||||
assert args["mode"] == "c"
|
||||
|
||||
|
||||
def test_parse_args_nargs_plus():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_nargs_plus():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("files", nargs="+", type=str)
|
||||
|
||||
args = parser.parse_args(["a", "b", "c"])
|
||||
args = await parser.parse_args(["a", "b", "c"])
|
||||
assert args["files"] == ["a", "b", "c"]
|
||||
|
||||
args = parser.parse_args(["a"])
|
||||
args = await parser.parse_args(["a"])
|
||||
assert args["files"] == ["a"]
|
||||
|
||||
|
||||
def test_parse_args_flagged_nargs_plus():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_flagged_nargs_plus():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--files", nargs="+", type=str)
|
||||
|
||||
args = parser.parse_args(["--files", "a", "b", "c"])
|
||||
args = await parser.parse_args(["--files", "a", "b", "c"])
|
||||
assert args["files"] == ["a", "b", "c"]
|
||||
|
||||
args = parser.parse_args(["--files", "a"])
|
||||
args = await parser.parse_args(["--files", "a"])
|
||||
print(args)
|
||||
assert args["files"] == ["a"]
|
||||
|
||||
args = parser.parse_args([])
|
||||
args = await parser.parse_args([])
|
||||
assert args["files"] == []
|
||||
|
||||
|
||||
def test_parse_args_numbered_nargs():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_numbered_nargs():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("files", nargs=2, type=str)
|
||||
|
||||
args = parser.parse_args(["a", "b"])
|
||||
args = await parser.parse_args(["a", "b"])
|
||||
assert args["files"] == ["a", "b"]
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
args = parser.parse_args(["a"])
|
||||
args = await parser.parse_args(["a"])
|
||||
print(args)
|
||||
|
||||
|
||||
|
@ -436,48 +452,53 @@ def test_parse_args_nargs_zero():
|
|||
parser.add_argument("files", nargs=0, type=str)
|
||||
|
||||
|
||||
def test_parse_args_nargs_more_than_expected():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_nargs_more_than_expected():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("files", nargs=2, type=str)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.parse_args(["a", "b", "c", "d"])
|
||||
await parser.parse_args(["a", "b", "c", "d"])
|
||||
|
||||
|
||||
def test_parse_args_nargs_one_or_none():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_nargs_one_or_none():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("files", nargs="?", type=str)
|
||||
|
||||
args = parser.parse_args(["a"])
|
||||
args = await parser.parse_args(["a"])
|
||||
assert args["files"] == "a"
|
||||
|
||||
args = parser.parse_args([])
|
||||
args = await parser.parse_args([])
|
||||
assert args["files"] is None
|
||||
|
||||
|
||||
def test_parse_args_nargs_positional():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_nargs_positional():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("files", nargs="*", type=str)
|
||||
|
||||
args = parser.parse_args(["a", "b", "c"])
|
||||
args = await parser.parse_args(["a", "b", "c"])
|
||||
assert args["files"] == ["a", "b", "c"]
|
||||
|
||||
args = parser.parse_args([])
|
||||
args = await parser.parse_args([])
|
||||
assert args["files"] == []
|
||||
|
||||
|
||||
def test_parse_args_nargs_positional_plus():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_nargs_positional_plus():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("files", nargs="+", type=str)
|
||||
|
||||
args = parser.parse_args(["a", "b", "c"])
|
||||
args = await parser.parse_args(["a", "b", "c"])
|
||||
assert args["files"] == ["a", "b", "c"]
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
args = parser.parse_args([])
|
||||
args = await parser.parse_args([])
|
||||
|
||||
|
||||
def test_parse_args_nargs_multiple_positional():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_nargs_multiple_positional():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("files", nargs="+", type=str)
|
||||
parser.add_argument("mode", nargs=1)
|
||||
|
@ -485,7 +506,7 @@ def test_parse_args_nargs_multiple_positional():
|
|||
parser.add_argument("target", nargs="*")
|
||||
parser.add_argument("extra", nargs="+")
|
||||
|
||||
args = parser.parse_args(["a", "b", "c", "d", "e"])
|
||||
args = await parser.parse_args(["a", "b", "c", "d", "e"])
|
||||
assert args["files"] == ["a", "b", "c"]
|
||||
assert args["mode"] == "d"
|
||||
assert args["action"] == []
|
||||
|
@ -493,186 +514,209 @@ def test_parse_args_nargs_multiple_positional():
|
|||
assert args["extra"] == ["e"]
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.parse_args([])
|
||||
await parser.parse_args([])
|
||||
|
||||
|
||||
def test_parse_args_nargs_invalid_positional_arguments():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_nargs_invalid_positional_arguments():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("numbers", nargs="*", type=int)
|
||||
parser.add_argument("mode", nargs=1)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.parse_args(["1", "2", "c", "d"])
|
||||
await parser.parse_args(["1", "2", "c", "d"])
|
||||
|
||||
|
||||
def test_parse_args_append():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_append():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--numbers", action=ArgumentAction.APPEND, type=int)
|
||||
|
||||
args = parser.parse_args(["--numbers", "1", "--numbers", "2", "--numbers", "3"])
|
||||
args = await parser.parse_args(["--numbers", "1", "--numbers", "2", "--numbers", "3"])
|
||||
assert args["numbers"] == [1, 2, 3]
|
||||
|
||||
args = parser.parse_args(["--numbers", "1"])
|
||||
args = await parser.parse_args(["--numbers", "1"])
|
||||
assert args["numbers"] == [1]
|
||||
|
||||
args = parser.parse_args([])
|
||||
args = await parser.parse_args([])
|
||||
assert args["numbers"] == []
|
||||
|
||||
|
||||
def test_parse_args_nargs_append():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_nargs_append():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("numbers", action=ArgumentAction.APPEND, type=int, nargs="*")
|
||||
parser.add_argument("--mode")
|
||||
|
||||
args = parser.parse_args(["1", "2", "3", "--mode", "numbers", "4", "5"])
|
||||
args = await parser.parse_args(["1", "2", "3", "--mode", "numbers", "4", "5"])
|
||||
assert args["numbers"] == [[1, 2, 3], [4, 5]]
|
||||
|
||||
args = parser.parse_args(["1"])
|
||||
args = await parser.parse_args(["1"])
|
||||
assert args["numbers"] == [[1]]
|
||||
|
||||
args = parser.parse_args([])
|
||||
args = await parser.parse_args([])
|
||||
assert args["numbers"] == []
|
||||
|
||||
|
||||
def test_parse_args_append_flagged_invalid_type():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_append_flagged_invalid_type():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--numbers", action=ArgumentAction.APPEND, type=int)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.parse_args(["--numbers", "a"])
|
||||
await parser.parse_args(["--numbers", "a"])
|
||||
|
||||
|
||||
def test_append_groups_nargs():
|
||||
@pytest.mark.asyncio
|
||||
async def test_append_groups_nargs():
|
||||
cap = CommandArgumentParser()
|
||||
cap.add_argument("--item", action=ArgumentAction.APPEND, type=str, nargs=2)
|
||||
|
||||
parsed = cap.parse_args(["--item", "a", "b", "--item", "c", "d"])
|
||||
parsed = await cap.parse_args(["--item", "a", "b", "--item", "c", "d"])
|
||||
assert parsed["item"] == [["a", "b"], ["c", "d"]]
|
||||
|
||||
|
||||
def test_extend_flattened():
|
||||
@pytest.mark.asyncio
|
||||
async def test_extend_flattened():
|
||||
cap = CommandArgumentParser()
|
||||
cap.add_argument("--value", action=ArgumentAction.EXTEND, type=str)
|
||||
|
||||
parsed = cap.parse_args(["--value", "x", "--value", "y"])
|
||||
parsed = await cap.parse_args(["--value", "x", "--value", "y"])
|
||||
assert parsed["value"] == ["x", "y"]
|
||||
|
||||
|
||||
def test_parse_args_split_order():
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_args_split_order():
|
||||
cap = CommandArgumentParser()
|
||||
cap.add_argument("a")
|
||||
cap.add_argument("--x")
|
||||
cap.add_argument("b", nargs="*")
|
||||
args, kwargs = cap.parse_args_split(["1", "--x", "100", "2"])
|
||||
args, kwargs = await cap.parse_args_split(["1", "--x", "100", "2"])
|
||||
assert args == ("1", ["2"])
|
||||
assert kwargs == {"x": "100"}
|
||||
|
||||
|
||||
def test_help_signal_triggers():
|
||||
@pytest.mark.asyncio
|
||||
async def test_help_signal_triggers():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--foo")
|
||||
with pytest.raises(HelpSignal):
|
||||
parser.parse_args(["--help"])
|
||||
await parser.parse_args(["--help"])
|
||||
|
||||
|
||||
def test_empty_parser_defaults():
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_parser_defaults():
|
||||
parser = CommandArgumentParser()
|
||||
with pytest.raises(HelpSignal):
|
||||
parser.parse_args(["--help"])
|
||||
await parser.parse_args(["--help"])
|
||||
|
||||
|
||||
def test_extend_basic():
|
||||
@pytest.mark.asyncio
|
||||
async def test_extend_basic():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--tag", action=ArgumentAction.EXTEND, type=str)
|
||||
|
||||
args = parser.parse_args(["--tag", "a", "--tag", "b", "--tag", "c"])
|
||||
args = await parser.parse_args(["--tag", "a", "--tag", "b", "--tag", "c"])
|
||||
assert args["tag"] == ["a", "b", "c"]
|
||||
|
||||
|
||||
def test_extend_nargs_2():
|
||||
@pytest.mark.asyncio
|
||||
async def test_extend_nargs_2():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--pair", action=ArgumentAction.EXTEND, type=str, nargs=2)
|
||||
|
||||
args = parser.parse_args(["--pair", "a", "b", "--pair", "c", "d"])
|
||||
args = await parser.parse_args(["--pair", "a", "b", "--pair", "c", "d"])
|
||||
assert args["pair"] == ["a", "b", "c", "d"]
|
||||
|
||||
|
||||
def test_extend_nargs_star():
|
||||
@pytest.mark.asyncio
|
||||
async def test_extend_nargs_star():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--files", action=ArgumentAction.EXTEND, type=str, nargs="*")
|
||||
|
||||
args = parser.parse_args(["--files", "x", "y", "z"])
|
||||
args = await parser.parse_args(["--files", "x", "y", "z"])
|
||||
assert args["files"] == ["x", "y", "z"]
|
||||
|
||||
args = parser.parse_args(["--files"])
|
||||
args = await parser.parse_args(["--files"])
|
||||
assert args["files"] == []
|
||||
|
||||
|
||||
def test_extend_nargs_plus():
|
||||
@pytest.mark.asyncio
|
||||
async def test_extend_nargs_plus():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--inputs", action=ArgumentAction.EXTEND, type=int, nargs="+")
|
||||
|
||||
args = parser.parse_args(["--inputs", "1", "2", "3", "--inputs", "4"])
|
||||
args = await parser.parse_args(["--inputs", "1", "2", "3", "--inputs", "4"])
|
||||
assert args["inputs"] == [1, 2, 3, 4]
|
||||
|
||||
|
||||
def test_extend_invalid_type():
|
||||
@pytest.mark.asyncio
|
||||
async def test_extend_invalid_type():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--nums", action=ArgumentAction.EXTEND, type=int)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.parse_args(["--nums", "a"])
|
||||
await parser.parse_args(["--nums", "a"])
|
||||
|
||||
|
||||
def test_greedy_invalid_type():
|
||||
@pytest.mark.asyncio
|
||||
async def test_greedy_invalid_type():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--nums", nargs="*", type=int)
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.parse_args(["--nums", "a"])
|
||||
await parser.parse_args(["--nums", "a"])
|
||||
|
||||
|
||||
def test_append_vs_extend_behavior():
|
||||
@pytest.mark.asyncio
|
||||
async def test_append_vs_extend_behavior():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--x", action=ArgumentAction.APPEND, nargs=2)
|
||||
parser.add_argument("--y", action=ArgumentAction.EXTEND, nargs=2)
|
||||
|
||||
args = parser.parse_args(
|
||||
args = await parser.parse_args(
|
||||
["--x", "a", "b", "--x", "c", "d", "--y", "1", "2", "--y", "3", "4"]
|
||||
)
|
||||
assert args["x"] == [["a", "b"], ["c", "d"]]
|
||||
assert args["y"] == ["1", "2", "3", "4"]
|
||||
|
||||
|
||||
def test_append_vs_extend_behavior_error():
|
||||
@pytest.mark.asyncio
|
||||
async def test_append_vs_extend_behavior_error():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--x", action=ArgumentAction.APPEND, nargs=2)
|
||||
parser.add_argument("--y", action=ArgumentAction.EXTEND, nargs=2)
|
||||
|
||||
# This should raise an error because the last argument is not a valid pair
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.parse_args(["--x", "a", "b", "--x", "c", "d", "--y", "1", "2", "--y", "3"])
|
||||
await parser.parse_args(
|
||||
["--x", "a", "b", "--x", "c", "d", "--y", "1", "2", "--y", "3"]
|
||||
)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.parse_args(["--x", "a", "b", "--x", "c", "--y", "1", "--y", "3", "4"])
|
||||
await parser.parse_args(
|
||||
["--x", "a", "b", "--x", "c", "--y", "1", "--y", "3", "4"]
|
||||
)
|
||||
|
||||
|
||||
def test_extend_positional():
|
||||
@pytest.mark.asyncio
|
||||
async def test_extend_positional():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("files", action=ArgumentAction.EXTEND, type=str, nargs="*")
|
||||
|
||||
args = parser.parse_args(["a", "b", "c"])
|
||||
args = await parser.parse_args(["a", "b", "c"])
|
||||
assert args["files"] == ["a", "b", "c"]
|
||||
|
||||
args = parser.parse_args([])
|
||||
args = await parser.parse_args([])
|
||||
assert args["files"] == []
|
||||
|
||||
|
||||
def test_extend_positional_nargs():
|
||||
@pytest.mark.asyncio
|
||||
async def test_extend_positional_nargs():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("files", action=ArgumentAction.EXTEND, type=str, nargs="+")
|
||||
|
||||
args = parser.parse_args(["a", "b", "c"])
|
||||
args = await parser.parse_args(["a", "b", "c"])
|
||||
assert args["files"] == ["a", "b", "c"]
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.parse_args([])
|
||||
await parser.parse_args([])
|
||||
|
|
Loading…
Reference in New Issue