From 2b657493fa8afae9731a7237c58d737ec3ccf2ae Mon Sep 17 00:00:00 2001 From: suzunn Date: Sat, 6 Jun 2026 09:09:15 +0300 Subject: [PATCH] fix: preserve shared option callback values --- src/click/core.py | 13 ++++++++++++- tests/test_options.py | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/click/core.py b/src/click/core.py index a6c9c6421..1b9ff2559 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -2674,10 +2674,21 @@ def handle_parse_result( and existing_default_explicit and not self._default_explicit ) + unprocessed_would_downgrade_callback = ( + same_source + and source == ParameterSource.COMMANDLINE + and self.callback is None + and existing_value is not UNSET + and existing_value != value + ) is_winner = ( slot_empty or more_explicit - or (same_source and not auto_would_downgrade_explicit) + or ( + same_source + and not auto_would_downgrade_explicit + and not unprocessed_would_downgrade_callback + ) ) if is_winner: diff --git a/tests/test_options.py b/tests/test_options.py index 1b93e136e..333976716 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1626,6 +1626,32 @@ def main(fmt): assert result.exit_code == 0 +def test_shared_option_callback_value_is_preserved(runner): + marker = "$_fetch" + + def fetch_value(ctx, param, value): + if value == marker: + return "fetched" + + return value + + @click.command() + @click.option("--custom", "custom") + @click.option("--fetch", "custom", flag_value=marker, callback=fetch_value) + def cli(custom): + click.echo(custom) + + for args, expected in ( + (["--fetch"], "fetched"), + (["--custom", "typed", "--fetch"], "fetched"), + (["--fetch", "--custom", "typed"], "typed"), + ): + result = runner.invoke(cli, args) + + assert result.output == f"{expected}\n" + assert result.exit_code == 0 + + @pytest.mark.parametrize( ("flag_value", "envvar_value", "expected"), [