From bdddc4cd221fb82cfa49ebd71b7535f6f01eb3b3 Mon Sep 17 00:00:00 2001 From: Pkcha Date: Mon, 30 Mar 2026 10:35:39 +0800 Subject: [PATCH 1/2] write your commit message --- tests/test_basic.py | 82 +++++++++++++++++++++++++++++++ tests/test_options.py | 112 ++++++++++++++++++++++++++++++++++++++++++ tests/test_types.py | 89 +++++++++++++++++++++++++++++++++ 3 files changed, 283 insertions(+) diff --git a/tests/test_basic.py b/tests/test_basic.py index 125fac3c44..9b25e16224 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -601,6 +601,88 @@ def cli(start_date): assert result.output == "2010-06-05T00:00:00\n" +@pytest.mark.parametrize( + ("args", "expect"), + [ + ([], "2023-01-15T10:30:00"), + (["--start_date=2024-12-25"], "2024-12-25T00:00:00"), + (["--start_date=2024-12-25 14:45:30"], "2024-12-25T14:45:30"), + ], +) +def test_datetime_option_with_default(runner, args, expect): + from datetime import datetime + + @click.command() + @click.option( + "--start_date", + type=click.DateTime(), + default=datetime(2023, 1, 15, 10, 30, 0), + ) + def cli(start_date): + click.echo(start_date.strftime("%Y-%m-%dT%H:%M:%S")) + + result = runner.invoke(cli, args) + assert not result.exception + assert expect in result.output + + +@pytest.mark.parametrize( + ("formats", "input_value", "expect_output"), + [ + (["%Y-%m-%d", "%d/%m/%Y", "%B %d, %Y"], "2024-06-15", "2024-06-15"), + (["%Y-%m-%d", "%d/%m/%Y", "%B %d, %Y"], "15/06/2024", "2024-06-15"), + (["%Y-%m-%d", "%d/%m/%Y", "%B %d, %Y"], "June 15, 2024", "2024-06-15"), + ], +) +def test_datetime_multiple_formats(runner, formats, input_value, expect_output): + @click.command() + @click.option("--date", type=click.DateTime(formats=formats)) + def cli(date): + click.echo(date.strftime("%Y-%m-%d")) + + result = runner.invoke(cli, [f"--date={input_value}"]) + assert not result.exception + assert expect_output in result.output + + +@pytest.mark.parametrize( + ("input_value", "error_expect"), + [ + ("2024/13/01", "month must be in 1..12"), + ("2024-02-30", "day is out of range for month"), + ("2024-04-31", "day is out of range for month"), + ("2024-01-01 25:00:00", "hour must be in 0..23"), + ("2024-01-01 12:61:00", "minute must be in 0..59"), + ("invalid-date", "does not match the formats"), + ], +) +def test_datetime_invalid_inputs(runner, input_value, error_expect): + @click.command() + @click.option("--date", type=click.DateTime()) + def cli(date): + click.echo(date) + + result = runner.invoke(cli, [f"--date={input_value}"]) + assert result.exit_code == 2 + assert error_expect in result.output or "Invalid value" in result.output + + +def test_datetime_help_formats_display(runner): + @click.command() + @click.option( + "--date", + type=click.DateTime(formats=["%Y-%m-%d", "%d/%m/%Y", "%H:%M:%S"]), + ) + def cli(date): + click.echo(date) + + result = runner.invoke(cli, ["--help"]) + assert not result.exception + assert "%Y-%m-%d" in result.output + assert "%d/%m/%Y" in result.output + assert "%H:%M:%S" in result.output + + def test_required_option(runner): @click.command() @click.option("--foo", required=True) diff --git a/tests/test_options.py b/tests/test_options.py index f198d10183..0bc20d5ddd 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -2319,3 +2319,115 @@ def cmd(theflag): result = runner.invoke(cmd, args) assert result.exit_code == 0 assert result.output == expect_output + + +@pytest.mark.parametrize( + ("option_type", "input_value", "error_pattern"), + [ + (click.INT, "not_an_int", r"not a valid integer"), + (click.FLOAT, "not_a_float", r"not a valid float"), + (click.UUID, "not_a_uuid", r"not a valid UUID"), + (click.IntRange(0, 10), "15", r"not in the range"), + (click.FloatRange(0.0, 1.0), "1.5", r"not in the range"), + ], +) +def test_param_type_error_messages(runner, option_type, input_value, error_pattern): + """Test consistent error message formatting for type conversion failures.""" + + @click.command() + @click.option("--value", type=option_type) + def cmd(value): + click.echo(value) + + result = runner.invoke(cmd, ["--value", input_value]) + assert result.exit_code == 2 + assert re.search(error_pattern, result.output, re.IGNORECASE) is not None + + +@pytest.mark.parametrize( + ("option_type", "valid_input", "help_snippet"), + [ + (click.IntRange(1, 100), "50", r"1<=x<=100"), + (click.FloatRange(0.0, 1.0), "0.5", r"0<=x<=1"), + (click.Choice(["small", "medium", "large"]), "medium", r"small|medium|large"), + ], +) +def test_type_info_in_help(runner, option_type, valid_input, help_snippet): + """Test that type constraints are shown in help output.""" + + @click.command() + @click.option("--value", type=option_type, help="A value") + def cmd(value): + click.echo(value) + + # Test help output + help_result = runner.invoke(cmd, ["--help"]) + assert help_result.exit_code == 0 + assert re.search(help_snippet, help_result.output) is not None + + # Test actual parsing works with valid input + parse_result = runner.invoke(cmd, ["--value", valid_input]) + assert parse_result.exit_code == 0 + + +def test_unrecognized_option_suggestions(runner): + """Test that helpful suggestions are provided for unrecognized options.""" + + @click.command() + @click.option("--verbose", is_flag=True) + @click.option("--version", is_flag=True) + @click.option("--verify", is_flag=True) + def cmd(verbose, version, verify): + pass + + # Test with similar option + result = runner.invoke(cmd, ["--verion"]) + assert result.exit_code == 2 + assert "Did you mean" in result.output or "Possible options" in result.output + + +def test_missing_required_multiple_options(runner): + """Test error message when multiple required options are missing.""" + + @click.command() + @click.option("--username", required=True) + @click.option("--password", required=True) + def login(username, password): + click.echo(f"Logging in as {username}") + + result = runner.invoke(login, []) + assert result.exit_code == 2 + # Should mention at least one required option is missing + assert "Missing option" in result.output + + +@pytest.mark.parametrize( + "bad_args", + [ + ["--username"], # Option without value + ], +) +def test_option_missing_value_errors(runner, bad_args): + """Test error messages for options missing required values.""" + + @click.command() + @click.option("--username", required=True) + def cmd(username): + click.echo(username) + + result = runner.invoke(cmd, bad_args) + assert result.exit_code == 2 + assert "requires an argument" in result.output or "Missing option" in result.output + + +def test_empty_value_different_error_messages(runner): + """Test different error scenarios for option values.""" + # Test missing value for integer type + @click.command() + @click.option("--count", type=int) + def cmd_int(count): + click.echo(count) + + result = runner.invoke(cmd_int, ["--count"]) + assert result.exit_code == 2 + assert "requires an argument" in result.output diff --git a/tests/test_types.py b/tests/test_types.py index 75434f1042..15d00521c6 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -255,3 +255,92 @@ def test_choice_get_invalid_choice_message(): choice = click.Choice(["a", "b", "c"]) message = choice.get_invalid_choice_message("d", ctx=None) assert message == "'d' is not one of 'a', 'b', 'c'." + + +@pytest.mark.parametrize( + ("tuple_types", "input_values", "expected"), + [ + ((str, int), ("hello", "42"), ("hello", 42)), + ((int, float), ("10", "3.14"), (10, 3.14)), + ((click.INT, click.STRING), ("123", "test"), (123, "test")), + ((bool, bool), ("true", "false"), (True, False)), + ], +) +def test_tuple_type_conversion(runner, tuple_types, input_values, expected): + @click.command() + @click.option("--item", type=tuple_types) + def cmd(item): + click.echo(repr(item)) + + result = runner.invoke(cmd, ["--item"] + list(input_values)) + assert not result.exception + assert result.output.strip() == repr(expected) + + +@pytest.mark.parametrize( + ("tuple_types", "input_values", "error_msg"), + [ + ((str, int), ("hello", "not_int"), "not a valid integer"), + ((int, float), ("not_int", "3.14"), "not a valid integer"), + ((int, int), ("10", "not_int"), "not a valid integer"), + ], +) +def test_tuple_type_conversion_error(runner, tuple_types, input_values, error_msg): + @click.command() + @click.option("--item", type=tuple_types) + def cmd(item): + click.echo(item) + + result = runner.invoke(cmd, ["--item"] + list(input_values)) + assert result.exit_code == 2 + assert error_msg in result.output or "Invalid value" in result.output + + +@pytest.mark.parametrize( + ("input_count", "expected_pattern"), + [ + (1, "requires 2 arguments"), + (3, "unexpected extra argument"), + (0, "requires 2 arguments"), + ], +) +def test_tuple_type_wrong_arg_count(runner, input_count, expected_pattern): + @click.command() + @click.option("--item", type=(str, int)) + def cmd(item): + click.echo(item) + + args = ["--item"] + [str(i) for i in range(input_count)] + result = runner.invoke(cmd, args) + assert result.exit_code == 2 + assert expected_pattern in result.output.lower() + + +def test_tuple_type_in_multiple_options(runner): + @click.command() + @click.option("--coord", type=(int, int), multiple=True) + def cmd(coord): + for c in coord: + click.echo(f"({c[0]}, {c[1]})") + + result = runner.invoke(cmd, ["--coord", "10", "20", "--coord", "30", "40"]) + assert not result.exception + assert "(10, 20)" in result.output + assert "(30, 40)" in result.output + + +def test_tuple_type_with_custom_types(runner): + @click.command() + @click.option("--range", type=(click.IntRange(0, 100), click.FloatRange(0, 1.0))) + def cmd(range): + percent, ratio = range + click.echo(f"percent={percent}, ratio={ratio:.2f}") + + result = runner.invoke(cmd, ["--range", "50", "0.75"]) + assert not result.exception + assert "percent=50, ratio=0.75" in result.output + + # Test boundary error with invalid values + result = runner.invoke(cmd, ["--range", "150", "0.75"]) + assert result.exit_code == 2 + assert "150 is not in the range 0<=x<=100" in result.output From 10786ca7696185cec9110042684303215188847e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:36:46 +0000 Subject: [PATCH 2/2] [pre-commit.ci lite] apply automatic fixes --- tests/test_options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_options.py b/tests/test_options.py index 0bc20d5ddd..d23148806e 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -2422,6 +2422,7 @@ def cmd(username): def test_empty_value_different_error_messages(runner): """Test different error scenarios for option values.""" + # Test missing value for integer type @click.command() @click.option("--count", type=int)