Repository: pallets/click

Issue:
don't type cast when getting default
Click 8 changed the processing pipeline in an effort to be more consistent. Part of that made fetching the default value happen in the same area of code as fetching the value from other sources, then processing whatever value was fetched. However, `get_default` still calls `type_cast_value`, which was a holdover from the previous code.

This causes default values to be type cast twice, once when fetching the default value, then again when processing the value. It also causes the default to be type cast when showing the help text, which can lead to unexpected output, such as the string representation of an open file object instead of the filename.

In general, custom parameter types *should* still be able to handle being passed a value of the correct type. This can still happen if the default is the correct type, or when prompting (which converts the value to the type for validation before returning), or when passing arguments to `main()` from Python instead of the command line.

Fetching the default value should behave more like the other source handled in `consume_value`. Env vars and context defaults are not type cast, so the default should not be either. This should prevent the most common source of issues with existing custom types (although they should still be updated to work as described above).

Related to:

- #1898
- #1951
- #1976
- #1999
- #2035
- #2081

There are still some other unexpected behaviors, such as type casting `flag_value`, but that will have to be handled separately.


Discussion:

Resolving patch:
diff --git a/src/click/core.py b/src/click/core.py
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -2192,6 +2192,9 @@ def get_default(
         :param call: If the default is a callable, call it. Disable to
             return the callable instead.
 
+        .. versionchanged:: 8.0.2
+            Type casting is no longer performed when getting a default.
+
         .. versionchanged:: 8.0.1
             Type casting can fail in resilient parsing mode. Invalid
             defaults will not prevent showing help text.
@@ -2207,20 +2210,10 @@ def get_default(
         if value is None:
             value = self.default
 
-        if callable(value):
-            if not call:
-                # Don't type cast the callable.
-                return value
-
+        if call and callable(value):
             value = value()
 
-        try:
-            return self.type_cast_value(ctx, value)
-        except BadParameter:
-            if ctx.resilient_parsing:
-                return value
-
-            raise
+        return value
 
     def add_to_parser(self, parser: OptionParser, ctx: Context) -> None:
         raise NotImplementedError()
@@ -2305,8 +2298,7 @@ def value_is_missing(self, value: t.Any) -> bool:
         return False
 
     def process_value(self, ctx: Context, value: t.Any) -> t.Any:
-        if value is not None:
-            value = self.type_cast_value(ctx, value)
+        value = self.type_cast_value(ctx, value)
 
         if self.required and self.value_is_missing(value):
             raise MissingParameter(ctx=ctx, param=self)


Test patch:
diff --git a/tests/test_options.py b/tests/test_options.py
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -252,7 +252,7 @@ def test_multiple_default_composite_type():
     assert isinstance(opt.type, click.Tuple)
     assert opt.type.types == [click.INT, click.STRING]
     ctx = click.Context(click.Command("test"))
-    assert opt.get_default(ctx) == ((1, "a"),)
+    assert opt.type_cast_value(ctx, opt.get_default(ctx)) == ((1, "a"),)
 
 
 def test_parse_multiple_default_composite_type(runner):
@@ -323,12 +323,27 @@ def test_intrange_default_help_text(type, expect):
 
 def test_count_default_type_help():
     """A count option with the default type should not show >=0 in help."""
-    option = click.Option(["--couunt"], count=True, help="some words")
+    option = click.Option(["--count"], count=True, help="some words")
     context = click.Context(click.Command("test"))
     result = option.get_help_record(context)[1]
     assert result == "some words"
 
 
+def test_file_type_help_default():
+    """The default for a File type is a filename string. The string
+    should be displayed in help, not an open file object.
+
+    Type casting is only applied to defaults in processing, not when
+    getting the default value.
+    """
+    option = click.Option(
+        ["--in"], type=click.File(), default=__file__, show_default=True
+    )
+    context = click.Context(click.Command("test"))
+    result = option.get_help_record(context)[1]
+    assert __file__ in result
+
+
 def test_toupper_envvar_prefix(runner):
     @click.command()
     @click.option("--arg")