Repository: pallets/click (python)

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

Pull request:
detect type from flag_value
If `type` is not given and the option is a flag, the type is detected from `flag_value` instead of falling back to `STRING`. This works even if `default=True` is given for feature flags, which simplifies `convert_type` a bit.

For 8.1, a further change to consider is for `convert_type` to return `UNPROCESSED` instead of `STRING` if the type was guessed from the default value. In that case, what should really happen is that a custom `type` should have been provided that would recognize/return the value in `default`, but converting it to `STRING` isn't correct either.

<!--
Link to relevant issues or previous PRs, one per line. Use "fixes" to
automatically close an issue.
-->

- fixes #1886

<!--
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 4438c15..7f103d0 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -16,6 +16,8 @@ Unreleased
     3.8. :issue:`1889`
 -   Arguments with ``nargs=-1`` only use env var value if no command
     line values are given. :issue:`1903`
+-   Flag options guess their type from ``flag_value`` if given, like
+    regular options do from ``default``. :issue:`1886`
 
 
 Version 8.0.0
diff --git a/src/click/core.py b/src/click/core.py
index ab325b2..63506ba 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -13,6 +13,7 @@ from gettext import gettext as _
 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 .parser import split_opt
 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 @@ class Parameter:
         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 @@ class Option(Parameter):
         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 @@ class Option(Parameter):
         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 @@ class Option(Parameter):
 
             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
index 86aaae0..21f0e4f 100644
--- 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
index 94e5eb8..50e6b5a 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -756,3 +756,10 @@ def test_option_with_optional_value(runner, args, expect):
 
     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