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
901 lines
27 KiB
Python
901 lines
27 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from falyx import Falyx
|
|
from falyx.exceptions import EntryNotFoundError, FalyxOptionError
|
|
from falyx.mode import FalyxMode
|
|
from falyx.parser.falyx_parser import FalyxParser
|
|
from falyx.parser.option import Option
|
|
from falyx.parser.parser_types import FalyxTLDRExample
|
|
|
|
|
|
@pytest.fixture
|
|
def flx() -> Falyx:
|
|
flx = Falyx()
|
|
flx.add_command(
|
|
"D",
|
|
description="Deploy command",
|
|
action=lambda: "deploy",
|
|
aliases=["deploy"],
|
|
)
|
|
return flx
|
|
|
|
|
|
@pytest.fixture
|
|
def parser(flx: Falyx) -> FalyxParser:
|
|
return FalyxParser(flx)
|
|
|
|
|
|
def test_init_registers_reserved_options_by_default(parser: FalyxParser) -> None:
|
|
flags = parser.get_flags()
|
|
|
|
assert "-h" in flags
|
|
assert "-v" in flags
|
|
assert "-d" in flags
|
|
assert "-n" in flags
|
|
assert parser.help_option is not None
|
|
assert parser.tldr_option is None
|
|
|
|
|
|
def test_init_respects_disabled_reserved_root_options() -> None:
|
|
parser = FalyxParser(
|
|
Falyx(
|
|
disable_verbose_option=True,
|
|
disable_debug_hooks_option=True,
|
|
disable_never_prompt_option=True,
|
|
)
|
|
)
|
|
|
|
assert parser.get_flags() == ["-h"]
|
|
|
|
with pytest.raises(FalyxOptionError, match="unknown option '-v'"):
|
|
parser.parse_args(["-v"])
|
|
|
|
|
|
def test_get_options_returns_registered_options(parser: FalyxParser) -> None:
|
|
parser.add_option("--region", "-r", default="us-east")
|
|
|
|
options = parser.get_options()
|
|
|
|
assert any(option.dest == "region" for option in options)
|
|
assert parser.get_flags()[-1] == "--region"
|
|
|
|
|
|
def test_add_option_registers_store_option_with_default_and_choices(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option(
|
|
"--region",
|
|
"-r",
|
|
default="us-east",
|
|
choices=["us-east", "us-west"],
|
|
)
|
|
|
|
result = parser.parse_args(["--region", "us-west", "deploy"])
|
|
|
|
assert result.namespace_options["region"] == "us-west"
|
|
assert result.namespace_defaults["region"] == "us-east"
|
|
assert result.remaining_argv == ["deploy"]
|
|
|
|
|
|
def test_add_option_infers_dest_from_long_flag(parser: FalyxParser) -> None:
|
|
parser.add_option("--dry-run-mode", default="safe")
|
|
|
|
result = parser.parse_args([])
|
|
|
|
assert result.namespace_defaults["dry_run_mode"] == "safe"
|
|
|
|
|
|
def test_add_option_uses_explicit_dest(parser: FalyxParser) -> None:
|
|
parser.add_option("--profile-name", dest="profile", default="dev")
|
|
|
|
result = parser.parse_args(["--profile-name", "prod"])
|
|
|
|
assert result.namespace_options["profile"] == "prod"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("flags", "match"),
|
|
[
|
|
((), "no flags provided"),
|
|
(("region",), "must start with '-'"),
|
|
(("--",), "long flags must have at least one character"),
|
|
(("-abc",), "short flags must be a single character"),
|
|
],
|
|
)
|
|
def test_add_option_rejects_invalid_flags(
|
|
parser: FalyxParser,
|
|
flags: tuple[str, ...],
|
|
match: str,
|
|
) -> None:
|
|
with pytest.raises(FalyxOptionError, match=match):
|
|
parser.add_option(*flags)
|
|
|
|
|
|
@pytest.mark.parametrize("dest", ["help", "tldr"])
|
|
def test_add_option_rejects_reserved_dests(
|
|
parser: FalyxParser,
|
|
dest: str,
|
|
) -> None:
|
|
with pytest.raises(FalyxOptionError, match="reserved"):
|
|
parser.add_option("--custom", dest=dest)
|
|
|
|
|
|
def test_add_option_rejects_duplicate_dest(parser: FalyxParser) -> None:
|
|
parser.add_option("--region")
|
|
|
|
with pytest.raises(FalyxOptionError, match="duplicate option dest 'region'"):
|
|
parser.add_option("--region-name", dest="region")
|
|
|
|
|
|
def test_add_option_rejects_duplicate_flag(parser: FalyxParser) -> None:
|
|
parser.add_option("--region")
|
|
|
|
with pytest.raises(FalyxOptionError, match="already used"):
|
|
parser.add_option("--region", dest="other_region")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("dest", "match"),
|
|
[
|
|
("bad-dest", "valid identifier"),
|
|
("1bad", "cannot start with a digit"),
|
|
],
|
|
)
|
|
def test_add_option_rejects_invalid_explicit_dest(
|
|
parser: FalyxParser,
|
|
dest: str,
|
|
match: str,
|
|
) -> None:
|
|
with pytest.raises(FalyxOptionError, match=match):
|
|
parser.add_option("--valid", dest=dest)
|
|
|
|
|
|
def test_add_option_rejects_invalid_action(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="invalid option action"):
|
|
parser.add_option("--region", action="not-real")
|
|
|
|
|
|
def test_add_option_rejects_invalid_store_true_default(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="must be False or None"):
|
|
parser.add_option("--foo", action="store_true", default=True)
|
|
|
|
|
|
def test_add_option_rejects_invalid_store_false_default(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="must be True or None"):
|
|
parser.add_option("--foo", action="store_false", default=False)
|
|
|
|
|
|
def test_add_option_rejects_invalid_store_bool_optional_default(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError,
|
|
match="default value for 'store_bool_optional' action must be None",
|
|
):
|
|
parser.add_option("--foo", action="store_bool_optional", default="not-bool")
|
|
|
|
|
|
def test_add_option_rejects_default_for_help_or_tldr_option(parser: FalyxParser) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError, match="default value cannot be set for action 'help'"
|
|
):
|
|
parser.add_option("--additional-help", action="help", default=True)
|
|
|
|
with pytest.raises(
|
|
FalyxOptionError, match="default value cannot be set for action 'tldr'"
|
|
):
|
|
parser.add_option("--more-tldr", action="tldr", default=True)
|
|
|
|
|
|
def test_add_option_rejects_choices_for_boolean_option(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="choices cannot be specified"):
|
|
parser.add_option("--foo", action="store_true", choices=["yes"])
|
|
|
|
|
|
def test_add_option_rejects_default_outside_choices(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="not in allowed choices"):
|
|
parser.add_option(
|
|
"--region",
|
|
default="eu-central",
|
|
choices=["us-east", "us-west"],
|
|
)
|
|
|
|
|
|
def test_add_option_rejects_invalid_default_type(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="cannot be coerced to int"):
|
|
parser.add_option("--retries", type=int, default="not-an-int")
|
|
|
|
|
|
def test_add_option_rejects_invalid_choice_type(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="invalid choice"):
|
|
parser.add_option("--retries", type=int, choices=["1", "bad"])
|
|
|
|
|
|
def test_add_option_rejects_non_list_suggestions(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="suggestions must be a list or None"):
|
|
parser.add_option("--profile", suggestions=("dev", "prod"))
|
|
|
|
|
|
def test_add_option_rejects_non_string_suggestions(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="suggestions must be a list of strings"):
|
|
parser.add_option("--profile", suggestions=["dev", 1])
|
|
|
|
|
|
def test_add_option_rejects_non_string_flags(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="invalid flag '123': must be a string"):
|
|
parser.add_option("--region", 123)
|
|
|
|
|
|
def test_add_option_rejects_flags_with_invalid_prefix(parser: FalyxParser) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError, match="invalid flag 'region': must start with '-'"
|
|
):
|
|
parser.add_option("region")
|
|
|
|
|
|
def test_add_option_rejects_long_flag_with_insufficient_length(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError, match="long flags must have at least one character after '--'"
|
|
):
|
|
parser.add_option("--", dest="invalid")
|
|
|
|
|
|
def test_add_option_rejects_speacial_characters_in_dest(parser: FalyxParser) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError, match="invalid dest 'bad-dest': must be a valid identifier"
|
|
):
|
|
parser.add_option("--bad-dest", dest="bad-dest")
|
|
|
|
with pytest.raises(
|
|
FalyxOptionError, match=r"invalid dest 'bad\*dest': must be a valid identifier"
|
|
):
|
|
parser.add_option("--bad-dest", dest="bad*dest")
|
|
|
|
with pytest.raises(
|
|
FalyxOptionError, match="invalid dest '1bad-dest': must be a valid identifier"
|
|
):
|
|
parser.add_option("--bad-dest", dest="1bad-dest")
|
|
|
|
|
|
def test_add_option_rejects_dest_starting_with_digit(parser: FalyxParser) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError, match="invalid dest '1bad': cannot start with a digit"
|
|
):
|
|
parser.add_option("--1bad", dest="1bad")
|
|
|
|
|
|
def test_add_option_rejects_special_characters_in_flags(parser: FalyxParser) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError,
|
|
match=r"invalid flag '--bad\*flag': must only contain letters, digits, underscores, or hyphens",
|
|
):
|
|
parser.add_option("--bad*flag", dest="bad_flag")
|
|
|
|
|
|
def test_add_option_rejects_short_flag_with_multiple_characters(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError,
|
|
match="invalid flag '-ab': short flags must be a single character",
|
|
):
|
|
parser.add_option("-ab")
|
|
|
|
|
|
def test_add_option_rejects_bad_flags(parser: FalyxParser) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError,
|
|
match="--region1@': must only contain letters, digits, underscores, or hyphens",
|
|
):
|
|
parser.add_option("--region1@")
|
|
|
|
with pytest.raises(
|
|
FalyxOptionError, match="invalid dest '42region': cannot start with a digit"
|
|
):
|
|
parser.add_option("--42region")
|
|
|
|
|
|
def test_register_option_rejects_duplicate_flag(parser: FalyxParser) -> None:
|
|
parser.add_option("--region", "-r")
|
|
parser.add_option("--profile", "-p")
|
|
|
|
option1 = Option(flags=("--region", "-r"), dest="region")
|
|
option2 = Option(flags=("--profile", "-p"), dest="profile")
|
|
|
|
with pytest.raises(FalyxOptionError, match="already used"):
|
|
parser._register_option(option1)
|
|
|
|
with pytest.raises(FalyxOptionError, match="already used"):
|
|
parser._register_option(option2)
|
|
|
|
|
|
def test_parse_args_with_no_args_returns_defaults(parser: FalyxParser) -> None:
|
|
result = parser.parse_args([])
|
|
|
|
assert result.mode is FalyxMode.COMMAND
|
|
assert result.raw_argv == []
|
|
assert result.remaining_argv == []
|
|
assert result.current_head == ""
|
|
assert result.help is False
|
|
assert result.tldr is False
|
|
assert result.namespace_defaults["help"] is False
|
|
assert result.root_defaults["verbose"] is False
|
|
assert result.root_defaults["debug_hooks"] is False
|
|
assert result.root_defaults["never_prompt"] is False
|
|
|
|
|
|
def test_parse_args_splits_root_and_namespace_options(parser: FalyxParser) -> None:
|
|
parser.add_option("--profile", default="dev")
|
|
|
|
result = parser.parse_args(["--verbose", "--profile", "prod", "deploy"])
|
|
|
|
assert result.root_options == {"verbose": True}
|
|
assert result.namespace_options == {"profile": "prod"}
|
|
assert result.remaining_argv == ["deploy"]
|
|
assert result.current_head == "deploy"
|
|
|
|
|
|
@pytest.mark.parametrize("help_flag", ["-h", "--help"])
|
|
def test_parse_args_help_flag_sets_help_mode(
|
|
parser: FalyxParser,
|
|
help_flag: str,
|
|
) -> None:
|
|
result = parser.parse_args([help_flag])
|
|
|
|
assert result.mode is FalyxMode.HELP
|
|
assert result.help is True
|
|
assert result.namespace_options["help"] is True
|
|
assert result.remaining_argv == []
|
|
|
|
|
|
def test_parse_args_tldr_flag_sets_help_mode_after_tldr_registered(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_tldr_example(
|
|
entry_key="D",
|
|
usage="--region us-east",
|
|
description="Deploy to us-east",
|
|
)
|
|
|
|
result = parser.parse_args(["--tldr"])
|
|
|
|
assert result.mode is FalyxMode.HELP
|
|
assert result.tldr is True
|
|
assert result.namespace_options["tldr"] is True
|
|
|
|
|
|
def test_parse_args_unknown_leading_option_raises(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="unknown option '--wat'"):
|
|
parser.parse_args(["--wat"])
|
|
|
|
|
|
def test_parse_args_stops_at_first_non_option_boundary(parser: FalyxParser) -> None:
|
|
result = parser.parse_args(["deploy", "--verbose", "--never-prompt"])
|
|
|
|
assert result.root_options == {}
|
|
assert result.namespace_options == {}
|
|
assert result.remaining_argv == ["deploy", "--verbose", "--never-prompt"]
|
|
assert result.current_head == "deploy"
|
|
|
|
|
|
def test_parse_args_allows_unknown_options_after_route_boundary(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
result = parser.parse_args(["deploy", "--command-local-option"])
|
|
|
|
assert result.remaining_argv == ["deploy", "--command-local-option"]
|
|
|
|
|
|
def test_parse_args_store_true_and_store_false(parser: FalyxParser) -> None:
|
|
parser.add_option("--json", action="store_true")
|
|
parser.add_option("--color", action="store_false")
|
|
|
|
result = parser.parse_args(["--json", "--color"])
|
|
|
|
assert result.namespace_defaults["json"] is False
|
|
assert result.namespace_defaults["color"] is True
|
|
assert result.namespace_options["json"] is True
|
|
assert result.namespace_options["color"] is False
|
|
|
|
|
|
def test_parse_args_count_option(parser: FalyxParser) -> None:
|
|
parser.add_option("-q", "--quiet", action="count")
|
|
|
|
result = parser.parse_args(["-q", "-q", "--quiet"])
|
|
|
|
assert result.namespace_defaults["quiet"] == 0
|
|
assert result.namespace_options["quiet"] == 3
|
|
|
|
|
|
def test_parse_args_posix_bundles_boolean_and_count_options(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option("-q", "--quiet", action="count")
|
|
|
|
result = parser.parse_args(["-vdnq", "deploy"])
|
|
|
|
assert result.root_options == {
|
|
"verbose": True,
|
|
"debug_hooks": True,
|
|
"never_prompt": True,
|
|
}
|
|
assert result.namespace_options == {"quiet": 1}
|
|
assert result.remaining_argv == ["deploy"]
|
|
|
|
|
|
def test_parse_args_posix_bundle_can_end_with_store_option(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option("-q", "--quiet", action="count")
|
|
parser.add_option("-r", "--region")
|
|
|
|
result = parser.parse_args(["-qr", "us-east", "deploy"])
|
|
|
|
assert result.namespace_options == {
|
|
"quiet": 1,
|
|
"region": "us-east",
|
|
}
|
|
assert result.remaining_argv == ["deploy"]
|
|
|
|
|
|
def test_parse_args_does_not_expand_invalid_posix_bundle(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="unknown option '-vz'"):
|
|
parser.parse_args(["-vz"])
|
|
|
|
|
|
def test_parse_args_store_option_requires_value(parser: FalyxParser) -> None:
|
|
parser.add_option("--region")
|
|
|
|
with pytest.raises(FalyxOptionError, match="expected a value"):
|
|
parser.parse_args(["--region"])
|
|
|
|
|
|
def test_parse_args_store_option_coerces_value(parser: FalyxParser) -> None:
|
|
parser.add_option("--retries", type=int)
|
|
|
|
result = parser.parse_args(["--retries", "3"])
|
|
|
|
assert result.namespace_options["retries"] == 3
|
|
|
|
|
|
def test_parse_args_store_option_rejects_invalid_value(parser: FalyxParser) -> None:
|
|
parser.add_option("--retries", type=int)
|
|
|
|
with pytest.raises(FalyxOptionError, match="invalid value for '--retries'"):
|
|
parser.parse_args(["--retries", "abc"])
|
|
|
|
|
|
def test_parse_args_store_option_rejects_value_outside_choices(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option("--region", choices=["us-east", "us-west"])
|
|
|
|
with pytest.raises(FalyxOptionError, match="expected one of"):
|
|
parser.parse_args(["--region", "eu-central"])
|
|
|
|
|
|
def test_suggest_next_returns_no_suggestions_for_empty_args(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
suggestions, expecting_value = parser.suggest_next([], cursor_at_end_of_token=False)
|
|
|
|
assert suggestions == []
|
|
assert expecting_value is False
|
|
|
|
|
|
def test_suggest_next_suggests_matching_option_flags(parser: FalyxParser) -> None:
|
|
parser.add_option("--region", "-r")
|
|
|
|
suggestions, expecting_value = parser.suggest_next(
|
|
["--r"],
|
|
cursor_at_end_of_token=False,
|
|
)
|
|
|
|
assert suggestions == ["--region"]
|
|
assert expecting_value is False
|
|
|
|
|
|
def test_suggest_next_suggests_all_remaining_flags_at_token_boundary(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option("--region", "-r")
|
|
|
|
suggestions, expecting_value = parser.suggest_next(
|
|
["--"],
|
|
cursor_at_end_of_token=False,
|
|
)
|
|
|
|
assert "--help" in suggestions
|
|
assert "--verbose" in suggestions
|
|
assert "--debug-hooks" in suggestions
|
|
assert "--never-prompt" in suggestions
|
|
assert "--region" in suggestions
|
|
assert expecting_value is False
|
|
|
|
|
|
def test_suggest_next_suggests_choice_values_after_store_option(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option("--region", choices=["us-east", "us-west"])
|
|
|
|
suggestions, expecting_value = parser.suggest_next(
|
|
["--region"],
|
|
cursor_at_end_of_token=True,
|
|
)
|
|
|
|
assert suggestions == ["us-east", "us-west"]
|
|
assert expecting_value is True
|
|
|
|
|
|
def test_suggest_next_filters_choice_values_by_prefix(parser: FalyxParser) -> None:
|
|
parser.add_option("--region", choices=["us-east", "us-west", "eu-central"])
|
|
|
|
suggestions, expecting_value = parser.suggest_next(
|
|
["--region", "us-e"],
|
|
cursor_at_end_of_token=False,
|
|
)
|
|
|
|
assert suggestions == ["us-east"]
|
|
assert expecting_value is True
|
|
|
|
|
|
def test_suggest_next_uses_custom_value_suggestions(parser: FalyxParser) -> None:
|
|
parser.add_option("--profile", suggestions=["dev", "prod", "staging"])
|
|
|
|
suggestions, expecting_value = parser.suggest_next(
|
|
["--profile", "pr"],
|
|
cursor_at_end_of_token=False,
|
|
)
|
|
|
|
assert suggestions == ["prod"]
|
|
assert expecting_value is True
|
|
|
|
|
|
def test_suggest_next_excludes_consumed_options(parser: FalyxParser) -> None:
|
|
parser.add_option("--region", choices=["us-east", "us-west"])
|
|
|
|
parser.parse_args(["--region", "us-east"])
|
|
suggestions, expecting_value = parser.suggest_next(
|
|
["-"],
|
|
cursor_at_end_of_token=False,
|
|
)
|
|
|
|
assert "--region" not in suggestions
|
|
assert "-r" not in suggestions
|
|
assert expecting_value is False
|
|
|
|
|
|
def test_add_tldr_example_registers_example_and_tldr_option(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_tldr_example(
|
|
entry_key="D",
|
|
usage="--region us-east",
|
|
description="Deploy to us-east",
|
|
)
|
|
|
|
assert parser.tldr_option is not None
|
|
assert "--tldr" in parser._options_by_dest
|
|
assert parser._tldr_examples == [
|
|
FalyxTLDRExample(
|
|
entry_key="D",
|
|
usage="--region us-east",
|
|
description="Deploy to us-east",
|
|
)
|
|
]
|
|
|
|
|
|
def test_add_tldr_example_rejects_unknown_entry(parser: FalyxParser) -> None:
|
|
with pytest.raises(EntryNotFoundError) as error:
|
|
parser.add_tldr_example(
|
|
entry_key="depoy",
|
|
usage="",
|
|
description="Typo example",
|
|
)
|
|
|
|
assert error.value.unknown_name == "depoy"
|
|
assert error.value.suggestions == ["DEPLOY"]
|
|
|
|
|
|
def test_add_tldr_examples_accepts_dataclass_instances(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
example = FalyxTLDRExample(
|
|
entry_key="deploy",
|
|
usage="--region us-east",
|
|
description="Deploy to us-east",
|
|
)
|
|
|
|
parser.add_tldr_examples([example])
|
|
|
|
assert parser.tldr_option is not None
|
|
assert parser._tldr_examples == [example]
|
|
|
|
|
|
def test_add_tldr_examples_accepts_three_tuple_examples(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_tldr_examples(
|
|
[
|
|
("deploy", "--region us-east", "Deploy to us-east"),
|
|
]
|
|
)
|
|
|
|
assert parser.tldr_option is not None
|
|
assert parser._tldr_examples == [
|
|
FalyxTLDRExample(
|
|
entry_key="deploy",
|
|
usage="--region us-east",
|
|
description="Deploy to us-east",
|
|
)
|
|
]
|
|
|
|
|
|
def test_add_tldr_examples_rejects_invalid_tuple_shape(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
with pytest.raises(FalyxOptionError, match="invalid TLDR example format"):
|
|
parser.add_tldr_examples([("deploy", "missing description")])
|
|
|
|
|
|
def test_add_tldr_examples_rejects_unknown_entry(parser: FalyxParser) -> None:
|
|
with pytest.raises(EntryNotFoundError) as error:
|
|
parser.add_tldr_examples(
|
|
[
|
|
("depoy", "--region us-east", "Typo example"),
|
|
]
|
|
)
|
|
|
|
with pytest.raises(EntryNotFoundError) as error:
|
|
parser.add_tldr_examples(
|
|
[
|
|
FalyxTLDRExample(
|
|
entry_key="depoy",
|
|
usage="--region us-east",
|
|
description="Typo example",
|
|
)
|
|
]
|
|
)
|
|
|
|
assert error.value.unknown_name == "depoy"
|
|
assert error.value.suggestions == ["DEPLOY"]
|
|
|
|
|
|
def test_store_bool_optional_registers_positive_and_negative_flags(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option("--cache", action="store_bool_optional")
|
|
|
|
assert "--cache" in parser._options_by_dest
|
|
assert "--no-cache" in parser._options_by_dest
|
|
|
|
result = parser.parse_args([])
|
|
assert result.namespace_defaults["cache"] is None
|
|
|
|
result = parser.parse_args(["--cache"])
|
|
assert result.namespace_options["cache"] is True
|
|
|
|
result = parser.parse_args(["--no-cache"])
|
|
assert result.namespace_options["cache"] is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("flag", "expected"),
|
|
[
|
|
("--cache", True),
|
|
("--no-cache", False),
|
|
],
|
|
)
|
|
def test_parse_args_store_bool_optional_intended_behavior(
|
|
parser: FalyxParser,
|
|
flag: str,
|
|
expected: bool,
|
|
) -> None:
|
|
parser.add_option("--cache", action="store_bool_optional")
|
|
|
|
result = parser.parse_args([flag])
|
|
|
|
assert result.namespace_options["cache"] is expected
|
|
|
|
|
|
def test_parse_args_store_bool_optional_rejects_multiple_flags(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError, match="store_bool_optional action can only have a single flag"
|
|
):
|
|
parser.add_option("--cache", "-c", action="store_bool_optional")
|
|
|
|
|
|
def test_parse_args_store_bool_optional_rejects_short_flags(parser: FalyxParser) -> None:
|
|
with pytest.raises(
|
|
FalyxOptionError, match="store_bool_optional action must use a long flag"
|
|
):
|
|
parser.add_option("-c", action="store_bool_optional")
|
|
|
|
|
|
def test_parse_args_long_root_flags(parser: FalyxParser) -> None:
|
|
result = parser.parse_args(["--verbose", "--debug-hooks", "--never-prompt", "deploy"])
|
|
|
|
assert result.root_options == {
|
|
"verbose": True,
|
|
"debug_hooks": True,
|
|
"never_prompt": True,
|
|
}
|
|
assert result.namespace_options == {}
|
|
assert result.remaining_argv == ["deploy"]
|
|
|
|
|
|
@pytest.mark.parametrize("help_flag", ["-h", "--help"])
|
|
def test_parse_args_forwards_help_after_route_boundary(
|
|
parser: FalyxParser,
|
|
help_flag: str,
|
|
) -> None:
|
|
result = parser.parse_args(["deploy", help_flag])
|
|
|
|
assert result.mode is FalyxMode.COMMAND
|
|
assert result.help is False
|
|
assert result.namespace_options == {}
|
|
assert result.remaining_argv == ["deploy", help_flag]
|
|
|
|
|
|
def test_parse_args_short_tldr_flag_sets_help_mode_after_tldr_registered(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_tldr_example(
|
|
entry_key="D",
|
|
usage="--region us-east",
|
|
description="Deploy to us-east",
|
|
)
|
|
|
|
result = parser.parse_args(["-T"])
|
|
|
|
assert result.mode is FalyxMode.HELP
|
|
assert result.tldr is True
|
|
assert result.namespace_options["tldr"] is True
|
|
|
|
|
|
def test_add_tldr_examples_registers_tldr_option_only_once(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_tldr_example(
|
|
entry_key="deploy",
|
|
usage="--region us-east",
|
|
description="Deploy to us-east",
|
|
)
|
|
parser.add_tldr_example(
|
|
entry_key="deploy",
|
|
usage="--region us-west",
|
|
description="Deploy to us-west",
|
|
)
|
|
|
|
tldr_options = [option for option in parser.get_options() if option.dest == "tldr"]
|
|
|
|
assert len(tldr_options) == 1
|
|
assert parser.get_flags().count("--tldr") == 1
|
|
assert len(parser._tldr_examples) == 2
|
|
|
|
|
|
def test_parse_args_resets_consumed_option_state_between_parses(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option("--region", "-r", choices=["us-east", "us-west"])
|
|
|
|
parser.parse_args(["--region", "us-east"])
|
|
suggestions, _ = parser.suggest_next(["-"], cursor_at_end_of_token=False)
|
|
assert "--region" not in suggestions
|
|
|
|
parser.parse_args([])
|
|
suggestions, expecting_value = parser.suggest_next(
|
|
["--r"],
|
|
cursor_at_end_of_token=False,
|
|
)
|
|
|
|
assert suggestions == ["--region"]
|
|
assert expecting_value is False
|
|
|
|
|
|
def test_disabled_reserved_root_options_are_omitted_from_defaults() -> None:
|
|
parser = FalyxParser(
|
|
Falyx(
|
|
disable_verbose_option=True,
|
|
disable_debug_hooks_option=True,
|
|
disable_never_prompt_option=True,
|
|
)
|
|
)
|
|
|
|
result = parser.parse_args([])
|
|
|
|
assert result.root_defaults == {}
|
|
assert result.namespace_defaults["help"] is False
|
|
|
|
|
|
def test_parse_args_typed_choices_are_compared_after_coercion(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option("--retries", type=int, choices=["1", "2"])
|
|
|
|
result = parser.parse_args(["--retries", "1"])
|
|
|
|
assert result.namespace_options["retries"] == 1
|
|
|
|
|
|
def test_add_option_rejects_dict_choices(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="choices cannot be a dict"):
|
|
parser.add_option("--region", choices={"east": "us-east"})
|
|
|
|
|
|
def test_add_option_rejects_non_iterable_choices(parser: FalyxParser) -> None:
|
|
with pytest.raises(FalyxOptionError, match="choices must be iterable"):
|
|
parser.add_option("--region", choices=1)
|
|
|
|
|
|
@pytest.mark.parametrize("flag", ["--verbose", "--debug-hooks", "--never-prompt"])
|
|
def test_suggest_next_does_not_expect_value_for_root_boolean_flags(
|
|
parser: FalyxParser,
|
|
flag: str,
|
|
) -> None:
|
|
suggestions, expecting_value = parser.suggest_next(
|
|
[flag],
|
|
cursor_at_end_of_token=True,
|
|
)
|
|
|
|
assert suggestions == []
|
|
assert expecting_value is False
|
|
|
|
|
|
def test_add_option_normalizes_typed_choices(parser: FalyxParser) -> None:
|
|
option = parser.add_option("--retries", type=int, choices=["1", "2"])
|
|
|
|
assert option.choices == [1, 2]
|
|
|
|
|
|
def test_parse_args_accepts_value_matching_normalized_typed_choice(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option("--retries", type=int, choices=["1", "2"])
|
|
|
|
result = parser.parse_args(["--retries", "1"])
|
|
|
|
assert result.namespace_options["retries"] == 1
|
|
|
|
|
|
def test_add_option_normalizes_typed_default_before_choice_check(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
parser.add_option("--retries", type=int, choices=["1", "2"], default="1")
|
|
|
|
result = parser.parse_args([])
|
|
|
|
assert result.namespace_defaults["retries"] == 1
|
|
|
|
|
|
def test_add_option_returns_registered_option(parser: FalyxParser) -> None:
|
|
option = parser.add_option(
|
|
"--retries",
|
|
type=int,
|
|
default="1",
|
|
choices=["1", "2"],
|
|
)
|
|
|
|
assert isinstance(option, Option)
|
|
assert option.dest == "retries"
|
|
assert option.default == 1
|
|
assert option.choices == [1, 2]
|
|
assert option in parser.get_options()
|
|
|
|
|
|
def test_add_option_store_bool_optional_returns_primary_option(
|
|
parser: FalyxParser,
|
|
) -> None:
|
|
option = parser.add_option("--cache", action="store_bool_optional")
|
|
|
|
assert option.dest == "cache"
|
|
assert option.flags == ("--cache",)
|
|
assert "--cache" in parser._options_by_dest
|
|
assert "--no-cache" in parser._options_by_dest
|