Repository: pallets/click (python)

Issue:
Give `Option.show_default` higher priority wrt `Context.show_default`
Currently, `Context.show_default` (a global setting) overrides `Option.show_default` (a local setting). This is counterintuitive and limit expressiveness: once `Context.show_default` is set to `True`, it's impossible to disable `show_default` for a specific option.

Pull request:
Give `Option.show_default` higher priority wrt `Context.show_default`
Fixes #1963. 

After this, a non-null `show_default` passed to an option overrides the corresponding context setting. 

### Implementation notes
- This also fixes the previous type hint of `Option.show_default`, adding `str` to the `Union`. 
- I didn't include `str` in the type of `Context.show_default` because, as far as I understand, passing a `str` makes sense for dynamic options (not for a global default).
- Added a static constant `Option.DEFAULT_SHOW_DEFAULT = False`. If you change your mind about the default, the tests I added don't require any change.

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [ ] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [x] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 1589305..93db491 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -22,6 +22,8 @@ Version 8.1.0
     without parentheses. :issue:`1359`
 -   The ``Path`` type can check whether the target is executable.
     :issue:`1961`
+-   ``Command.show_default`` overrides ``Context.show_default``, instead
+    of the other way around. :issue:`1963`
 
 
 Version 8.0.4
diff --git a/src/click/core.py b/src/click/core.py
index 3d11ab5..4b96767 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -223,9 +223,14 @@ class Context:
                   codes are used in texts that Click prints which is by
                   default not the case.  This for instance would affect
                   help output.
-    :param show_default: Show defaults for all options. If not set,
-        defaults to the value from a parent context. Overrides an
-        option's ``show_default`` argument.
+    :param show_default: Show the default value for commands. If this
+        value is not set, it defaults to the value from the parent
+        context. ``Command.show_default`` overrides this default for the
+        specific command.
+
+    .. versionchanged:: 8.1
+        The ``show_default`` parameter is overridden by
+        ``Command.show_default``, instead of the other way around.
 
     .. versionchanged:: 8.0
         The ``show_default`` parameter defaults to the value from the
@@ -2372,14 +2377,15 @@ class Option(Parameter):
 
     All other parameters are passed onwards to the parameter constructor.
 
-    :param show_default: Controls if the default value should be shown
-        on the help page. Normally, defaults are not shown. If this
-        value is a string, it shows that string in parentheses instead
-        of the actual value. This is particularly useful for dynamic
-        options. For single option boolean flags, the default remains
-        hidden if its value is ``False``.
+    :param show_default: Show the default value for this option in its
+        help text. Values are not shown by default, unless
+        :attr:`Context.show_default` is ``True``. If this value is a
+        string, it shows that string in parentheses instead of the
+        actual value. This is particularly useful for dynamic options.
+        For single option boolean flags, the default remains hidden if
+        its value is ``False``.
     :param show_envvar: Controls if an environment variable should be
-        shown on the help page. Normally, environment ariables are not
+        shown on the help page. Normally, environment variables are not
         shown.
     :param prompt: If set to ``True`` or a non empty string then the
         user will be prompted for input. If set to ``True`` the prompt
@@ -2409,6 +2415,10 @@ class Option(Parameter):
     :param help: the help string.
     :param hidden: hide this option from help outputs.
 
+    .. versionchanged:: 8.1.0
+        The ``show_default`` parameter overrides
+        ``Context.show_default``.
+
     .. versionchanged:: 8.1.0
         The default of a single option boolean flag is not shown if the
         default value is ``False``.
@@ -2422,7 +2432,7 @@ class Option(Parameter):
     def __init__(
         self,
         param_decls: t.Optional[t.Sequence[str]] = None,
-        show_default: t.Union[bool, str] = False,
+        show_default: t.Union[bool, str, None] = None,
         prompt: t.Union[bool, str] = False,
         confirmation_prompt: t.Union[bool, str] = False,
         prompt_required: bool = True,
@@ -2689,11 +2699,18 @@ class Option(Parameter):
         finally:
             ctx.resilient_parsing = resilient
 
-        show_default_is_str = isinstance(self.show_default, str)
+        show_default = False
+        show_default_is_str = False
 
-        if show_default_is_str or (
-            default_value is not None and (self.show_default or ctx.show_default)
-        ):
+        if self.show_default is not None:
+            if isinstance(self.show_default, str):
+                show_default_is_str = show_default = True
+            else:
+                show_default = self.show_default
+        elif ctx.show_default is not None:
+            show_default = ctx.show_default
+
+        if show_default_is_str or (show_default and (default_value is not None)):
             if show_default_is_str:
                 default_string = f"({self.show_default})"
             elif isinstance(default_value, (list, tuple)):


Test patch:
diff --git a/tests/test_options.py b/tests/test_options.py
index 7367a32..ab230c4 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -643,7 +643,6 @@ def test_option_custom_class_reusable(runner):
 
     # Both of the commands should have the --help option now.
     for cmd in (cmd1, cmd2):
-
         result = runner.invoke(cmd, ["--help"])
         assert "I am a help text" in result.output
         assert "you wont see me" not in result.output
@@ -795,6 +794,28 @@ def test_do_not_show_default_empty_multiple():
     assert message == "values"
 
 
+@pytest.mark.parametrize(
+    ("ctx_value", "opt_value", "expect"),
+    [
+        (None, None, False),
+        (None, False, False),
+        (None, True, True),
+        (False, None, False),
+        (False, False, False),
+        (False, True, True),
+        (True, None, True),
+        (True, False, False),
+        (True, True, True),
+        (False, "one", True),
+    ],
+)
+def test_show_default_precedence(ctx_value, opt_value, expect):
+    ctx = click.Context(click.Command("test"), show_default=ctx_value)
+    opt = click.Option("-a", default=1, help="value", show_default=opt_value)
+    help = opt.get_help_record(ctx)[1]
+    assert ("default:" in help) is expect
+
+
 @pytest.mark.parametrize(
     ("args", "expect"),
     [