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