Skip to content

Commit 27aaed3

Browse files
authored
Fix default handling to defer UNSET normalization (#3079)
2 parents 2ed395b + 189068a commit 27aaed3

3 files changed

Lines changed: 43 additions & 5 deletions

File tree

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Unreleased
99
:pr:`3055`
1010
- Replace ``Sentinel.UNSET`` default values by ``None`` as they're passed through
1111
the ``Context.invoke()`` method. :issue:`3066` :issue:`3065` :pr:`3068`
12+
- Fix conversion of ``Sentinel.UNSET`` happening too early, which caused incorrect
13+
behavior for multiple parameters using the same name. :issue:`3071` :pr:`3079`
1214

1315
Version 8.3.0
1416
--------------

src/click/core.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,19 @@ def parse_args(self, ctx: Context, args: list[str]) -> list[str]:
12261226
for param in iter_params_for_processing(param_order, self.get_params(ctx)):
12271227
_, args = param.handle_parse_result(ctx, opts, args)
12281228

1229+
# We now have all parameters' values into `ctx.params`, but the data may contain
1230+
# the `UNSET` sentinel.
1231+
# Convert `UNSET` to `None` to ensure that the user doesn't see `UNSET`.
1232+
#
1233+
# Waiting until after the initial parse to convert allows us to treat `UNSET`
1234+
# more like a missing value when multiple params use the same name.
1235+
# Refs:
1236+
# https://github.com/pallets/click/issues/3071
1237+
# https://github.com/pallets/click/pull/3079
1238+
for name, value in ctx.params.items():
1239+
if value is UNSET:
1240+
ctx.params[name] = None
1241+
12291242
if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
12301243
ctx.fail(
12311244
ngettext(
@@ -2538,17 +2551,14 @@ def handle_parse_result(
25382551
# We skip adding the value if it was previously set by another parameter
25392552
# targeting the same variable name. This prevents parameters competing for
25402553
# the same name to override each other.
2541-
and self.name not in ctx.params
2554+
and (self.name not in ctx.params or ctx.params[self.name] is UNSET)
25422555
):
25432556
# Click is logically enforcing that the name is None if the parameter is
25442557
# not to be exposed. We still assert it here to please the type checker.
25452558
assert self.name is not None, (
25462559
f"{self!r} parameter's name should not be None when exposing value."
25472560
)
2548-
# Normalize UNSET values to None, as we're about to pass them to the
2549-
# command function and move them to the pure-Python realm of user-written
2550-
# code.
2551-
ctx.params[self.name] = value if value is not UNSET else None
2561+
ctx.params[self.name] = value
25522562

25532563
return value, args
25542564

tests/test_defaults.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,29 @@ def foo(name):
8484

8585
result = runner.invoke(cli, ["foo", "--help"], default_map={"foo": {"name": True}})
8686
assert "default: name" in result.output
87+
88+
89+
def test_shared_param_prefers_first_default(runner):
90+
"""test that the first default is chosen when multiple flags share a param name"""
91+
92+
@click.command
93+
@click.option("--red", "color", flag_value="red")
94+
@click.option("--green", "color", flag_value="green", default=True)
95+
def prefers_green(color):
96+
click.echo(color)
97+
98+
@click.command
99+
@click.option("--red", "color", flag_value="red", default=True)
100+
@click.option("--green", "color", flag_value="green")
101+
def prefers_red(color):
102+
click.echo(color)
103+
104+
result = runner.invoke(prefers_green, [])
105+
assert "green" in result.output
106+
result = runner.invoke(prefers_green, ["--red"])
107+
assert "red" in result.output
108+
109+
result = runner.invoke(prefers_red, [])
110+
assert "red" in result.output
111+
result = runner.invoke(prefers_red, ["--green"])
112+
assert "green" in result.output

0 commit comments

Comments
 (0)