Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ Version 8.3.x

Unreleased

- Rework relationship between ``flag_value`` and ``default``: the value given to
``default`` is now left untouched, and keep the value it receive. So
``default=<desired_value>`` is respected and ``<desired_value>`` is passed on as-is
to the CLI function. With the exception of flag options, where setting
``default=True`` maintain the legacy behavior of defaulting to the ``flag_value``.
This allow ``default`` to be of any type, including ``bool`` or ``None``, fixing
inconsistencies reported in: :issue:`1992` :issue:`2012` :issue:`2514`
:issue:`2610` :issue:`3024` :pr:`3030`
- **Improved flag option handling**: Reworked the relationship between ``flag_value``
and ``default`` parameters for better consistency:

* The ``default`` parameter value is now preserved as-is and passed directly
to CLI functions (no more unexpected transformations)
* Exception: flag options with ``default=True`` maintain backward compatibility
by defaulting to their ``flag_value``
* The ``default`` parameter can now be any type (``bool``, ``None``, etc.)
* Fixes inconsistencies reported in: :issue:`1992` :issue:`2514` :issue:`2610`
:issue:`3024` :pr:`3030`
- Allow ``default`` to be set on ``Argument`` for ``nargs = -1``. :issue:`2164`
:pr:`3030`
- Show correct auto complete value for ``nargs`` option in combination with flag
Expand Down
14 changes: 5 additions & 9 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,14 +335,12 @@ To have an flag pass a value to the underlying function set `flag_value`. This a
invoke(info)
```

````{caution}
The `default` argument of options always give to the underlying function its value *as-is*.
````{note}
The `default` value is given to the underlying function as-is. So if you set `default=None`, the value passed to the function is the `None` Python value. Same for any other type.

But for flags, the interaction between `flag_value` and `default` is a bit special.
But there is a special case for flags. If a flag has a `flag_value`, then setting `default=True` is interpreted as *the flag should be activated by default*. So instead of the underlying function receiving the `True` Python value, it will receive the `flag_value`.

If a flag has a `flag_value`, setting `default` to `True` means that the flag is activated by default. Not that the value passed to the underlying function is the `True` Python value. Instead, the default value will be aligned to the `flag_value` behind the scenes.

Which means, the in example above, this option:
Which means, in example above, this option:

```python
@click.option('--upper', 'transformation', flag_value='upper', default=True)
Expand All @@ -354,9 +352,7 @@ is equivalent to:
@click.option('--upper', 'transformation', flag_value='upper', default='upper')
```

This was implemented to support legacy behavior, that will be removed in Click 9.0 to allow for default to take any value, including `True`.

In the mean time, to avoid confusion, it is recommended to always set `default` to the actual default value you want to pass to the underlying function, and not use `True`, `False` or `None`. Unless that's the precise value you want to explicitly force as default.
Because the two are equivalent, it is recommended to always use the second form, and set `default` to the actual value you want to pass. And not use the special `True` case. This makes the code more explicit and predictable.
````

## Values from Environment Variables
Expand Down
42 changes: 13 additions & 29 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2606,15 +2606,6 @@ class Option(Parameter):
:param hidden: hide this option from help outputs.
:param attrs: Other command arguments described in :class:`Parameter`.

.. caution::
Flags specifying a ``flag_value`` and whose ``default=True`` will have
their ``default`` aligned to the ``flag_value``.

This means there is no way to setup a flag whose default ``True`` and
whose ``flag_value`` is something else than ``True``.

This is to support legacy behavior that will be removed in Click 9.0.

.. versionchanged:: 8.2
``envvar`` used with ``flag_value`` will always use the ``flag_value``,
previously it would use the value of the environment variable.
Expand Down Expand Up @@ -2742,23 +2733,13 @@ def __init__(
if self.default is UNSET and not self.required:
self.default = False

# XXX Support the legacy case of aligning the default value with the flag_value
# for flags whose default is explicitly set to True. As long as we have this
# condition, there is no way a flag can have a default set to True, unless its
# flag_value itself is set to True. Refs:
# Support the special case of aligning the default value with the flag_value
# for flags whose default is explicitly set to True. Note that as long as we
# have this condition, there is no way a flag can have a default set to True,
# and a flag_value set to something else. Refs:
# https://github.com/pallets/click/issues/3024#issuecomment-3146199461
# https://github.com/pallets/click/pull/3030/files#r2288936493
# https://github.com/pallets/click/pull/3030/commits/06847da
if self.default is True and self.flag_value is not UNSET:
# This message is a convoluted way to explain that if you want things
# to be equal, make them equal.
# warnings.warn(
# "A flag's `default` value will no longer be aligned with its "
# "`flag_value` if `default=True` in Click 9.0. If you want the flag "
# "to get the same `default` as its `flag_value`, update the option "
# "to make its `default` parameter equal to its `flag_value`.",
# DeprecationWarning,
# stacklevel=2,
# )
self.default = self.flag_value

# Set the default flag_value if it is not set.
Expand Down Expand Up @@ -3070,11 +3051,14 @@ def prompt_for_value(self, ctx: Context) -> t.Any:
# one.
if default in (UNSET, None):
default = None
# Nothing prevent you to declare an option that is auto-detected as a
# boolean flag, is allow to prompt but still declare a non-boolean default.
# So with this casting, we aligns the default value to the prompt behavior.
# The prompt is going to default to [Y/n]), and so not entering a value for
# input is expected to make the option takes True as the default.
# Nothing prevent you to declare an option that is simultaneously:
# 1) auto-detected as a boolean flag,
# 2) allowed to prompt, and
# 3) still declare a non-boolean default.
# This forced casting into a boolean is necessary to align any non-boolean
# default to the prompt, which is going to be a [y/n]-style confirmation
# because the option is still a boolean flag. That way, instead of [y/n],
# we get [Y/n] or [y/N] depending on the truthy value of the default.
# Refs: https://github.com/pallets/click/pull/3030#discussion_r2289180249
else:
default = bool(default)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,8 @@ def cli(flag):
(None, [], None),
# Default is normalized to None if it is UNSET.
(UNSET, [], None),
# Legacy case: if default=True and flag_value is set, The value returned is the
# flag_value, not default itself.
# Special case: if default=True and flag_value is set, the value returned is the
# flag_value, not the True Python value itself.
(True, [], "upper"),
# Non-string defaults are process as strings by the default Parameter's type.
(False, [], "False"),
Expand Down
6 changes: 3 additions & 3 deletions tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1478,11 +1478,11 @@ def cmd(foo):
("xMl", [], "xMl"),
(" ᕕ( ᐛ )ᕗ ", [], " ᕕ( ᐛ )ᕗ "),
(None, [], None),
# Legacy case: UNSET is not exposed directly to the callback, but converted to
# Special case: UNSET is not provided as-is to the callback, but normalized to
# None.
(UNSET, [], None),
# Legacy case: if default=True and flag_value is set, The value returned is the
# flag_value, not default itself.
# Special case: if default=True and flag_value is set, the value returned is the
# flag_value, not the True Python value itself.
(True, [], "js"),
# Non-string defaults are process as strings by the default Parameter's type.
(False, [], "False"),
Expand Down
Loading