Repository: pypa/pipx (python)

Issue:
Unhandled exception with multiple `--quiet` flags
**Describe the bug**
Giving `-q` more than twice to commands like `list` gives unhandled exception:

```
$ pipx list -qqq
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/logging/config.py", line 564, in configure
    handler = self.configure_handler(handlers[name])
  File "/usr/local/lib/python3.9/logging/config.py", line 758, in configure_handler
    result.setLevel(logging._checkLevel(level))
  File "/usr/local/lib/python3.9/logging/__init__.py", line 198, in _checkLevel
    raise ValueError("Unknown level: %r" % level)
ValueError: Unknown level: 'Level 60'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/bin/pipx", line 8, in <module>
    sys.exit(cli())
  File "/usr/local/lib/python3.9/site-packages/pipx/main.py", line 916, in cli
    setup(parsed_pipx_args)
  File "/usr/local/lib/python3.9/site-packages/pipx/main.py", line 856, in setup
    setup_logging(verbose)
  File "/usr/local/lib/python3.9/site-packages/pipx/main.py", line 846, in setup_logging
    logging.config.dictConfig(logging_config)
  File "/usr/local/lib/python3.9/logging/config.py", line 809, in dictConfig
    dictConfigClass(config).configure()
  File "/usr/local/lib/python3.9/logging/config.py", line 571, in configure
    raise ValueError('Unable to configure handler '
ValueError: Unable to configure handler 'stream'
```

**How to reproduce**

`pipx list -qq` works fine, `pipx list -qqq` does not.

**Expected behavior**

Either ignore the superfluous `q`-flags, or print a notice about the number of times this subcommand accepts the flag. 

Pull request description:
limit verbosity to supported range
<!-- add an 'x' in the brackets below -->

- [x] I have added a news fragment under `changelog.d/` (if the patch affects the end users)

## Summary of changes
Fixes #1283 by clamping the possible verbosity value.

## Test plan

<!-- provide evidence of testing, preferably with command(s) that can be copy+pasted by others -->

Tested by running

```
pipx list -qqq
pipx list -vvv
```


Patch:
diff --git a/changelog.d/1283.bugfix.md b/changelog.d/1283.bugfix.md
new file mode 100644
index 0000000..482f8af
--- /dev/null
+++ b/changelog.d/1283.bugfix.md
@@ -0,0 +1,1 @@
+Fix crashes due to superfluous `-q ` flags by discarding exceeding values
diff --git a/src/pipx/main.py b/src/pipx/main.py
index f3f4859..9e43784 100644
--- a/src/pipx/main.py
+++ b/src/pipx/main.py
@@ -758,11 +758,20 @@ def get_command_parser() -> Tuple[argparse.ArgumentParser, Dict[str, argparse.Ar
         default=0,
         help=(
             "Give less output. May be used multiple times corresponding to the"
-            " WARNING, ERROR, and CRITICAL logging levels."
+            " ERROR and CRITICAL logging levels. The count maxes out at 2."
         ),
     )
 
-    shared_parser.add_argument("--verbose", "-v", action="count", default=0, help=("Give more output."))
+    shared_parser.add_argument(
+        "--verbose",
+        "-v",
+        action="count",
+        default=0,
+        help=(
+            "Give more output. May be used multiple times corresponding to the"
+            " INFO, DEBUG and NOTSET logging levels. The count maxes out at 3."
+        ),
+    )
 
     parser = argparse.ArgumentParser(
         prog=prog_name(),
@@ -843,8 +852,8 @@ def setup_logging(verbose: int) -> None:
     pipx_str = bold(green("pipx >")) if sys.stdout.isatty() else "pipx >"
     pipx.constants.pipx_log_file = setup_log_file()
 
-    # Determine logging level
-    level_number = max(0, logging.WARNING - 10 * verbose)
+    # Determine logging level, a value between 0 and 50
+    level_number = min(max(0, logging.WARNING - 10 * verbose), 50)
 
     level = logging.getLevelName(level_number)
 


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index 3a8bd53..a8e2a43 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -35,3 +35,8 @@ def test_prog_name(monkeypatch, argv, executable, expected):
     monkeypatch.setattr("pipx.main.sys.argv", [argv])
     monkeypatch.setattr("pipx.main.sys.executable", executable)
     assert main.prog_name() == expected
+
+
+def test_limit_verbosity():
+    assert not run_pipx_cli(["list", "-qqq"])
+    assert not run_pipx_cli(["list", "-vvvv"])