Repository: pallets/click (python)

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


Pull request:
show help text with invalid default
If `ctx.resilient_parsing` is enabled, type casting can fail when getting the default, and the raw invalid value will be returned instead. Showing help text temporarily enables resilient parsing mode when fetching the default. I wasn't sure what the effect would be of enabling that for the entire help formatting, so it's just local to the default right now.

<!--
Link to relevant issues or previous PRs, one per line. Use "fixes" to
automatically close an issue.
-->

- fixes #1899

<!--
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to
each box below.

If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] 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 6d807d1..9bc6e54 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -25,6 +25,8 @@ Unreleased
     ``default_map`` lookups. When using patterns like ``AliasedGroup``,
     override ``resolve_command`` to change the name that is returned if
     needed. :issue:`1895`
+-   If a default value is invalid, it does not prevent showing help
+    text. :issue:`1889`
 
 
 Version 8.0.0
diff --git a/src/click/core.py b/src/click/core.py
index 3a7533b..7000cff 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -2196,6 +2196,10 @@ class Parameter:
         :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 @@ class Parameter:
 
             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 @@ class Option(Parameter):
                 )
                 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 @@ class Option(Parameter):
                     (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
index c35e69a..c38c1af 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -547,3 +547,20 @@ def test_summary_line(runner):
     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
index 50e6b5a..323d1c4 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -553,7 +553,7 @@ def test_option_help_preserve_paragraphs(runner):
 
 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"