1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
|
import sys
from argparse import ArgumentParser, Namespace
from typing import Callable, List, Optional
import pytest
from argh.assembling import (
ArgumentNameMappingError,
NameMappingPolicy,
infer_argspecs_from_function,
)
POLICIES = list(NameMappingPolicy) + [None]
@pytest.mark.parametrize("name_mapping_policy", POLICIES)
def test_no_args(name_mapping_policy) -> None:
def func() -> None: ...
parser = _make_parser_for_function(func, name_mapping_policy=name_mapping_policy)
assert_usage(parser, "usage: test [-h]")
@pytest.mark.parametrize("name_mapping_policy", POLICIES)
def test_one_positional(name_mapping_policy) -> None:
def func(alpha: str) -> str:
return f"{alpha}"
parser = _make_parser_for_function(func, name_mapping_policy=name_mapping_policy)
assert_usage(parser, "usage: test [-h] alpha")
assert_parsed(parser, ["hello"], Namespace(alpha="hello"))
@pytest.mark.parametrize("name_mapping_policy", POLICIES)
def test_two_positionals(name_mapping_policy) -> None:
def func(alpha: str, beta: str) -> str:
return f"{alpha}, {beta}"
parser = _make_parser_for_function(func, name_mapping_policy=name_mapping_policy)
assert_usage(parser, "usage: test [-h] alpha beta")
assert_parsed(parser, ["one", "two"], Namespace(alpha="one", beta="two"))
@pytest.mark.parametrize(
"name_mapping_policy,expected_usage",
[
(
NameMappingPolicy.BY_NAME_IF_HAS_DEFAULT,
"usage: test [-h] [-b BETA] alpha",
),
(NameMappingPolicy.BY_NAME_IF_KWONLY, "usage: test [-h] alpha [beta]"),
],
)
def test_two_positionals_one_with_default(name_mapping_policy, expected_usage) -> None:
def func(alpha: str, beta: int = 123) -> str:
return f"{alpha}, {beta}"
parser = _make_parser_for_function(func, name_mapping_policy=name_mapping_policy)
assert_usage(parser, expected_usage)
assert_parsed(parser, ["one"], Namespace(alpha="one", beta=123))
if name_mapping_policy == NameMappingPolicy.BY_NAME_IF_HAS_DEFAULT:
assert_parsed(
parser, ["one", "--beta", "two"], Namespace(alpha="one", beta="two")
)
elif name_mapping_policy == NameMappingPolicy.BY_NAME_IF_KWONLY:
assert_parsed(parser, ["one", "two"], Namespace(alpha="one", beta="two"))
@pytest.mark.parametrize("name_mapping_policy", POLICIES)
def test_varargs(name_mapping_policy) -> None:
def func(*file_paths) -> str:
return f"{file_paths}"
parser = _make_parser_for_function(func, name_mapping_policy=name_mapping_policy)
expected_usage = "usage: test [-h] [file-paths ...]"
# TODO: remove once we drop support for Python 3.8
if sys.version_info < (3, 9):
# https://github.com/python/cpython/issues/82619
expected_usage = "usage: test [-h] [file-paths [file-paths ...]]"
assert_usage(parser, expected_usage)
@pytest.mark.parametrize(
"name_mapping_policy,expected_usage",
[
(NameMappingPolicy.BY_NAME_IF_HAS_DEFAULT, "usage: test [-h] alpha beta"),
(NameMappingPolicy.BY_NAME_IF_KWONLY, "usage: test [-h] -b BETA alpha"),
(None, "usage: test [-h] -b BETA alpha"),
],
)
def test_varargs_between_positional_and_kwonly__no_defaults(
name_mapping_policy, expected_usage
) -> None:
def func(alpha, *, beta) -> str:
return f"{alpha}, {beta}"
parser = _make_parser_for_function(func, name_mapping_policy=name_mapping_policy)
assert_usage(parser, expected_usage)
@pytest.mark.parametrize(
"name_mapping_policy,expected_usage",
[
(
NameMappingPolicy.BY_NAME_IF_HAS_DEFAULT,
"usage: test [-h] [-a ALPHA] [-b BETA]",
),
(NameMappingPolicy.BY_NAME_IF_KWONLY, "usage: test [-h] [-b BETA] [alpha]"),
],
)
def test_varargs_between_positional_and_kwonly__with_defaults(
name_mapping_policy, expected_usage
) -> None:
def func(alpha: int = 1, *, beta: int = 2) -> str:
return f"{alpha}, {beta}"
parser = _make_parser_for_function(func, name_mapping_policy=name_mapping_policy)
assert_usage(parser, expected_usage)
def test_varargs_between_positional_and_kwonly__with_defaults__no_explicit_policy() -> (
None
):
def func(alpha: int = 1, *, beta: int = 2) -> str:
return f"{alpha}, {beta}"
with pytest.raises(ArgumentNameMappingError) as exc:
_make_parser_for_function(func, name_mapping_policy=None)
assert (
'Argument "alpha" in function "func"\n'
"is not keyword-only but has a default value."
) in str(exc.value)
# TODO: remove in v.0.33 if it happens, otherwise in v1.0.
def test_positional_with_defaults_without_kwonly__no_explicit_policy() -> None:
def func(alpha: str, beta: int = 1) -> str:
return f"{alpha} {beta}"
message_pattern = 'Argument "beta" in function "func"\nis not keyword-only but has a default value.'
with pytest.warns(DeprecationWarning, match=message_pattern):
parser = _make_parser_for_function(func, name_mapping_policy=None)
assert_usage(parser, "usage: test [-h] [-b BETA] alpha")
@pytest.mark.parametrize("name_mapping_policy", POLICIES)
def test_kwargs(name_mapping_policy) -> None:
def func(**kwargs) -> str:
return f"{kwargs}"
parser = _make_parser_for_function(func, name_mapping_policy=name_mapping_policy)
assert_usage(parser, "usage: test [-h]")
@pytest.mark.parametrize(
"name_mapping_policy,expected_usage",
[
(
NameMappingPolicy.BY_NAME_IF_HAS_DEFAULT,
"usage: test [-h] [-b BETA] [-d DELTA] alpha gamma",
),
(
NameMappingPolicy.BY_NAME_IF_KWONLY,
"usage: test [-h] -g GAMMA [-d DELTA] alpha [beta]",
),
],
)
def test_all_types_mixed_no_named_varargs(name_mapping_policy, expected_usage) -> None:
def func(alpha: str, beta: int = 1, *, gamma: str, delta: int = 2) -> str:
return f"{alpha}, {beta}, {gamma}, {delta}"
parser = _make_parser_for_function(func, name_mapping_policy=name_mapping_policy)
assert_usage(parser, expected_usage)
def _make_parser_for_function(
func: Callable,
name_mapping_policy: Optional[NameMappingPolicy] = None,
) -> ArgumentParser:
parser = ArgumentParser(prog="test")
parser_add_argument_specs = infer_argspecs_from_function(
function=func, name_mapping_policy=name_mapping_policy
)
for parser_add_argument_spec in parser_add_argument_specs:
parser.add_argument(
*parser_add_argument_spec.cli_arg_names,
**parser_add_argument_spec.get_all_kwargs(),
)
return parser
def assert_usage(parser: ArgumentParser, expected_usage: str) -> None:
if not expected_usage.endswith("\n"):
expected_usage += "\n"
assert expected_usage == parser.format_usage()
def assert_parsed(
parser: ArgumentParser, argv: List[str], expected_result: Namespace
) -> None:
parsed = parser.parse_args(argv)
assert parsed == expected_result
|