Repository: pallets/click

Issue:
Arguments may break `--help`
Using `click.Path` for an argument may prevent the `--help` from showing up. A simple example is the following:

```
@click.command()
@click.option(
    "-f",
    type=click.Path(exists=True, file_okay=True),
    default="this does not exist",
    help="Output file name",
    show_default=True,
)
def cli():
    pass
```

When trying to print the `--help` the default argument will be checked for validity and instead print an error that `-f` is an invalid value.

Environment:

- Python version: Python 3.8.9
- Click version: 8.0.0



Discussion:
Hard to say whether this should be an issue. If you're going to give a default to `Path(exists=True)` that doesn't exist, you're going to have to address that anyway, as it will fail when not using `--help` too.
@davidism as an end user if you're not using `--help` it's still going to print what you need to fix. If you still don't understand the message, the current behavior blocks even looking at the `--help` to see if there's anything more that would be helpful or look at what all the arguments mean.

I understand the value will need to be addressed by the developer and potentially the end user, but how should a developer handle catching the error in the `--help` path and allow or unset defaults that might be problematic / allow printing the default as given as I did in the PR?

Regarding your comment (mentioning here just to keep the discussion in one place):

> I don't think it's the correct fix, `call=False` is used for more than generating help, ignoring errors in that case has wider potential effects.
https://github.com/pallets/click/pull/1900#issuecomment-843673138

It didn't look to be the case when I scanned the code, but I may have missed something and I understand this may be overloading the `call` argument more than is desired.

Is the only option moving either the default or the existence check into the beginning of the command? I couldn't tell if this code was new as I was simply contributing to a library which already used `click` and I was upgrading it to `click` 8 to allow it to use the typing that is provided, but I would have expected since `click` is auto-generating the `--help` it's `click`'s responsibility to _try_ to render the help as much as it can. Falling back to the default as given doesn't seem like a very big ask, but I understand it may need to be done in a different manner than I had implemented in #1900.
OK, so the issue is that you want to provide a default value that may or may not exist and show that in help text. The problem is that the processing pipeline changed to apply type casting consistently to parameter values, including when getting defaults.

Resolving patch:
diff --git a/src/click/core.py b/src/click/core.py
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -2196,6 +2196,10 @@ def get_default(
         :param call: If the default is a callable, call it. Disable to
             return the callable instead.
 
+        .. versionchanged:: 8.0.1
+            Type casting can fail in resilient parsing mode. Invalid
+            defaults will not prevent showing help text.
+
         .. versionchanged:: 8.0
             Looks at ``ctx.default_map`` first.
 
@@ -2214,7 +2218,13 @@ def get_default(
 
             value = value()
 
-        return self.type_cast_value(ctx, value)
+        try:
+            return self.type_cast_value(ctx, value)
+        except BadParameter:
+            if ctx.resilient_parsing:
+                return value
+
+            raise
 
     def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
         raise NotImplementedError()
@@ -2700,14 +2710,24 @@ def _write_opts(opts: t.Sequence[str]) -> str:
                 )
                 extra.append(_("env var: {var}").format(var=var_str))
 
-        default_value = self.get_default(ctx, call=False)
+        # Temporarily enable resilient parsing to avoid type casting
+        # failing for the default. Might be possible to extend this to
+        # help formatting in general.
+        resilient = ctx.resilient_parsing
+        ctx.resilient_parsing = True
+
+        try:
+            default_value = self.get_default(ctx, call=False)
+        finally:
+            ctx.resilient_parsing = resilient
+
         show_default_is_str = isinstance(self.show_default, str)
 
         if show_default_is_str or (
             default_value is not None and (self.show_default or ctx.show_default)
         ):
             if show_default_is_str:
-                default_string: t.Union[str, t.Any] = f"({self.show_default})"
+                default_string = f"({self.show_default})"
             elif isinstance(default_value, (list, tuple)):
                 default_string = ", ".join(str(d) for d in default_value)
             elif callable(default_value):
@@ -2719,7 +2739,7 @@ def _write_opts(opts: t.Sequence[str]) -> str:
                     (self.opts if self.default else self.secondary_opts)[0]
                 )[1]
             else:
-                default_string = default_value
+                default_string = str(default_value)
 
             extra.append(_("default: {default}").format(default=default_string))
 


Test patch:
diff --git a/tests/test_basic.py b/tests/test_basic.py
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -547,3 +547,20 @@ def cmd():
     result = runner.invoke(cli, ["--help"])
     assert "Summary line without period" in result.output
     assert "Here is a sentence." not in result.output
+
+
+def test_help_invalid_default(runner):
+    cli = click.Command(
+        "cli",
+        params=[
+            click.Option(
+                ["-a"],
+                type=click.Path(exists=True),
+                default="not found",
+                show_default=True,
+            ),
+        ],
+    )
+    result = runner.invoke(cli, ["--help"])
+    assert result.exit_code == 0
+    assert "default: not found" in result.output
diff --git a/tests/test_options.py b/tests/test_options.py
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -553,7 +553,7 @@ def cmd(config):
 
 def test_argument_custom_class(runner):
     class CustomArgument(click.Argument):
-        def get_default(self, ctx):
+        def get_default(self, ctx, call=True):
             """a dumb override of a default value for testing"""
             return "I am a default"