From a2456134ec14305c9de131c4f38d027184bd19c0 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Mon, 25 Aug 2025 07:51:43 +0400 Subject: [PATCH] Tone-down the case of `default=True` for flags Amends: 06847da9126a99c88bf347adf213d33bfe4e4af2 Follow up on: https://github.com/pallets/click/pull/3030 --- CHANGES.rst | 18 ++++++++++-------- docs/options.md | 14 +++++--------- src/click/core.py | 42 +++++++++++++----------------------------- tests/test_basic.py | 4 ++-- tests/test_options.py | 6 +++--- 5 files changed, 33 insertions(+), 51 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b03b4e26e..ea6e90283 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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=`` is respected and ```` 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 diff --git a/docs/options.md b/docs/options.md index e26c0d3c0..df784f583 100644 --- a/docs/options.md +++ b/docs/options.md @@ -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) @@ -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 diff --git a/src/click/core.py b/src/click/core.py index c2af25371..3d4d52f85 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -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. @@ -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. @@ -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) diff --git a/tests/test_basic.py b/tests/test_basic.py index dcf429c28..125fac3c4 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -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"), diff --git a/tests/test_options.py b/tests/test_options.py index 35317c78e..7c7abb03a 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -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"),