Repository: pallets/click (python)

Issue:
`required=True` does not work for flags
When using a flag option, setting `required=True` has no effect unless I also include `default=None`. I would expect there to be no default if the flag is required.


**Steps to reproduce:**
```
import click

@click.command()
@click.option('--foo/--no_foo', is_flag=True, required=True)
def bar(foo):
    pass

if __name__ == '__main__':
    bar()

```

`python test.py` runs without raising an error.

I would expect `python test.py` to raise:
```
Usage: test.py [OPTIONS]
Try 'test.py --help' for help.

Error: Missing option '--foo'.
```

Environment:

- Python version: 3.9.5
- Click version: 8.0.1


Pull request:
Enforce required flag
- fixes #1978. Feel like the best way to resolve is to _not_ set a "required" flag to False if not present

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [ ] 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.
- [ ] 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 a01dd43..35307d4 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -31,6 +31,8 @@ Version 8.1.0
     cleaned the same as using the ``@option`` and ``@command``
     decorators does. A command's ``epilog`` and ``short_help`` are also
     processed. :issue:`1985`
+-   A flag option with ``required=True`` requires that the flag is
+    passed instead of choosing the implicit default value. :issue:`1978`
 
 
 Version 8.0.4
diff --git a/src/click/core.py b/src/click/core.py
index 97d699e..1ab2b20 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -2511,7 +2511,7 @@ class Option(Parameter):
             # flag if flag_value is set.
             self._flag_needs_value = flag_value is not None
 
-        if is_flag and default_is_missing:
+        if is_flag and default_is_missing and not self.required:
             self.default: t.Union[t.Any, t.Callable[[], t.Any]] = False
 
         if flag_value is None:


Test patch:
diff --git a/tests/test_options.py b/tests/test_options.py
index ab230c4..3beff11 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -502,6 +502,15 @@ def test_missing_option_string_cast():
     assert str(excinfo.value) == "Missing parameter: a"
 
 
+def test_missing_required_flag(runner):
+    cli = click.Command(
+        "cli", params=[click.Option(["--on/--off"], is_flag=True, required=True)]
+    )
+    result = runner.invoke(cli)
+    assert result.exit_code == 2
+    assert "Error: Missing option '--on'." in result.output
+
+
 def test_missing_choice(runner):
     @click.command()
     @click.option("--foo", type=click.Choice(["foo", "bar"]), required=True)