Add clone support across Action types and Command so commands can be safely registered or runner-bound without mutating the original instances. - clone BaseAction implementations across simple, composite, IO, prompt, file, HTTP, process, and signal actions - bind cloned commands in Falyx.add_command_from_command() and CommandRunner - preserve local never_prompt settings when cloning actions - rename shared runtime state from options to options_manager for consistency - seed root and execution option namespaces consistently - apply scoped root and namespace option overrides during routing and dispatch - improve namespace completion by delegating option suggestions to FalyxParser - enrich missing-value errors and error hints
320 lines
8.4 KiB
Python
320 lines
8.4 KiB
Python
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.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
|
|
async def test_run_basic(capsys):
|
|
sys.argv = ["falyx", "-h"]
|
|
flx = Falyx()
|
|
with pytest.raises(SystemExit):
|
|
await flx.run()
|
|
|
|
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_manager.get("debug_hooks", namespace_name="root") is False
|
|
|
|
with pytest.raises(SystemExit):
|
|
await flx.run()
|
|
|
|
assert flx.options_manager.get("debug_hooks", namespace_name="root") is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_never_prompt(flx):
|
|
sys.argv = ["falyx", "--never-prompt", "T"]
|
|
|
|
assert flx.options_manager.get("never_prompt", namespace_name="root") is False
|
|
|
|
with pytest.raises(SystemExit):
|
|
await flx.run()
|
|
|
|
assert flx.options_manager.get("debug_hooks", namespace_name="root") is False
|
|
assert flx.options_manager.get("never_prompt", namespace_name="root") is False
|
|
|
|
|
|
@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
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_applies_root_defaults_without_overwriting_existing_root_values():
|
|
child = Falyx(program="child")
|
|
child.add_command("D", "Deploy", action=lambda: "ok", aliases=["deploy"])
|
|
|
|
child.options_manager.set("verbose", True, "root")
|
|
|
|
root = Falyx(program="root")
|
|
root.add_submenu(
|
|
key="C",
|
|
description="Child Menu",
|
|
submenu=child,
|
|
)
|
|
|
|
async def fake_dispatch_route(*, route, args, kwargs, execution_args, **_):
|
|
assert route.namespace is child
|
|
assert route.root_overrides == {}
|
|
assert route.root_defaults["verbose"] is False
|
|
assert route.namespace.options_manager.get("verbose", False, "root") is True
|
|
|
|
root._dispatch_route = fake_dispatch_route
|
|
|
|
sys.argv = ["falyx", "C", "D"]
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
await root.run()
|
|
|
|
assert excinfo.value.code == 0
|
|
|
|
assert child.options_manager.get("verbose", False, "root") is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_applies_root_overrides_temporarily_and_restores_root_namespace():
|
|
child = Falyx(program="child")
|
|
child.add_command("D", "Deploy", action=lambda: "ok", aliases=["deploy"])
|
|
|
|
child.options_manager.set("verbose", False, "root")
|
|
|
|
root = Falyx(program="root")
|
|
root.add_submenu(
|
|
key="C",
|
|
description="Child Menu",
|
|
submenu=child,
|
|
)
|
|
|
|
seen_during_dispatch = {}
|
|
|
|
async def fake_dispatch_route(*, route, args, kwargs, execution_args, **_):
|
|
seen_during_dispatch["verbose"] = route.namespace.options_manager.get(
|
|
"verbose", False, "root"
|
|
)
|
|
assert route.namespace is child
|
|
assert route.root_overrides == {"verbose": True}
|
|
assert seen_during_dispatch["verbose"] is True
|
|
|
|
root._dispatch_route = fake_dispatch_route
|
|
|
|
sys.argv = ["falyx", "--verbose", "C", "D"]
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
await root.run()
|
|
|
|
assert excinfo.value.code == 0
|
|
assert seen_during_dispatch["verbose"] is True
|
|
|
|
assert child.options_manager.get("verbose", False, "root") is False
|