Repository: pallets/click

Issue:
Set the `catch_exceptions` value for the whole runner
I always pass `catch_exceptions=False` for all my test suites.
It would be convenient to configure this value once for all in `CLIRunner` for example.

For example, we could add a `CLIRunner.catch_exceptions` parameter, and make `invoke` use that value if not directly passed in parameter of `invoke`. Then I would just have to put the runner in a pytest fixture and that would do the thing for me.

If that is OK, I can open a PR for this.

What do you think?

Hints from issue comments:
@Rowlando13: Look like catch_exceptions=True is the default option, so you should not need to pass it in. 
![image](https://github.com/user-attachments/assets/a7821496-6597-4e07-87a6-148bd302329d)
@azmeuk: My bad, I meant the other way. I edit the OP.@davidism: Might make more sense to add the param to `__init__`, and default to `None` in `invoke`. If it's `None`, it will use whatever was set in `__init__`.

Solution patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index a90ff58..1a38be8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -88,6 +88,9 @@ Unreleased
     -   Parameters cannot be required nor prompted or an error is raised.
     -   A warning will be printed when something deprecated is used.
 
+-   Add a ``catch_exceptions`` parameter to :class:`CliRunner`. If
+    ``catch_exceptions`` is not passed to :meth:`CliRunner.invoke`,
+    the value from :class:`CliRunner`. :issue:`2817` :pr:`2818`
 
 Version 8.1.8
 -------------
diff --git a/src/click/testing.py b/src/click/testing.py
index 73462db..9972d66 100644
--- a/src/click/testing.py
+++ b/src/click/testing.py
@@ -227,6 +227,11 @@ class CliRunner:
                        to `<stdout>`.  This is useful for showing examples in
                        some circumstances.  Note that regular prompts
                        will automatically echo the input.
+    :param catch_exceptions: Whether to catch any exceptions other than
+                             ``SystemExit`` when running :meth:`~CliRunner.invoke`.
+
+    .. versionchanged:: 8.2
+        Added the ``catch_exceptions`` parameter.
 
     .. versionchanged:: 8.2
         ``mix_stderr`` parameter has been removed.
@@ -237,10 +242,12 @@ class CliRunner:
         charset: str = "utf-8",
         env: cabc.Mapping[str, str | None] | None = None,
         echo_stdin: bool = False,
+        catch_exceptions: bool = True,
     ) -> None:
         self.charset = charset
         self.env: cabc.Mapping[str, str | None] = env or {}
         self.echo_stdin = echo_stdin
+        self.catch_exceptions = catch_exceptions
 
     def get_default_prog_name(self, cli: Command) -> str:
         """Given a command object it will return the default program name
@@ -410,7 +417,7 @@ class CliRunner:
         args: str | cabc.Sequence[str] | None = None,
         input: str | bytes | t.IO[t.Any] | None = None,
         env: cabc.Mapping[str, str | None] | None = None,
-        catch_exceptions: bool = True,
+        catch_exceptions: bool | None = None,
         color: bool = False,
         **extra: t.Any,
     ) -> Result:
@@ -429,7 +436,8 @@ class CliRunner:
         :param input: the input data for `sys.stdin`.
         :param env: the environment overrides.
         :param catch_exceptions: Whether to catch any other exceptions than
-                                 ``SystemExit``.
+                                 ``SystemExit``. If :data:`None`, the value
+                                 from :class:`CliRunner` is used.
         :param extra: the keyword arguments to pass to :meth:`main`.
         :param color: whether the output should contain color codes. The
                       application can still override this explicitly.
@@ -457,6 +465,9 @@ class CliRunner:
             traceback if available.
         """
         exc_info = None
+        if catch_exceptions is None:
+            catch_exceptions = self.catch_exceptions
+
         with self.isolation(input=input, env=env, color=color) as outstreams:
             return_value = None
             exception: BaseException | None = None


Test patch:
diff --git a/tests/test_testing.py b/tests/test_testing.py
index 0f2d512..55d6470 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -184,6 +184,28 @@ def test_catch_exceptions():
     assert result.exit_code == 1
 
 
+def test_catch_exceptions_cli_runner():
+    """Test that invoke `catch_exceptions` takes the value from CliRunner if not set
+    explicitly."""
+
+    class CustomError(Exception):
+        pass
+
+    @click.command()
+    def cli():
+        raise CustomError(1)
+
+    runner = CliRunner(catch_exceptions=False)
+
+    result = runner.invoke(cli, catch_exceptions=True)
+    assert isinstance(result.exception, CustomError)
+    assert type(result.exc_info) is tuple
+    assert len(result.exc_info) == 3
+
+    with pytest.raises(CustomError):
+        runner.invoke(cli)
+
+
 def test_with_color():
     @click.command()
     def cli():