diff --git a/src/click/core.py b/src/click/core.py index f0a624be3..1eafd4695 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -2897,7 +2897,11 @@ def get_default( # (instead of eagerly in __init__) prevents callable flag_values # (like classes) from being instantiated by the callable check below. # https://github.com/pallets/click/issues/3121 - if value is True and self.is_flag: + # Skip the conversion when flag_value is explicitly False (a negative flag). + # For negative flags, default=True is the literal default value — not a signal + # to activate the flag. Converting it to False would silently override the + # user's explicit default. https://github.com/pallets/click/issues/3111 + if value is True and self.is_flag and self.flag_value is not False: value = self.flag_value elif call and callable(value): value = value() diff --git a/tests/test_options.py b/tests/test_options.py index e335f3c1a..ab1eb66e2 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1435,13 +1435,13 @@ def test_type_from_flag_value(): # Not passing --foo returns the default value as-is, in its Python type, then # converted by the option type. ({"type": bool, "default": True, "flag_value": True}, [], True), - ({"type": bool, "default": True, "flag_value": False}, [], False), + ({"type": bool, "default": True, "flag_value": False}, [], True), ({"type": bool, "default": False, "flag_value": True}, [], False), ({"type": bool, "default": False, "flag_value": False}, [], False), ({"type": bool, "default": None, "flag_value": True}, [], None), ({"type": bool, "default": None, "flag_value": False}, [], None), ({"type": str, "default": True, "flag_value": True}, [], "True"), - ({"type": str, "default": True, "flag_value": False}, [], "False"), + ({"type": str, "default": True, "flag_value": False}, [], "True"), ({"type": str, "default": False, "flag_value": True}, [], "False"), ({"type": str, "default": False, "flag_value": False}, [], "False"), ({"type": str, "default": "foo", "flag_value": True}, [], "foo"), @@ -1480,6 +1480,31 @@ def cmd(foo): assert result.output == repr(expected) +def test_negative_flag_with_true_default(runner): + """A negative flag (flag_value=False, default=True) should return True when not + passed and False when passed. Regression test for https://github.com/pallets/click/issues/3111.""" + + @click.command() + @click.option( + "--without-xyz", + "enable_xyz", + flag_value=False, + default=True, + ) + def cmd(enable_xyz): + click.echo(repr(enable_xyz), nl=False) + + result = runner.invoke(cmd, []) + assert result.output == "True", ( + "default=True should be returned when flag is not passed" + ) + + result = runner.invoke(cmd, ["--without-xyz"]) + assert result.output == "False", ( + "flag_value=False should be returned when flag is passed" + ) + + @pytest.mark.parametrize( ("args", "opts"), [ diff --git a/tests/test_termui.py b/tests/test_termui.py index 8220431bb..7f0881779 100644 --- a/tests/test_termui.py +++ b/tests/test_termui.py @@ -604,9 +604,9 @@ def cmd(arg1): ({"prompt": True, "default": True, "flag_value": True}, [], "[Y/n]", "", True), ({"prompt": True, "default": True, "flag_value": True}, [], "[Y/n]", "y", True), ({"prompt": True, "default": True, "flag_value": True}, [], "[Y/n]", "n", False), - ({"prompt": True, "default": True, "flag_value": False}, [], "[y/N]", "", False), - ({"prompt": True, "default": True, "flag_value": False}, [], "[y/N]", "y", True), - ({"prompt": True, "default": True, "flag_value": False}, [], "[y/N]", "n", False), + ({"prompt": True, "default": True, "flag_value": False}, [], "[Y/n]", "", True), + ({"prompt": True, "default": True, "flag_value": False}, [], "[Y/n]", "y", True), + ({"prompt": True, "default": True, "flag_value": False}, [], "[Y/n]", "n", False), # default=False ({"prompt": True, "default": False, "flag_value": True}, [], "[y/N]", "", False), ({"prompt": True, "default": False, "flag_value": True}, [], "[y/N]", "y", True),