From db955d8f85c9d3fa0ada6d1a98f1b28357ccefcf Mon Sep 17 00:00:00 2001 From: vigneshakaviki Date: Sat, 4 Apr 2026 19:11:51 -0700 Subject: [PATCH 1/2] Fix negative flag with default=True returning wrong value A flag option with flag_value=False (a negative flag) and default=True was incorrectly returning False when the flag was not passed. get_default() was unconditionally converting default=True to flag_value for all flag types, including negative flags where flag_value=False. This caused default=True to be silently overridden with False. Fix the condition to skip the conversion when flag_value is explicitly False, preserving default=True as the actual default for negative flags. Fixes #3111 --- src/click/core.py | 6 +++++- tests/test_options.py | 25 +++++++++++++++++++++++-- tests/test_termui.py | 7 ++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/click/core.py b/src/click/core.py index f0a624be3b..1eafd4695a 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 e335f3c1a0..5f5bd17822 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,27 @@ 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 8220431bb4..d73a36f187 100644 --- a/tests/test_termui.py +++ b/tests/test_termui.py @@ -604,9 +604,10 @@ 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), From 54eac5244449e3a40c844582cdb43c11f6b09f1d Mon Sep 17 00:00:00 2001 From: vigneshakaviki Date: Sat, 4 Apr 2026 19:17:38 -0700 Subject: [PATCH 2/2] Fix ruff line-length and formatting issues in tests --- tests/test_options.py | 8 ++++++-- tests/test_termui.py | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_options.py b/tests/test_options.py index 5f5bd17822..ab1eb66e2d 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1495,10 +1495,14 @@ 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" + 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" + assert result.output == "False", ( + "flag_value=False should be returned when flag is passed" + ) @pytest.mark.parametrize( diff --git a/tests/test_termui.py b/tests/test_termui.py index d73a36f187..7f0881779b 100644 --- a/tests/test_termui.py +++ b/tests/test_termui.py @@ -607,7 +607,6 @@ def cmd(arg1): ({"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),