diff --git a/CHANGES.rst b/CHANGES.rst index 493cf2d880..08556ed93a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,8 @@ Version 8.3.x Unreleased +- If ``flag_value`` is set or no default is provided the flag can be accepted + without an argument.:issue:`3084` :pr:`#3104` - Don't discard pager arguments by correctly using ``subprocess.Popen``. :issue:`3039` :pr:`3055` - Replace ``Sentinel.UNSET`` default values by ``None`` as they're passed through diff --git a/src/click/core.py b/src/click/core.py index 184870abdc..41b15b9181 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -2654,6 +2654,10 @@ class Option(Parameter): :param hidden: hide this option from help outputs. :param attrs: Other command arguments described in :class:`Parameter`. + .. versionchanged:: 8.3.dev + If ``flag_value`` is set or no default is provided, the flag can be + accepted without an argument. + .. versionchanged:: 8.2 ``envvar`` used with ``flag_value`` will always use the ``flag_value``, previously it would use the value of the environment variable. @@ -2747,10 +2751,13 @@ def __init__( # Implicitly a flag because secondary options names were given. elif self.secondary_opts: is_flag = True - # The option is explicitly not a flag. But we do not know yet if it needs a - # value or not. So we look at the default value to determine it. + + # Handle options that are not flags but provide a flag_value. + # If flag_value is set or no default is provided the flag can be accepted + # without an argument. + # https://github.com/pallets/click/issues/3084 elif is_flag is False and not self._flag_needs_value: - self._flag_needs_value = self.default is UNSET + self._flag_needs_value = flag_value is not UNSET or self.default is UNSET if is_flag: # Set missing default for flags if not explicitly required or prompted. diff --git a/tests/test_options.py b/tests/test_options.py index 359d03757a..33bc8ce90a 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -15,6 +15,7 @@ from click import Option from click import UNPROCESSED from click._utils import UNSET +from click.testing import CliRunner def test_prefixes(runner): @@ -2275,3 +2276,36 @@ def rcli(scm_ignore_files): result = runner.invoke(rcli, ["--without-scm-ignore-files"]) assert result.stdout == "frozenset()" assert result.exit_code == 0 + + +def test_flag_value_optional_behavior(): + """Test that options with flag_value and is_flag=False use flag_value + when only flag is provided + + Reproduces https://github.com/pallets/click/issues/3084 + """ + + @click.command() + @click.option("--name", is_flag=False, flag_value="Flag", default="Default") + def hello(name): + click.echo(f"Hello, {name}!") + + runner = CliRunner() + result = runner.invoke(hello, ["--name"]) + assert result.exit_code == 0 + assert result.output == "Hello, Flag!\n" + + +def test_flag_value_with_type_conversion(): + """Test that flag_value is correctly type-converted when used as an option value.""" + + @click.command() + @click.option("--count", is_flag=False, flag_value="1", type=int, default=0) + def repeat(count): + for i in range(count): + click.echo(f"Line {i + 1}") + + runner = CliRunner() + result = runner.invoke(repeat, ["--count"]) + assert result.exit_code == 0 + assert result.output == "Line 1\n"