Repository: pallets/click

Issue:
flag_value converted to str in Click 8
Consider the following code:

```python
import click

@click.command()
@click.option("--answer", flag_value=42)
def main(answer):
    click.echo(repr(answer))

if __name__ == '__main__':
    main()
```

Under click 7, passing the `--answer` option to this script prints `42` (an int), as expected.  Under click 8, it prints `'42'` (a str).

Environment:

- Python version: 3.9.5
- Click version: 7.1.2 vs. 8.0.0


Discussion:
The default type of parameters is `str`. You need to specify `type=int` if you want int values.
@davidism OK, but in my actual use case, the `flag_value` is an instance of a class with a zero-argument constructor.  How am I expected to get that to work?  Trying `@click.option("--answer", is_flag=True, type=Klass)` (and also with `flag_value=None`) results in a `Klass() takes no arguments` TypeError.
Then it should use `type=click.UNPROCESSED` to pass the value through unchanged.
I think what's affecting this is that we now apply type casting consistently to any value coming through the pipeline. I wonder if setting the type to unprocessed automatically for flag values is safe.
@davidism Using `type=click.UNPROCESSED` works, thanks.
> The most basic option is a value option. These options accept one argument which is a value. *If no type is provided, the type of the default value is used.*

Click 8.0.0 appears to ignore the type of the default value of options if `type` is not given:

```python
bugval = functools.partial(range, 10)

@click.command()
@click.option('-b', 'bug', default=bugval, flag_value=bugval)
def cli(bug):
    print("type of bug:", type(bug), "value of bug", bug)
```

```bash
$ python click_bug.py   
type of bug: <class 'str'> value of bug functools.partial(<class 'range'>, 10)
```

I'm sorry, I can't tell what you're trying to demonstrate with that example. It seems at best tangentially related to this issue, but it's not clear at all.
Sorry, I thought the example was fairly obvious: as per the documentation (quoted from 8.0.0), and as it works in 7.1.2, Click should infer the type of the value from what's given to `default`, but in 8.0.0 the type of the `default` value is ignored and the flag value is cast to `str` instead.

This is pretty clearly a bug; the same bug for which the issue was opened, in fact.
The default you gave is dynamic, so it wouldn't be able to infer a type anyway.
It's not dynamic, it's the value of `bugval` - a `functools.partial` object.
Which is a callable, not a literal value.
A callable is dynamic...
A `functools.partial` is a callable, literal value that has a type (`functools.partial`, specifically.)
@crashfrog sorry, it sounds like we're having trouble explaining the difference. You're going to have to trust us that there is, in fact, a difference, this tangent is derailing this issue.
I'm not sure what you're having trouble explaining - in 7.1.2 it works to pass callable values as `default` and `flag_value`, which is extremely useful for things like flags that set output formats (TSV vs. JSON vs. XML, for instance.)

In 8.0.0 this behavior is broken because `click.option` now ignores the `default` argument's type and converts callable values to strings when there's no type argument. Since this isn't described in the documentation and it's a breaking change from 7.1.2, it's pretty obviously a bug and it's exactly the bug described in the issue; this is an example of it.

https://click.palletsprojects.com/en/8.0.x/changes/#version-8-0-0

Here's the patch notes; I don't see where it says "we now convert all callable values to `str`" or something, and even if that were in the notes, that's undesirable behavior.

Resolving patch:
diff --git a/src/click/core.py b/src/click/core.py
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -13,6 +13,7 @@
 from gettext import ngettext
 from itertools import repeat
 
+from . import types
 from ._unicodefun import _verify_python_env
 from .exceptions import Abort
 from .exceptions import BadParameter
@@ -30,11 +31,6 @@
 from .termui import confirm
 from .termui import prompt
 from .termui import style
-from .types import _NumberRangeBase
-from .types import BOOL
-from .types import convert_type
-from .types import IntRange
-from .types import ParamType
 from .utils import _detect_program_name
 from .utils import _expand_args
 from .utils import echo
@@ -2010,7 +2006,7 @@ class Parameter:
     def __init__(
         self,
         param_decls: t.Optional[t.Sequence[str]] = None,
-        type: t.Optional[t.Union["ParamType", t.Any]] = None,
+        type: t.Optional[t.Union[types.ParamType, t.Any]] = None,
         required: bool = False,
         default: t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]] = None,
         callback: t.Optional[t.Callable[[Context, "Parameter", t.Any], t.Any]] = None,
@@ -2035,8 +2031,7 @@ def __init__(
         self.name, self.opts, self.secondary_opts = self._parse_decls(
             param_decls or (), expose_value
         )
-
-        self.type = convert_type(type, default)
+        self.type = types.convert_type(type, default)
 
         # Default nargs to what the type tells us if we have that
         # information available.
@@ -2439,6 +2434,9 @@ class Option(Parameter):
                                context.
     :param help: the help string.
     :param hidden: hide this option from help outputs.
+
+    .. versionchanged:: 8.0.1
+        ``type`` is detected from ``flag_value`` if given.
     """
 
     param_type_name = "option"
@@ -2456,7 +2454,7 @@ def __init__(
         multiple: bool = False,
         count: bool = False,
         allow_from_autoenv: bool = True,
-        type: t.Optional[t.Union["ParamType", t.Any]] = None,
+        type: t.Optional[t.Union[types.ParamType, t.Any]] = None,
         help: t.Optional[str] = None,
         hidden: bool = False,
         show_choices: bool = True,
@@ -2507,20 +2505,20 @@ def __init__(
         if flag_value is None:
             flag_value = not self.default
 
+        if is_flag and type is None:
+            # Re-guess the type from the flag value instead of the
+            # default.
+            self.type = types.convert_type(None, flag_value)
+
         self.is_flag: bool = is_flag
+        self.is_bool_flag = isinstance(self.type, types.BoolParamType)
         self.flag_value: t.Any = flag_value
 
-        if self.is_flag and isinstance(self.flag_value, bool) and type in [None, bool]:
-            self.type: "ParamType" = BOOL
-            self.is_bool_flag = True
-        else:
-            self.is_bool_flag = False
-
         # Counting
         self.count = count
         if count:
             if type is None:
-                self.type = IntRange(min=0)
+                self.type = types.IntRange(min=0)
             if default_is_missing:
                 self.default = 0
 
@@ -2725,7 +2723,7 @@ def _write_opts(opts: t.Sequence[str]) -> str:
 
             extra.append(_("default: {default}").format(default=default_string))
 
-        if isinstance(self.type, _NumberRangeBase):
+        if isinstance(self.type, types._NumberRangeBase):
             range_str = self.type._describe_range()
 
             if range_str:
diff --git a/src/click/types.py b/src/click/types.py
--- a/src/click/types.py
+++ b/src/click/types.py
@@ -1000,10 +1000,7 @@ def convert_type(ty: t.Optional[t.Any], default: t.Optional[t.Any] = None) -> Pa
     if ty is float:
         return FLOAT
 
-    # Booleans are only okay if not guessed. For is_flag options with
-    # flag_value, default=True indicates which flag_value is the
-    # default.
-    if ty is bool and not guessed_type:
+    if ty is bool:
         return BOOL
 
     if guessed_type:


Test patch:
diff --git a/tests/test_options.py b/tests/test_options.py
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -756,3 +756,10 @@ def cli(opt, a, b):
 
     result = runner.invoke(cli, args, standalone_mode=False, catch_exceptions=False)
     assert result.return_value == expect
+
+
+def test_type_from_flag_value():
+    param = click.Option(["-a", "x"], default=True, flag_value=4)
+    assert param.type is click.INT
+    param = click.Option(["-b", "x"], flag_value=8)
+    assert param.type is click.INT