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
|
from dataclasses import dataclass
from typing import Annotated
from unittest.mock import Mock
import pytest
from cyclopts import Parameter, ValidationError
from cyclopts.exceptions import CoercionError
from cyclopts.token import Token
@pytest.fixture
def validator():
return Mock()
def test_custom_converter(app, assert_parse_args):
def custom_converter(type_, tokens):
return 2 * int(tokens[0].value)
@app.default
def foo(age: Annotated[int, Parameter(converter=custom_converter)]):
pass
assert_parse_args(foo, "5", age=10)
def test_custom_converter_dict(app, assert_parse_args):
def custom_converter(type_, tokens):
return {k: 2 * int(v[0].value) for k, v in tokens.items()}
@app.default
def foo(*, color: Annotated[dict[str, int], Parameter(converter=custom_converter)]):
pass
assert_parse_args(foo, "--color.red 5 --color.green 10", color={"red": 10, "green": 20})
def test_custom_converter_user_value_error_single_token(app):
def custom_converter(type_, tokens):
raise ValueError
@app.default
def foo(age: Annotated[int, Parameter(converter=custom_converter)]):
pass
with pytest.raises(CoercionError) as e:
app("5", exit_on_error=False)
assert str(e.value) == 'Invalid value for "AGE": unable to convert "5" into int.'
def test_custom_converter_user_value_error_multi_token(app):
def custom_converter(type_, tokens):
raise ValueError
@app.default
def foo(age: Annotated[tuple[int, int], Parameter(converter=custom_converter)]):
pass
with pytest.raises(CoercionError) as e:
app("5 6", exit_on_error=False)
assert str(e.value) == 'Invalid value for "--age": unable to convert value to tuple[int, int].'
def test_custom_converter_user_value_error_with_message(app):
def custom_converter(type_, tokens):
raise ValueError("Some user-provided message.")
@app.default
def foo(age: Annotated[int, Parameter(converter=custom_converter)]):
pass
with pytest.raises(CoercionError) as e:
app("5", exit_on_error=False)
assert str(e.value) == "Some user-provided message."
def test_custom_converter_user_kwargs_error(app):
def custom_converter(type_, tokens):
raise ValueError
@app.default
def foo(**kwargs: Annotated[int, Parameter(converter=custom_converter)]):
pass
with pytest.raises(CoercionError) as e:
app("--foo 5", exit_on_error=False)
assert str(e.value) == 'Invalid value for "--foo": unable to convert "5" into int.'
def test_custom_converter_user_kwargs_error_with_message(app):
def custom_converter(type_, tokens):
raise ValueError("Some user-provided message.")
@app.default
def foo(**kwargs: Annotated[int, Parameter(converter=custom_converter)]):
pass
with pytest.raises(CoercionError) as e:
app("--foo 5", exit_on_error=False)
assert str(e.value) == "Invalid value for --foo: Some user-provided message."
def test_custom_validator_positional_or_keyword(app, assert_parse_args, validator):
@app.default
def foo(age: Annotated[int, Parameter(validator=validator)]):
pass
assert_parse_args(foo, "10", age=10)
validator.assert_called_once_with(int, 10)
def test_custom_validator_var_keyword(app, assert_parse_args, validator):
@app.default
def foo(**age: Annotated[int, Parameter(validator=validator)]):
pass
assert_parse_args(foo, "--age=10", age=10)
validator.assert_called_once_with(int, 10)
def test_custom_validator_var_positional(app, assert_parse_args, validator):
@app.default
def foo(*age: Annotated[int, Parameter(validator=validator)]):
pass
assert_parse_args(foo, "10", 10)
validator.assert_called_once_with(int, 10)
def test_custom_validators(app, assert_parse_args):
def lower_bound(type_, value):
if value <= 0:
raise ValueError("An unreasonable age was entered.")
def upper_bound(type_, value):
if value > 150:
raise ValueError("An unreasonable age was entered.")
@app.default
def foo(age: Annotated[int, Parameter(validator=[lower_bound, upper_bound])]):
pass
assert_parse_args(foo, "10", 10)
with pytest.raises(ValidationError):
app.parse_args("0", print_error=False, exit_on_error=False)
with pytest.raises(ValidationError):
app.parse_args("200", print_error=False, exit_on_error=False)
def test_custom_converter_and_validator(app, assert_parse_args, validator):
def custom_validator(type_, value):
if not (0 < value < 150):
raise ValueError("An unreasonable age was entered.")
def custom_converter(type_, tokens):
return 2 * int(tokens[0].value)
@app.default
def foo(age: Annotated[int, Parameter(converter=custom_converter, validator=validator)]):
pass
assert_parse_args(foo, "5", 10)
validator.assert_called_once_with(int, 10)
def test_custom_validator_on_default_signature_value(app, validator):
@app.default
def foo(age: Annotated[int, Parameter(validator=validator)] = -1):
pass
app.parse_args("", print_error=False, exit_on_error=False)
validator.assert_called_once_with(int, -1)
def test_custom_command_validator(app, assert_parse_args):
validator = Mock()
@app.default(validator=validator)
def foo(a: int, b: int, c: int):
pass
assert_parse_args(foo, "1 2 3", 1, 2, 3)
validator.assert_called_once_with(a=1, b=2, c=3)
def test_custom_converter_inside_class(app, mocker):
converter = mocker.Mock(return_value=5)
@Parameter(name="*")
@dataclass
class Config:
foo: Annotated[int, Parameter(converter=converter)]
@app.default
def default(config: Config):
pass
app("bar")
converter.assert_called_once_with(int, (Token(value="bar", source="cli"),))
|