Add ArgumentAction.ACTION, support POSIX bundling in CAP, Move all Actions to their own file

This commit is contained in:
2025-05-25 19:25:32 -04:00
parent 429b434566
commit fb1ffbe9f6
46 changed files with 1630 additions and 842 deletions

View File

@ -345,22 +345,24 @@ def test_add_argument_choices_invalid():
def test_add_argument_bad_nargs():
parser = CommandArgumentParser()
# ❌ Invalid nargs value
with pytest.raises(CommandArgumentError):
parser.add_argument("--falyx", nargs="invalid")
# ❌ Invalid nargs type
with pytest.raises(CommandArgumentError):
parser.add_argument("--falyx", nargs=123)
parser.add_argument("--foo", nargs="123")
# ❌ Invalid nargs type
with pytest.raises(CommandArgumentError):
parser.add_argument("--falyx", nargs=None)
parser.add_argument("--foo", nargs=[1, 2])
with pytest.raises(CommandArgumentError):
parser.add_argument("--too", action="count", nargs=5)
with pytest.raises(CommandArgumentError):
parser.add_argument("falyx", action="store_true", nargs=5)
def test_add_argument_nargs():
parser = CommandArgumentParser()
# ✅ Valid nargs value
parser.add_argument("--falyx", nargs=2)
arg = parser._arguments[-1]
assert arg.dest == "falyx"
@ -398,8 +400,10 @@ async def test_parse_args_nargs():
parser = CommandArgumentParser()
parser.add_argument("files", nargs="+", type=str)
parser.add_argument("mode", nargs=1)
parser.add_argument("--action", action="store_true")
args = await parser.parse_args(["a", "b", "c"])
args = await parser.parse_args(["a", "b", "c", "--action"])
args = await parser.parse_args(["--action", "a", "b", "c"])
assert args["files"] == ["a", "b"]
assert args["mode"] == "c"
@ -517,6 +521,15 @@ async def test_parse_args_nargs_multiple_positional():
await parser.parse_args([])
@pytest.mark.asyncio
async def test_parse_args_nargs_none():
parser = CommandArgumentParser()
parser.add_argument("numbers", type=int)
parser.add_argument("mode")
await parser.parse_args(["1", "2"])
@pytest.mark.asyncio
async def test_parse_args_nargs_invalid_positional_arguments():
parser = CommandArgumentParser()
@ -542,20 +555,78 @@ async def test_parse_args_append():
assert args["numbers"] == []
@pytest.mark.asyncio
async def test_parse_args_nargs_int_append():
parser = CommandArgumentParser()
parser.add_argument("--numbers", action=ArgumentAction.APPEND, type=int, nargs=1)
args = await parser.parse_args(["--numbers", "1", "--numbers", "2", "--numbers", "3"])
assert args["numbers"] == [[1], [2], [3]]
args = await parser.parse_args(["--numbers", "1"])
assert args["numbers"] == [[1]]
args = await parser.parse_args([])
assert args["numbers"] == []
@pytest.mark.asyncio
async def test_parse_args_nargs_append():
parser = CommandArgumentParser()
parser.add_argument("numbers", action=ArgumentAction.APPEND, type=int, nargs="*")
parser.add_argument("--mode")
args = await parser.parse_args(["1"])
assert args["numbers"] == [[1]]
args = await parser.parse_args(["1", "2", "3", "--mode", "numbers", "4", "5"])
assert args["numbers"] == [[1, 2, 3], [4, 5]]
assert args["mode"] == "numbers"
args = await parser.parse_args(["1", "2", "3"])
assert args["numbers"] == [[1, 2, 3]]
args = await parser.parse_args([])
assert args["numbers"] == []
@pytest.mark.asyncio
async def test_parse_args_int_optional_append():
parser = CommandArgumentParser()
parser.add_argument("numbers", action=ArgumentAction.APPEND, type=int)
args = await parser.parse_args(["1"])
assert args["numbers"] == [1]
@pytest.mark.asyncio
async def test_parse_args_int_optional_append_multiple_values():
parser = CommandArgumentParser()
parser.add_argument("numbers", action=ArgumentAction.APPEND, type=int)
with pytest.raises(CommandArgumentError):
await parser.parse_args(["1", "2"])
@pytest.mark.asyncio
async def test_parse_args_nargs_int_positional_append():
parser = CommandArgumentParser()
parser.add_argument("numbers", action=ArgumentAction.APPEND, type=int, nargs=1)
args = await parser.parse_args(["1"])
assert args["numbers"] == [[1]]
args = await parser.parse_args([])
assert args["numbers"] == []
with pytest.raises(CommandArgumentError):
await parser.parse_args(["1", "2", "3"])
parser2 = CommandArgumentParser()
parser2.add_argument("numbers", action=ArgumentAction.APPEND, type=int, nargs=2)
args = await parser2.parse_args(["1", "2"])
assert args["numbers"] == [[1, 2]]
with pytest.raises(CommandArgumentError):
await parser2.parse_args(["1", "2", "3"])
@pytest.mark.asyncio
@ -575,6 +646,9 @@ async def test_append_groups_nargs():
parsed = await cap.parse_args(["--item", "a", "b", "--item", "c", "d"])
assert parsed["item"] == [["a", "b"], ["c", "d"]]
with pytest.raises(CommandArgumentError):
await cap.parse_args(["--item", "a", "b", "--item", "c"])
@pytest.mark.asyncio
async def test_extend_flattened():
@ -720,3 +794,35 @@ async def test_extend_positional_nargs():
with pytest.raises(CommandArgumentError):
await parser.parse_args([])
def test_command_argument_parser_equality():
parser1 = CommandArgumentParser()
parser2 = CommandArgumentParser()
parser1.add_argument("--foo", type=str)
parser2.add_argument("--foo", type=str)
assert parser1 == parser2
parser1.add_argument("--bar", type=int)
assert parser1 != parser2
parser2.add_argument("--bar", type=int)
assert parser1 == parser2
assert parser1 != "not a parser"
assert parser1 is not None
assert parser1 != object()
assert parser1.to_definition_list() == parser2.to_definition_list()
assert hash(parser1) == hash(parser2)
@pytest.mark.asyncio
async def test_render_help():
parser = CommandArgumentParser()
parser.add_argument("--foo", type=str, help="Foo help")
parser.add_argument("--bar", action=ArgumentAction.APPEND, type=str, help="Bar help")
assert parser.render_help() is None

View File

@ -0,0 +1,227 @@
import pytest
from falyx.action import Action, SelectionAction
from falyx.exceptions import CommandArgumentError
from falyx.parsers import ArgumentAction, CommandArgumentParser
def test_add_argument():
"""Test the add_argument method."""
parser = CommandArgumentParser()
action = Action("test_action", lambda: "value")
parser.add_argument(
"test", action=ArgumentAction.ACTION, help="Test argument", resolver=action
)
with pytest.raises(CommandArgumentError):
parser.add_argument("test1", action=ArgumentAction.ACTION, help="Test argument")
with pytest.raises(CommandArgumentError):
parser.add_argument(
"test2",
action=ArgumentAction.ACTION,
help="Test argument",
resolver="Not an action",
)
@pytest.mark.asyncio
async def test_falyx_actions():
"""Test the Falyx actions."""
parser = CommandArgumentParser()
action = Action("test_action", lambda: "value")
parser.add_argument(
"-a",
"--alpha",
action=ArgumentAction.ACTION,
resolver=action,
help="Alpha option",
)
# Test valid cases
args = await parser.parse_args(["-a"])
assert args["alpha"] == "value"
@pytest.mark.asyncio
async def test_action_basic():
parser = CommandArgumentParser()
action = Action("hello", lambda: "hi")
parser.add_argument("--greet", action=ArgumentAction.ACTION, resolver=action)
args = await parser.parse_args(["--greet"])
assert args["greet"] == "hi"
@pytest.mark.asyncio
async def test_action_with_nargs():
parser = CommandArgumentParser()
def multiply(a, b):
return int(a) * int(b)
action = Action("multiply", multiply)
parser.add_argument("--mul", action=ArgumentAction.ACTION, resolver=action, nargs=2)
args = await parser.parse_args(["--mul", "3", "4"])
assert args["mul"] == 12
@pytest.mark.asyncio
async def test_action_with_nargs_positional():
parser = CommandArgumentParser()
def multiply(a, b):
return int(a) * int(b)
action = Action("multiply", multiply)
parser.add_argument("mul", action=ArgumentAction.ACTION, resolver=action, nargs=2)
args = await parser.parse_args(["3", "4"])
assert args["mul"] == 12
with pytest.raises(CommandArgumentError):
await parser.parse_args(["3"])
with pytest.raises(CommandArgumentError):
await parser.parse_args([])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["3", "4", "5"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["--mul", "3", "4"])
@pytest.mark.asyncio
async def test_action_with_nargs_positional_int():
parser = CommandArgumentParser()
def multiply(a, b):
return a * b
action = Action("multiply", multiply)
parser.add_argument(
"mul", action=ArgumentAction.ACTION, resolver=action, nargs=2, type=int
)
args = await parser.parse_args(["3", "4"])
assert args["mul"] == 12
with pytest.raises(CommandArgumentError):
await parser.parse_args(["3"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["abc", "3"])
@pytest.mark.asyncio
async def test_action_with_nargs_type():
parser = CommandArgumentParser()
def multiply(a, b):
return a * b
action = Action("multiply", multiply)
parser.add_argument(
"--mul", action=ArgumentAction.ACTION, resolver=action, nargs=2, type=int
)
args = await parser.parse_args(["--mul", "3", "4"])
assert args["mul"] == 12
with pytest.raises(CommandArgumentError):
await parser.parse_args(["--mul", "abc", "3"])
@pytest.mark.asyncio
async def test_action_with_custom_type():
parser = CommandArgumentParser()
def upcase(s):
return s.upper()
action = Action("upcase", upcase)
parser.add_argument("--word", action=ArgumentAction.ACTION, resolver=action, type=str)
args = await parser.parse_args(["--word", "hello"])
assert args["word"] == "HELLO"
@pytest.mark.asyncio
async def test_action_with_nargs_star():
parser = CommandArgumentParser()
def joiner(*args):
return "-".join(args)
action = Action("join", joiner)
parser.add_argument(
"--tags", action=ArgumentAction.ACTION, resolver=action, nargs="*"
)
args = await parser.parse_args(["--tags", "a", "b", "c"])
assert args["tags"] == "a-b-c"
@pytest.mark.asyncio
async def test_action_nargs_plus_missing():
parser = CommandArgumentParser()
action = Action("noop", lambda *args: args)
parser.add_argument("--x", action=ArgumentAction.ACTION, resolver=action, nargs="+")
with pytest.raises(CommandArgumentError):
await parser.parse_args(["--x"])
@pytest.mark.asyncio
async def test_action_with_default():
parser = CommandArgumentParser()
action = Action("default", lambda value: value)
parser.add_argument(
"--default",
action=ArgumentAction.ACTION,
resolver=action,
default="default_value",
)
args = await parser.parse_args([])
assert args["default"] == "default_value"
@pytest.mark.asyncio
async def test_action_with_default_and_value():
parser = CommandArgumentParser()
action = Action("default", lambda value: value)
parser.add_argument(
"--default",
action=ArgumentAction.ACTION,
resolver=action,
default="default_value",
)
args = await parser.parse_args(["--default", "new_value"])
assert args["default"] == "new_value"
@pytest.mark.asyncio
async def test_action_with_default_and_value_not():
parser = CommandArgumentParser()
action = Action("default", lambda: "default_value")
parser.add_argument(
"--default",
action=ArgumentAction.ACTION,
resolver=action,
default="default_value",
)
with pytest.raises(CommandArgumentError):
await parser.parse_args(["--default", "new_value"])
@pytest.mark.asyncio
async def test_action_with_default_and_value_positional():
parser = CommandArgumentParser()
action = Action("default", lambda: "default_value")
parser.add_argument("default", action=ArgumentAction.ACTION, resolver=action)
with pytest.raises(CommandArgumentError):
await parser.parse_args([])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["be"])
# @pytest.mark.asyncio
# async def test_selection_action():
# parser = CommandArgumentParser()
# action = SelectionAction("select", selections=["a", "b", "c"])
# parser.add_argument("--select", action=ArgumentAction.ACTION, resolver=action)
# args = await parser.parse_args(["--select"])

View File

@ -0,0 +1,90 @@
import pytest
from falyx.parsers import Argument, ArgumentAction
def test_positional_text_with_choices():
arg = Argument(flags=("path",), dest="path", positional=True, choices=["a", "b"])
assert arg.get_positional_text() == "{a,b}"
def test_positional_text_without_choices():
arg = Argument(flags=("path",), dest="path", positional=True)
assert arg.get_positional_text() == "path"
@pytest.mark.parametrize(
"nargs,expected",
[
(None, "VALUE"),
(1, "VALUE"),
("?", "[VALUE]"),
("*", "[VALUE ...]"),
("+", "VALUE [VALUE ...]"),
],
)
def test_choice_text_store_action_variants(nargs, expected):
arg = Argument(
flags=("--value",), dest="value", action=ArgumentAction.STORE, nargs=nargs
)
assert arg.get_choice_text() == expected
@pytest.mark.parametrize(
"nargs,expected",
[
(None, "value"),
(1, "value"),
("?", "[value]"),
("*", "[value ...]"),
("+", "value [value ...]"),
],
)
def test_choice_text_store_action_variants_positional(nargs, expected):
arg = Argument(
flags=("value",),
dest="value",
action=ArgumentAction.STORE,
nargs=nargs,
positional=True,
)
assert arg.get_choice_text() == expected
def test_choice_text_with_choices():
arg = Argument(flags=("--mode",), dest="mode", choices=["dev", "prod"])
assert arg.get_choice_text() == "{dev,prod}"
def test_choice_text_append_and_extend():
for action in [ArgumentAction.APPEND, ArgumentAction.EXTEND]:
arg = Argument(flags=("--tag",), dest="tag", action=action)
assert arg.get_choice_text() == "TAG"
def test_equality():
a1 = Argument(flags=("--f",), dest="f")
a2 = Argument(flags=("--f",), dest="f")
a3 = Argument(flags=("-x",), dest="x")
assert a1 == a2
assert a1 != a3
assert hash(a1) == hash(a2)
def test_inequality_with_non_argument():
arg = Argument(flags=("--f",), dest="f")
assert arg != "not an argument"
def test_argument_equality():
arg = Argument("--foo", dest="foo", type=str, default="default_value")
arg2 = Argument("--foo", dest="foo", type=str, default="default_value")
arg3 = Argument("--bar", dest="bar", type=int, default=42)
arg4 = Argument("--foo", dest="foo", type=str, default="foobar")
assert arg == arg2
assert arg != arg3
assert arg != arg4
assert arg != "not an argument"
assert arg is not None
assert arg != object()

View File

@ -0,0 +1,11 @@
from falyx.parsers import ArgumentAction
def test_argument_action():
action = ArgumentAction.APPEND
assert action == ArgumentAction.APPEND
assert action != ArgumentAction.STORE
assert action != "invalid_action"
assert action.value == "append"
assert str(action) == "append"
assert len(ArgumentAction.choices()) == 8

View File

View File

@ -0,0 +1,56 @@
import pytest
from falyx.exceptions import CommandArgumentError
from falyx.parsers import ArgumentAction, CommandArgumentParser
@pytest.mark.asyncio
async def test_nargs():
"""Test the nargs argument for command-line arguments."""
parser = CommandArgumentParser()
parser.add_argument(
"-a",
"--alpha",
action=ArgumentAction.STORE,
nargs=2,
help="Alpha option with two arguments",
)
parser.add_argument(
"-b",
"--beta",
action=ArgumentAction.STORE,
nargs="+",
help="Beta option with one or more arguments",
)
parser.add_argument(
"-c",
"--charlie",
action=ArgumentAction.STORE,
nargs="*",
help="Charlie option with zero or more arguments",
)
# Test valid cases
args = await parser.parse_args(["-a", "value1", "value2"])
assert args["alpha"] == ["value1", "value2"]
args = await parser.parse_args(["-b", "value1", "value2", "value3"])
assert args["beta"] == ["value1", "value2", "value3"]
args = await parser.parse_args(["-c", "value1", "value2"])
assert args["charlie"] == ["value1", "value2"]
args = await parser.parse_args(["-c"])
assert args["charlie"] == []
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-a", "value1"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-a"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-a", "value1", "value2", "value3"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-b"])

View File

@ -0,0 +1,128 @@
import pytest
from falyx.exceptions import CommandArgumentError
from falyx.parsers import ArgumentAction, CommandArgumentParser
@pytest.mark.asyncio
async def test_posix_bundling():
"""Test the bundling of short options in the POSIX style."""
parser = CommandArgumentParser()
parser.add_argument(
"-a", "--alpha", action=ArgumentAction.STORE_FALSE, help="Alpha option"
)
parser.add_argument(
"-b", "--beta", action=ArgumentAction.STORE_TRUE, help="Beta option"
)
parser.add_argument(
"-c", "--charlie", action=ArgumentAction.STORE_TRUE, help="Charlie option"
)
# Test valid bundling
args = await parser.parse_args(["-abc"])
assert args["alpha"] is False
assert args["beta"] is True
assert args["charlie"] is True
@pytest.mark.asyncio
async def test_posix_bundling_last_has_value():
"""Test the bundling of short options in the POSIX style with last option having a value."""
parser = CommandArgumentParser()
parser.add_argument(
"-a", "--alpha", action=ArgumentAction.STORE_TRUE, help="Alpha option"
)
parser.add_argument(
"-b", "--beta", action=ArgumentAction.STORE_TRUE, help="Beta option"
)
parser.add_argument(
"-c", "--charlie", action=ArgumentAction.STORE, help="Charlie option"
)
# Test valid bundling with last option having a value
args = await parser.parse_args(["-abc", "value"])
assert args["alpha"] is True
assert args["beta"] is True
assert args["charlie"] == "value"
@pytest.mark.asyncio
async def test_posix_bundling_invalid():
"""Test the bundling of short options in the POSIX style with invalid cases."""
parser = CommandArgumentParser()
parser.add_argument(
"-a", "--alpha", action=ArgumentAction.STORE_FALSE, help="Alpha option"
)
parser.add_argument(
"-b", "--beta", action=ArgumentAction.STORE_TRUE, help="Beta option"
)
parser.add_argument(
"-c", "--charlie", action=ArgumentAction.STORE, help="Charlie option"
)
# Test invalid bundling
args = await parser.parse_args(["-abc", "value"])
assert args["alpha"] is False
assert args["beta"] is True
assert args["charlie"] == "value"
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-a", "value"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-b", "value"])
args = await parser.parse_args(["-c", "value"])
assert args["alpha"] is True
assert args["beta"] is False
assert args["charlie"] == "value"
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-cab", "value"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-a", "-b", "value"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-dbc", "value"])
with pytest.raises(CommandArgumentError):
args = await parser.parse_args(["-c"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-abc"])
@pytest.mark.asyncio
async def test_posix_bundling_fuzz():
"""Test the bundling of short options in the POSIX style with fuzzing."""
parser = CommandArgumentParser()
parser.add_argument(
"-a", "--alpha", action=ArgumentAction.STORE_FALSE, help="Alpha option"
)
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["--"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["--=value"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["--flag="])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-a=b"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["---"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-a", "-b", "-c"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-a", "--", "-b", "-c"])
with pytest.raises(CommandArgumentError):
await parser.parse_args(["-a", "--flag", "-b", "-c"])

View File

@ -1,6 +1,7 @@
import pytest
from falyx import Action, Falyx
from falyx import Falyx
from falyx.action import Action
@pytest.mark.asyncio