feat(core): advance options/state handling and workflow execution integration
- extend OptionsManager to support multi-namespace option resolution and toggling - integrate OptionsManager more deeply across Action, ChainedAction, and ActionGroup - propagate shared runtime configuration through execution layers - refine action composition model (sequential + parallel execution semantics) - improve lifecycle consistency across BaseAction, Action, ChainedAction, and ActionGroup - begin aligning execution flow with centralized context and options handling wip: routing and root option parsing behavior still in progress
This commit is contained in:
100
tests/test_actions/test_load_file_action.py
Normal file
100
tests/test_actions/test_load_file_action.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import pytest
|
||||
from rich.text import Text
|
||||
|
||||
from falyx.action import LoadFileAction
|
||||
from falyx.console import console as falyx_console
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_json_file_action(tmp_path):
|
||||
mock_data = '{"key": "value"}'
|
||||
file = tmp_path / "test.json"
|
||||
file.write_text(mock_data)
|
||||
action = LoadFileAction(name="load-file", file_path=file, file_type="json")
|
||||
result = await action()
|
||||
assert result == {"key": "value"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_yaml_file_action(tmp_path):
|
||||
mock_data = "key: value"
|
||||
file = tmp_path / "test.yaml"
|
||||
file.write_text(mock_data)
|
||||
action = LoadFileAction(name="load-file", file_path=file, file_type="yaml")
|
||||
result = await action()
|
||||
assert result == {"key": "value"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_toml_file_action(tmp_path):
|
||||
mock_data = 'key = "value"'
|
||||
file = tmp_path / "test.toml"
|
||||
file.write_text(mock_data)
|
||||
action = LoadFileAction(name="load-file", file_path=file, file_type="toml")
|
||||
result = await action()
|
||||
assert result == {"key": "value"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_csv_file_action(tmp_path):
|
||||
mock_data = "key,value\nfoo,bar"
|
||||
file = tmp_path / "test.csv"
|
||||
file.write_text(mock_data)
|
||||
action = LoadFileAction(name="load-file", file_path=file, file_type="csv")
|
||||
result = await action()
|
||||
print(result)
|
||||
assert result == [["key", "value"], ["foo", "bar"]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_tsv_file_action(tmp_path):
|
||||
mock_data = "key\tvalue\nfoo\tbar"
|
||||
file = tmp_path / "test.tsv"
|
||||
file.write_text(mock_data)
|
||||
action = LoadFileAction(name="load-file", file_path=file, file_type="tsv")
|
||||
result = await action()
|
||||
assert result == [["key", "value"], ["foo", "bar"]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_file_action_invalid_path():
|
||||
action = LoadFileAction(
|
||||
name="load-file", file_path="non_existent_file.json", file_type="json"
|
||||
)
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await action()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_file_action_invalid_json(tmp_path):
|
||||
invalid_json = '{"key": "value"' # Missing closing brace
|
||||
file = tmp_path / "invalid.json"
|
||||
file.write_text(invalid_json)
|
||||
action = LoadFileAction(name="load-file", file_path=file, file_type="json")
|
||||
with pytest.raises(ValueError):
|
||||
await action()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_file_action_unsupported_type(tmp_path):
|
||||
file = tmp_path / "test.txt"
|
||||
file.write_text("Just some text")
|
||||
with pytest.raises(ValueError):
|
||||
LoadFileAction(name="load-file", file_path=file, file_type="unsupported")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_of_load_file_action(tmp_path):
|
||||
mock_data = '{"key": "value"}'
|
||||
file = tmp_path / "test.json"
|
||||
file.write_text(mock_data)
|
||||
action = LoadFileAction(name="load-file", file_path=file, file_type="json")
|
||||
with falyx_console.capture() as capture:
|
||||
await action.preview()
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
assert "LoadFileAction" in captured
|
||||
assert "test.json" in captured
|
||||
assert "load-file" in captured
|
||||
assert "JSON" in captured
|
||||
assert "key" in captured
|
||||
assert "value" in captured
|
||||
@@ -1,5 +1,6 @@
|
||||
# test_command.py
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from falyx.action import Action, BaseIOAction, ChainedAction
|
||||
from falyx.command import Command
|
||||
@@ -172,3 +173,15 @@ def test_command_bad_action():
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
Command(key="TEST", description="Test Command", action="not_callable")
|
||||
assert str(exc_info.value) == "Action must be a callable or an instance of BaseAction"
|
||||
|
||||
|
||||
def test_command_bad_options_manager():
|
||||
"""Test if Command raises an exception when options_manager is not a dict or callable."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Command(
|
||||
key="TEST",
|
||||
description="Test Command",
|
||||
action=dummy_action,
|
||||
options_manager="not_a_dict_or_callable",
|
||||
)
|
||||
assert "Input should be an instance of OptionsManager" in str(exc_info.value)
|
||||
|
||||
@@ -118,6 +118,19 @@ def test_get_completions_namespace_boundary_suggests_help_flags(falyx):
|
||||
results = list(completer.get_completions(Document("OPS -"), None))
|
||||
texts = completion_texts(results)
|
||||
|
||||
assert "-h" in texts
|
||||
assert "--help" in texts
|
||||
assert "-T" not in texts
|
||||
assert "--tldr" not in texts
|
||||
|
||||
falyx.add_tldr_example(
|
||||
entry_key="R",
|
||||
usage="",
|
||||
description="This is a TLDR example for the R command.",
|
||||
)
|
||||
results = list(completer.get_completions(Document("-"), None))
|
||||
texts = completion_texts(results)
|
||||
|
||||
assert "-h" in texts
|
||||
assert "--help" in texts
|
||||
assert "-T" in texts
|
||||
@@ -247,3 +260,46 @@ def test_ensure_quote_wraps_whitespace(falyx):
|
||||
|
||||
assert completer._ensure_quote("hello world") == '"hello world"'
|
||||
assert completer._ensure_quote("hello") == "hello"
|
||||
|
||||
|
||||
def test_command_suggestions_are_case_insensitive(falyx):
|
||||
completer = FalyxCompleter(falyx)
|
||||
|
||||
results = list(completer.get_completions(Document("r"), None))
|
||||
texts = completion_texts(results)
|
||||
|
||||
assert "r" in texts
|
||||
assert "run" in texts
|
||||
|
||||
results = list(completer.get_completions(Document("R"), None))
|
||||
texts = completion_texts(results)
|
||||
|
||||
assert "R" in texts
|
||||
assert "RUN" in texts
|
||||
|
||||
|
||||
def test_namespace_suggestions_are_case_insensitive(falyx):
|
||||
completer = FalyxCompleter(falyx)
|
||||
|
||||
results = list(completer.get_completions(Document("op"), None))
|
||||
texts = completion_texts(results)
|
||||
|
||||
assert "ops" in texts
|
||||
assert "operations" in texts
|
||||
|
||||
results = list(completer.get_completions(Document("OP"), None))
|
||||
texts = completion_texts(results)
|
||||
|
||||
assert "OPS" in texts
|
||||
assert "OPERATIONS" in texts
|
||||
|
||||
|
||||
def test_command_completions_after_namespace(falyx):
|
||||
completer = FalyxCompleter(falyx)
|
||||
|
||||
results = list(completer.get_completions(Document("OPS D --"), None))
|
||||
texts = completion_texts(results)
|
||||
|
||||
assert "--target" in texts
|
||||
assert "--region" in texts
|
||||
assert "--help" in texts
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from rich.text import Text
|
||||
|
||||
from falyx import Falyx
|
||||
from falyx.console import console
|
||||
from falyx.exceptions import CommandArgumentError
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -82,17 +82,14 @@ async def test_help_command_by_tag(capsys):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_help_command_empty_tags(capsys):
|
||||
async def test_help_command_bad_argument(capsys):
|
||||
flx = Falyx()
|
||||
|
||||
async def untagged_command(falyx: Falyx):
|
||||
pass
|
||||
|
||||
flx.add_command(
|
||||
"U", "Untagged Command", untagged_command, help_text="This command has no tags."
|
||||
)
|
||||
await flx.execute_command("H nonexistent_tag")
|
||||
|
||||
captured = capsys.readouterr()
|
||||
text = Text.from_ansi(captured.out)
|
||||
assert "Unexpected positional argument: nonexistent_tag" in text.plain
|
||||
flx.add_command("U", "Untagged Command", untagged_command)
|
||||
with pytest.raises(
|
||||
CommandArgumentError, match="Unexpected positional argument: nonexistent_tag"
|
||||
):
|
||||
await flx.execute_command("H nonexistent_tag")
|
||||
|
||||
0
tests/test_falyx/test_routing.py
Normal file
0
tests/test_falyx/test_routing.py
Normal file
@@ -1,8 +1,72 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from rich.text import Text
|
||||
|
||||
from falyx import Falyx
|
||||
from falyx.console import console as falyx_console
|
||||
from falyx.exceptions import FalyxError
|
||||
from falyx.parser import ParseResult
|
||||
from falyx.signals import BackSignal, CancelSignal, FlowSignal, HelpSignal, QuitSignal
|
||||
|
||||
|
||||
async def throw_error_action(error: str):
|
||||
if error == "QuitSignal":
|
||||
raise QuitSignal("Quit signal triggered.")
|
||||
elif error == "BackSignal":
|
||||
raise BackSignal("Back signal triggered.")
|
||||
elif error == "CancelSignal":
|
||||
raise CancelSignal("Cancel signal triggered.")
|
||||
elif error == "ValueError":
|
||||
raise ValueError("This is a ValueError.")
|
||||
elif error == "HelpSignal":
|
||||
raise HelpSignal("Help signal triggered.")
|
||||
elif error == "FalyxError":
|
||||
raise FalyxError("This is a FalyxError.")
|
||||
elif error == "FlowSignal":
|
||||
raise FlowSignal("Flow signal triggered.")
|
||||
else:
|
||||
raise asyncio.CancelledError("An error occurred in the action.")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flx() -> Falyx:
|
||||
sys.argv = ["falyx", "T"]
|
||||
flx = Falyx()
|
||||
flx.add_command(
|
||||
"T",
|
||||
"Test",
|
||||
action=lambda: "hello",
|
||||
)
|
||||
flx.add_tldr_example(
|
||||
entry_key="T",
|
||||
usage="",
|
||||
description="This is a TLDR example for the T command.",
|
||||
)
|
||||
return flx
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flx_with_submenu() -> Falyx:
|
||||
flx = Falyx()
|
||||
submenu = Falyx("Submenu")
|
||||
submenu.add_command(
|
||||
"T",
|
||||
"Test",
|
||||
action=lambda: "hello from submenu",
|
||||
)
|
||||
submenu.add_tldr_example(
|
||||
entry_key="T",
|
||||
usage="",
|
||||
description="This is a TLDR example for the T command in the submenu.",
|
||||
)
|
||||
flx.add_submenu(
|
||||
"S",
|
||||
"Submenu",
|
||||
submenu=submenu,
|
||||
)
|
||||
return flx
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -14,3 +78,178 @@ async def test_run_basic(capsys):
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Show this help menu." in captured.out
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_default_to_menu(flx):
|
||||
sys.argv = ["falyx", "T"]
|
||||
flx.default_to_menu = False
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
await flx.run()
|
||||
|
||||
await flx.run(always_start_menu=True)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_default_to_menu_help(flx):
|
||||
sys.argv = ["falyx"]
|
||||
flx.default_to_menu = False
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
with falyx_console.capture() as capture:
|
||||
await flx.run()
|
||||
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
assert "Show this help menu." in captured
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_debug_hooks(flx):
|
||||
sys.argv = ["falyx", "--debug-hooks", "T"]
|
||||
|
||||
assert flx.options.get("debug_hooks") is False
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
await flx.run()
|
||||
|
||||
assert flx.options.get("debug_hooks") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_never_prompt(flx):
|
||||
sys.argv = ["falyx", "--never-prompt", "T"]
|
||||
|
||||
assert flx.options.get("never_prompt") is False
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
await flx.run()
|
||||
|
||||
falyx_console.print(flx.options.get_namespace_dict("default"))
|
||||
|
||||
assert flx.options.get("debug_hooks") is False
|
||||
assert flx.options.get("never_prompt") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_bad_args(flx):
|
||||
sys.argv = ["falyx", "T", "--unknown-arg"]
|
||||
|
||||
with pytest.raises(SystemExit, match="2"):
|
||||
await flx.run()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_help(flx):
|
||||
sys.argv = ["falyx", "T", "--help"]
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "--help"]
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "-h"]
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "--tldr"]
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "-T"]
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
await flx.run()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_entry_not_found(flx):
|
||||
sys.argv = ["falyx", "UNKNOWN_COMMAND"]
|
||||
|
||||
with pytest.raises(SystemExit, match="2"):
|
||||
await flx.run()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_test_exceptions(flx):
|
||||
flx.add_command(
|
||||
"E",
|
||||
"Throw Error",
|
||||
action=throw_error_action,
|
||||
)
|
||||
|
||||
sys.argv = ["falyx", "E", "ValueError"]
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "E", "QuitSignal"]
|
||||
with pytest.raises(SystemExit, match="130"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "E", "BackSignal"]
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "E", "CancelSignal"]
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "E", "HelpSignal"]
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "E", "FlowSignal"]
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "--verbose", "E", "FalyxError"]
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await flx.run()
|
||||
|
||||
sys.argv = ["falyx", "E", "UnknownError"]
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await flx.run()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_no_args(flx):
|
||||
sys.argv = ["falyx"]
|
||||
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
await flx.run()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_submenu(flx_with_submenu):
|
||||
sys.argv = ["falyx", "S", "T"]
|
||||
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
await flx_with_submenu.run()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_submenu_help(flx_with_submenu):
|
||||
sys.argv = ["falyx", "S", "--help"]
|
||||
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
await flx_with_submenu.run()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_submenu_tldr(flx_with_submenu):
|
||||
sys.argv = ["falyx", "S", "--tldr"]
|
||||
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
await flx_with_submenu.run()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_preview(flx):
|
||||
sys.argv = ["falyx", "preview", "T"]
|
||||
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
with falyx_console.capture() as capture:
|
||||
await flx.run()
|
||||
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
assert "Command: 'T'" in captured
|
||||
assert "Would call: <lambda>(args=(), kwargs={})" in captured
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
from falyx.parser.falyx_parser import FalyxParser, RootOptions
|
||||
|
||||
|
||||
def get_falyx_parser():
|
||||
return FalyxParser()
|
||||
|
||||
|
||||
def test_parse_root_options_empty():
|
||||
parser = get_falyx_parser()
|
||||
opts, remaining = parser._parse_root_options([])
|
||||
assert opts == RootOptions()
|
||||
assert remaining == []
|
||||
|
||||
|
||||
def test_parse_root_options_consumes_known_leading_flags():
|
||||
parser = get_falyx_parser()
|
||||
opts, remaining = parser._parse_root_options(
|
||||
["--verbose", "--never-prompt", "deploy", "--env", "prod"]
|
||||
)
|
||||
assert opts.verbose is True
|
||||
assert opts.never_prompt is True
|
||||
assert remaining == ["deploy", "--env", "prod"]
|
||||
|
||||
|
||||
def test_parse_root_options_stops_at_first_non_root_token():
|
||||
parser = get_falyx_parser()
|
||||
opts, remaining = parser._parse_root_options(["deploy", "--verbose"])
|
||||
assert opts == RootOptions()
|
||||
assert remaining == ["deploy", "--verbose"]
|
||||
|
||||
|
||||
def test_parse_root_options_supports_help():
|
||||
parser = get_falyx_parser()
|
||||
opts, remaining = parser._parse_root_options(["--help"])
|
||||
assert opts.help is True
|
||||
assert remaining == []
|
||||
|
||||
|
||||
def test_parse_root_options_supports_double_dash_separator():
|
||||
parser = get_falyx_parser()
|
||||
opts, remaining = parser._parse_root_options(
|
||||
["--verbose", "--", "deploy", "--verbose"]
|
||||
)
|
||||
assert opts.verbose is True
|
||||
assert remaining == ["deploy", "--verbose"]
|
||||
@@ -1,8 +1,10 @@
|
||||
import pytest
|
||||
from rich.text import Text
|
||||
|
||||
from falyx.action import Action
|
||||
from falyx.console import console as falyx_console
|
||||
from falyx.exceptions import CommandArgumentError
|
||||
from falyx.exceptions import CommandArgumentError, NotAFalyxError
|
||||
from falyx.options_manager import OptionsManager
|
||||
from falyx.parser import ArgumentAction, CommandArgumentParser
|
||||
from falyx.signals import HelpSignal
|
||||
|
||||
@@ -835,3 +837,175 @@ async def test_render_help():
|
||||
assert "Foo help" in output
|
||||
assert "--bar" in output
|
||||
assert "Bar help" in output
|
||||
|
||||
|
||||
def test_command_argument_parser_set_options_manager_invalid():
|
||||
parser = CommandArgumentParser()
|
||||
|
||||
with pytest.raises(NotAFalyxError):
|
||||
parser.set_options_manager("not_a_options_manager")
|
||||
|
||||
with pytest.raises(NotAFalyxError):
|
||||
parser.set_options_manager(123)
|
||||
|
||||
with pytest.raises(NotAFalyxError):
|
||||
parser.set_options_manager(None)
|
||||
|
||||
|
||||
def test_command_argument_parser_set_options_manager_valid():
|
||||
parser = CommandArgumentParser()
|
||||
options_manager = OptionsManager([("new_namespace", {"foo": "bar"})])
|
||||
parser.set_options_manager(options_manager)
|
||||
assert parser.options_manager == options_manager
|
||||
assert parser.options_manager.get("foo", namespace_name="new_namespace") == "bar"
|
||||
|
||||
|
||||
def test_add_argument_invalid_required():
|
||||
parser = CommandArgumentParser()
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action=ArgumentAction.STORE_TRUE, required=True)
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action=ArgumentAction.STORE_FALSE, required=True)
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument(
|
||||
"--foo", action=ArgumentAction.STORE_BOOL_OPTIONAL, required=True
|
||||
)
|
||||
|
||||
|
||||
def test_add_argument_invalid_choices():
|
||||
parser = CommandArgumentParser()
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action="store_true", choices="not_a_list")
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", choices=123)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", choices={"a": 1, "b": 2})
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", choices=["a", "b"], type=int)
|
||||
|
||||
|
||||
def test_add_argument_resolver_invalid():
|
||||
parser = CommandArgumentParser()
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", resolver=lambda x: x)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", resolver=123)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action="action", resolver="not_a_function")
|
||||
|
||||
|
||||
def test_add_argument_resolver_valid():
|
||||
parser = CommandArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
"--foo", action="action", resolver=Action("test", lambda x: x.upper())
|
||||
)
|
||||
|
||||
|
||||
def test_add_argument_resolve_invalid_default():
|
||||
parser = CommandArgumentParser()
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action="store_true", default="any value")
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action="store_false", default=False)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action="store_true", default=True)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action="store_bool_optional", default=False)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action="count", default=500)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action="append", default="not a list")
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--foo", action="extend", default="not a list")
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument("--count", action="count", default=0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_argument_resolve_valid_default():
|
||||
parser = CommandArgumentParser()
|
||||
|
||||
parser.add_argument("--foo", action="store_true", default=False)
|
||||
|
||||
parser.add_argument("--bar", action="store_false", default=True)
|
||||
|
||||
parser.add_argument("--baz", action="store_bool_optional", default=None)
|
||||
|
||||
parser.add_argument("--items", action="append", default=[])
|
||||
|
||||
parser.add_argument("--values", action="extend", default=[])
|
||||
|
||||
parser.add_argument("--number", action="store", nargs=1, type=int, default=0)
|
||||
|
||||
result = await parser.parse_args(["--number", "5"])
|
||||
|
||||
assert result["foo"] is False
|
||||
assert result["bar"] is True
|
||||
assert result["baz"] is None
|
||||
assert result["items"] == []
|
||||
assert result["values"] == []
|
||||
assert result["number"] == 5
|
||||
|
||||
|
||||
def test_add_argument_in_reserved_dests():
|
||||
parser = CommandArgumentParser()
|
||||
|
||||
with pytest.raises(
|
||||
CommandArgumentError,
|
||||
match="invalid dest .*'help' is reserved and cannot be used.",
|
||||
):
|
||||
parser.add_argument("--help")
|
||||
|
||||
with pytest.raises(
|
||||
CommandArgumentError,
|
||||
match="invalid dest .*'tldr' is reserved and cannot be used.",
|
||||
):
|
||||
parser.add_argument("--tldr")
|
||||
|
||||
|
||||
def test_add_argument_in_reserved_dests_positional():
|
||||
parser = CommandArgumentParser()
|
||||
|
||||
with pytest.raises(
|
||||
CommandArgumentError,
|
||||
match="invalid dest .*'help' is reserved and cannot be used.",
|
||||
):
|
||||
parser.add_argument("help")
|
||||
|
||||
with pytest.raises(
|
||||
CommandArgumentError,
|
||||
match="invalid dest .*'tldr' is reserved and cannot be used.",
|
||||
):
|
||||
parser.add_argument("tldr")
|
||||
|
||||
|
||||
def test_add_argument_invalid_suggestions():
|
||||
parser = CommandArgumentParser()
|
||||
|
||||
with pytest.raises(
|
||||
CommandArgumentError, match="suggestions must be a list or None, got int"
|
||||
):
|
||||
parser.add_argument("--valid", suggestions=112445)
|
||||
|
||||
|
||||
def test_add_argument_invalid_lazy_resolver():
|
||||
parser = CommandArgumentParser()
|
||||
|
||||
with pytest.raises(
|
||||
CommandArgumentError, match="lazy_resolver must be a boolean, got int"
|
||||
):
|
||||
parser.add_argument("--valid", lazy_resolver=123)
|
||||
@@ -31,6 +31,21 @@ def test_enable_execution_options_registers_retry_flags():
|
||||
assert "retry_backoff" in parser._execution_dests
|
||||
|
||||
|
||||
def test_enable_execution_options_invalid_double_registration_raises():
|
||||
parser = CommandArgumentParser()
|
||||
parser.enable_execution_options(frozenset({ExecutionOption.SUMMARY}))
|
||||
with pytest.raises(
|
||||
CommandArgumentError, match="destination 'summary' is already defined"
|
||||
):
|
||||
parser.enable_execution_options(frozenset({ExecutionOption.SUMMARY}))
|
||||
|
||||
with pytest.raises(
|
||||
CommandArgumentError,
|
||||
match="destination 'summary' is already registered as an execution argument",
|
||||
):
|
||||
parser._register_execution_dest("summary")
|
||||
|
||||
|
||||
def test_enable_execution_options_registers_confirm_flags():
|
||||
parser = CommandArgumentParser()
|
||||
parser.enable_execution_options(frozenset({ExecutionOption.CONFIRM}))
|
||||
@@ -48,12 +63,12 @@ def test_register_execution_dest_rejects_duplicates():
|
||||
parser = CommandArgumentParser()
|
||||
parser.enable_execution_options(frozenset({ExecutionOption.SUMMARY}))
|
||||
with pytest.raises(
|
||||
CommandArgumentError, match="Destination 'summary' is already defined"
|
||||
CommandArgumentError, match="destination 'summary' is already defined"
|
||||
):
|
||||
parser.add_argument("--summary", action="store_true")
|
||||
|
||||
with pytest.raises(
|
||||
CommandArgumentError, match="Destination 'summary' is already defined"
|
||||
CommandArgumentError, match="destination 'summary' is already defined"
|
||||
):
|
||||
parser.enable_execution_options(frozenset({ExecutionOption.SUMMARY}))
|
||||
|
||||
@@ -138,6 +153,6 @@ async def test_parse_args_split_with_conflicting_execution_option_raises():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_argument("--summary", action="store_true", help="A conflicting argument.")
|
||||
with pytest.raises(
|
||||
CommandArgumentError, match="Destination 'summary' is already defined"
|
||||
CommandArgumentError, match="destination 'summary' is already defined"
|
||||
):
|
||||
parser.enable_execution_options(frozenset({ExecutionOption.SUMMARY}))
|
||||
|
||||
96
tests/test_parsers/test_group_builder.py
Normal file
96
tests/test_parsers/test_group_builder.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import pytest
|
||||
|
||||
from falyx.exceptions import CommandArgumentError
|
||||
from falyx.parser import CommandArgumentParser
|
||||
from falyx.parser.command_argument_parser import _GroupBuilder
|
||||
|
||||
|
||||
def test_group_builder():
|
||||
parser = CommandArgumentParser(program="test_program")
|
||||
group_builder = _GroupBuilder(parser, group_name="test_group")
|
||||
assert group_builder.group_name == "test_group"
|
||||
assert "group='test_group'" in str(group_builder)
|
||||
|
||||
group_builder = _GroupBuilder(
|
||||
parser,
|
||||
mutex_name="test_group",
|
||||
)
|
||||
assert group_builder.mutex_name == "test_group"
|
||||
assert "mutex_group='test_group'" in str(group_builder)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
_GroupBuilder(parser, group_name="test_group", mutex_name="test_group")
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
_GroupBuilder(parser)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
builder = _GroupBuilder(parser, group_name="test_group")
|
||||
builder.group_name = None
|
||||
builder.mutex_name = None
|
||||
str(builder)
|
||||
|
||||
|
||||
def test_adding_arguments_to_group():
|
||||
parser = CommandArgumentParser(program="test_program")
|
||||
|
||||
group = parser.add_argument_group("test_group")
|
||||
assert group.group_name == "test_group"
|
||||
|
||||
group.add_argument("--foo", type=str, help="Foo argument")
|
||||
group.add_argument("--bar", type=int, help="Bar argument")
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument_group("test_group")
|
||||
|
||||
|
||||
def test_adding_arguments_to_mutex_group():
|
||||
parser = CommandArgumentParser(program="test_program")
|
||||
|
||||
mutex_group = parser.add_mutually_exclusive_group("test_mutex_group")
|
||||
assert mutex_group.mutex_name == "test_mutex_group"
|
||||
|
||||
mutex_group.add_argument("--foo", type=str, help="Foo argument")
|
||||
mutex_group.add_argument("--bar", type=int, help="Bar argument")
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_mutually_exclusive_group("test_mutex_group")
|
||||
|
||||
|
||||
def test_adding_arguments_to_group_with_invalid_group():
|
||||
parser = CommandArgumentParser(program="test_program")
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument(
|
||||
"--foo", type=str, help="Foo argument", group="non_existent_group"
|
||||
)
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
parser.add_argument(
|
||||
"--bar", type=int, help="Bar argument", mutex_group="non_existent_group"
|
||||
)
|
||||
|
||||
|
||||
def test_adding_positional_arguments_to_mutex_group():
|
||||
parser = CommandArgumentParser(program="test_program")
|
||||
|
||||
group = parser.add_mutually_exclusive_group("test_group")
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
group.add_argument(
|
||||
"positional_arg", type=str, help="This should fail because it's positional"
|
||||
)
|
||||
|
||||
|
||||
def test_adding_required_arguments_to_mutex_group():
|
||||
parser = CommandArgumentParser(program="test_program")
|
||||
|
||||
group = parser.add_mutually_exclusive_group("test_group")
|
||||
|
||||
with pytest.raises(CommandArgumentError):
|
||||
group.add_argument(
|
||||
"--foo",
|
||||
type=str,
|
||||
help="This should fail because it's required",
|
||||
required=True,
|
||||
)
|
||||
@@ -69,14 +69,14 @@ async def test_resolve_args_raises_on_conflicting_execution_option():
|
||||
execution_options=["summary"],
|
||||
)
|
||||
with pytest.raises(
|
||||
CommandArgumentError, match="Destination 'summary' is already defined"
|
||||
CommandArgumentError, match="destination 'summary' is already defined"
|
||||
):
|
||||
command.arg_parser.add_argument(
|
||||
"--summary", action="store_true", help="A conflicting argument."
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
CommandArgumentError, match="Destination 'summary' is already defined"
|
||||
CommandArgumentError, match="destination 'summary' is already defined"
|
||||
):
|
||||
command.arg_parser.enable_execution_options(frozenset({ExecutionOption.SUMMARY}))
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import pytest
|
||||
|
||||
from falyx.exceptions import CommandArgumentError
|
||||
from falyx.parser.command_argument_parser import CommandArgumentParser
|
||||
from falyx.parser.parser_types import TLDRExample
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -45,3 +46,27 @@ async def test_add_tldr_examples_in_init():
|
||||
assert parser._tldr_examples[0].description == "This is the first example."
|
||||
assert parser._tldr_examples[1].usage == "example2"
|
||||
assert parser._tldr_examples[1].description == "This is the second example."
|
||||
|
||||
|
||||
def test_add_tldr_example():
|
||||
parser = CommandArgumentParser()
|
||||
parser.add_tldr_example("example1", "This is the first example.")
|
||||
assert len(parser._tldr_examples) == 1
|
||||
assert parser._tldr_examples[0].usage == "example1"
|
||||
assert parser._tldr_examples[0].description == "This is the first example."
|
||||
|
||||
|
||||
def test_add_tldr_example_bad_args():
|
||||
parser = CommandArgumentParser()
|
||||
with pytest.raises(TypeError):
|
||||
parser.add_tldr_example("example1", "This is the first example.", "extra_arg")
|
||||
|
||||
|
||||
def test_add_tldr_examples_with_tldr_example_objects():
|
||||
parser = CommandArgumentParser()
|
||||
example1 = TLDRExample(usage="example1", description="This is the first example.")
|
||||
example2 = TLDRExample(usage="example2", description="This is the second example.")
|
||||
parser.add_tldr_examples([example1, example2])
|
||||
assert len(parser._tldr_examples) == 2
|
||||
assert parser._tldr_examples[0] == example1
|
||||
assert parser._tldr_examples[1] == example2
|
||||
|
||||
@@ -9,7 +9,13 @@ from falyx.action import Action
|
||||
from falyx.command import Command
|
||||
from falyx.command_runner import CommandRunner
|
||||
from falyx.console import console as falyx_console
|
||||
from falyx.exceptions import CommandArgumentError, FalyxError, NotAFalyxError
|
||||
from falyx.console import error_console
|
||||
from falyx.exceptions import (
|
||||
CommandArgumentError,
|
||||
FalyxError,
|
||||
InvalidHookError,
|
||||
NotAFalyxError,
|
||||
)
|
||||
from falyx.hook_manager import HookManager, HookType
|
||||
from falyx.options_manager import OptionsManager
|
||||
from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
|
||||
@@ -123,8 +129,10 @@ async def test_command_runner_initialization(
|
||||
command_with_no_parser,
|
||||
command_with_custom_parser,
|
||||
):
|
||||
runner = CommandRunner(command_with_parser)
|
||||
runner = CommandRunner(command_with_parser, program="test_program")
|
||||
assert runner.command == command_with_parser
|
||||
assert runner.program == "test_program"
|
||||
assert runner.command.arg_parser.program == "test_program"
|
||||
assert isinstance(runner.options, OptionsManager)
|
||||
assert isinstance(runner.runner_hooks, HookManager)
|
||||
assert runner.console == falyx_console
|
||||
@@ -133,7 +141,6 @@ async def test_command_runner_initialization(
|
||||
assert runner.command.options_manager == runner.options
|
||||
assert runner.executor.options == runner.options
|
||||
assert runner.executor.hooks == runner.runner_hooks
|
||||
assert runner.executor.console == runner.console
|
||||
assert runner.options.get("summary", namespace_name="execution") is None
|
||||
|
||||
runner_no_parser = CommandRunner(command_with_no_parser)
|
||||
@@ -166,7 +173,6 @@ def test_command_runner_initialization_with_custom_console(command_with_parser):
|
||||
custom_console = Console()
|
||||
runner = CommandRunner(command_with_parser, console=custom_console)
|
||||
assert runner.console == custom_console
|
||||
assert runner.executor.console == custom_console
|
||||
|
||||
|
||||
def test_command_runner_initialization_with_custom_hooks(command_with_parser):
|
||||
@@ -199,7 +205,9 @@ def test_command_runner_initialization_with_all_bad_components(command_with_pars
|
||||
console=custom_console,
|
||||
)
|
||||
|
||||
with pytest.raises(NotAFalyxError, match="hooks must be an instance of HookManager"):
|
||||
with pytest.raises(
|
||||
InvalidHookError, match="hooks must be an instance of HookManager"
|
||||
):
|
||||
CommandRunner(
|
||||
command_with_parser,
|
||||
runner_hooks=custom_hooks,
|
||||
@@ -236,8 +244,6 @@ async def test_command_runner_run_with_failing_action(command_with_failing_actio
|
||||
with pytest.raises(FalyxError, match="boom"):
|
||||
await runner.run("--foo 42", wrap_errors=True)
|
||||
|
||||
assert await runner.run("--foo 42", wrap_errors=False, raise_on_error=False) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_command_runner_debug_statement(command_with_parser, caplog):
|
||||
@@ -276,6 +282,22 @@ async def test_command_runner_run_with_retries_with_action(
|
||||
assert "[throw_error] All 2 retries failed." in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_command_runner_run_with_retries_delay_with_action(
|
||||
command_throwing_error, caplog
|
||||
):
|
||||
runner = CommandRunner(command_throwing_error)
|
||||
with pytest.raises(asyncio.CancelledError, match="An error occurred in the action."):
|
||||
await runner.run("Other")
|
||||
|
||||
with pytest.raises(ValueError, match="This is a ValueError."):
|
||||
await runner.run("ValueError --retries 2 --retry-delay 1.0 --retry-backoff 2.0")
|
||||
|
||||
assert "[throw_error] Retry attempt 1/2 failed due to 'ValueError'." in caplog.text
|
||||
assert "[throw_error] Retry attempt 2/2 failed due to 'ValueError'." in caplog.text
|
||||
assert "[throw_error] All 2 retries failed." in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_command_runner_run_from_command_build_with_all_execution_options(
|
||||
command_build_with_all_execution_options,
|
||||
@@ -313,7 +335,7 @@ async def test_command_runner_from_command_bad_command():
|
||||
CommandRunner.from_command("Not a Command")
|
||||
|
||||
with pytest.raises(
|
||||
NotAFalyxError, match="runner_hooks must be an instance of HookManager"
|
||||
InvalidHookError, match="runner_hooks must be an instance of HookManager"
|
||||
):
|
||||
CommandRunner.from_command(
|
||||
Command(
|
||||
@@ -360,7 +382,7 @@ async def test_command_runner_build_with_bad_execution_options():
|
||||
@pytest.mark.asyncio
|
||||
async def test_command_runner_build_with_bad_runner_hooks():
|
||||
with pytest.raises(
|
||||
NotAFalyxError, match="runner_hooks must be an instance of HookManager"
|
||||
InvalidHookError, match="runner_hooks must be an instance of HookManager"
|
||||
):
|
||||
CommandRunner.build(
|
||||
key="T",
|
||||
@@ -438,7 +460,7 @@ async def test_command_runner_cli_with_failing_action(command_with_failing_actio
|
||||
await runner.cli(["--help"])
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
|
||||
assert "usage: falyx T" in captured
|
||||
assert "usage: falyx" in captured
|
||||
assert "--foo" in captured
|
||||
assert "summary" in captured
|
||||
assert "retries" in captured
|
||||
@@ -453,54 +475,48 @@ async def test_command_runner_cli_exceptions(command_throwing_error):
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
await runner.cli(["--help"])
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
assert "falyx E [--help]" in captured
|
||||
assert "falyx [--help]" in captured
|
||||
assert "usage:" in captured
|
||||
assert "positional:" in captured
|
||||
assert "options:" in captured
|
||||
assert "❌" not in captured
|
||||
|
||||
with falyx_console.capture() as capture:
|
||||
with pytest.raises(SystemExit, match="2"):
|
||||
await runner.cli(["--not-an-arg"])
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
assert "falyx E [--help]" in captured
|
||||
assert "falyx [--help]" in captured
|
||||
assert "usage:" in captured
|
||||
assert "positional:" in captured
|
||||
assert "options:" in captured
|
||||
assert "❌" in captured
|
||||
falyx_console.clear()
|
||||
|
||||
with falyx_console.capture() as capture:
|
||||
with error_console.capture() as capture:
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await runner.cli(["FalyxError"])
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
assert "This is a FalyxError." in captured
|
||||
assert "❌ Error:" in captured
|
||||
assert "error:" in captured
|
||||
falyx_console.clear()
|
||||
|
||||
with falyx_console.capture() as capture:
|
||||
with pytest.raises(SystemExit, match="130"):
|
||||
await runner.cli(["QuitSignal"])
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
assert "❌" not in captured
|
||||
|
||||
with falyx_console.capture() as capture:
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await runner.cli(["BackSignal"])
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
assert "❌" not in captured
|
||||
|
||||
with falyx_console.capture() as capture:
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await runner.cli(["CancelSignal"])
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
assert "❌" not in captured
|
||||
|
||||
with falyx_console.capture() as capture:
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await runner.cli(["Other"])
|
||||
captured = Text.from_ansi(capture.get()).plain
|
||||
assert "❌" not in captured
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -514,3 +530,12 @@ async def test_command_runner_cli_uses_sys_argv(command_with_parser, monkeypatch
|
||||
assert "Action executed with args:" in captured
|
||||
assert "and kwargs:" in captured
|
||||
assert "{'foo': 42}" in captured
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_command_runner_run_error(command_with_parser):
|
||||
runner = CommandRunner(command_with_parser)
|
||||
with pytest.raises(FalyxError, match="requires either"):
|
||||
await runner.run(["--foo", "42"], raise_on_error=False, wrap_errors=False)
|
||||
await runner.run(["--foo", "42"], raise_on_error=False, wrap_errors=True)
|
||||
await runner.run(["--foo", "42"], raise_on_error=True, wrap_errors=False)
|
||||
|
||||
@@ -40,6 +40,7 @@ async def test_command_validator_is_preview():
|
||||
fake_falyx = AsyncMock()
|
||||
fake_route = SimpleNamespace()
|
||||
fake_route.is_preview = True
|
||||
fake_route.command = SimpleNamespace()
|
||||
fake_falyx.prepare_route.return_value = (fake_route, (), {}, {})
|
||||
validator = CommandValidator(fake_falyx, "Invalid!")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user