Repository: pylint-dev/pylint

Issue:
Undocumented `--colorized` option for pyreverse in 3.2.0-dev0 docs
### Bug description

In latest docs (3.2.0-dev-0) (at time of writing) there was no documentation for the --colorized option:
https://pylint.readthedocs.io/en/latest/pyreverse.html

I raise this issue because we've had a PR opened to integrate the option in a down-stream tool:
[sphinx-pyreverse](https://github.com/alendit/sphinx-pyreverse)


### Configuration

_No response_

### Command used

```shell
open https://pylint.readthedocs.io/en/latest/pyreverse.html
```


### Pylint output

```python
n/a
```


### Expected behavior

That the feature be documented :)

### Pylint version

```shell
pylint 3.2.0-dev
```


### OS / Environment

n/a

### Additional dependencies

```python
n/a
```



Discussion:

Patch:
diff --git a/doc/additional_tools/pyreverse/configuration.rst b/doc/additional_tools/pyreverse/configuration.rst
new file mode 100644
index 0000000000..aa42e8c9a5
--- /dev/null
+++ b/doc/additional_tools/pyreverse/configuration.rst
@@ -0,0 +1,169 @@
+.. This file is auto-generated. Make any changes to the associated
+.. docs extension in 'doc/exts/pyreverse_configuration.py'.
+
+
+
+Pyreverse Configuration
+^^^^^^^^^^^^^^^^^^^^^^^
+
+
+Filtering and Scope
+-------------------
+
+--all-ancestors
+"""""""""""""""
+*Show all ancestors of all classes in <projects>.*
+
+**Default:**  ``None``
+
+
+--all-associated
+""""""""""""""""
+*Show all classes associated with the target classes, including indirect associations.*
+
+**Default:**  ``None``
+
+
+--class
+"""""""
+*Create a class diagram with all classes related to <class>; this uses by default the options -ASmy*
+
+**Default:**  ``None``
+
+
+--filter-mode
+"""""""""""""
+*Filter attributes and functions according to <mode>. Correct modes are:
+'PUB_ONLY' filter all non public attributes [DEFAULT], equivalent to PRIVATE+SPECIAL
+'ALL' no filter
+'SPECIAL' filter Python special functions except constructor
+'OTHER' filter protected and private attributes*
+
+**Default:**  ``PUB_ONLY``
+
+
+--show-ancestors
+""""""""""""""""
+*Show <ancestor> generations of ancestor classes not in <projects>.*
+
+**Default:**  ``None``
+
+
+--show-associated
+"""""""""""""""""
+*Show <association_level> levels of associated classes not in <projects>.*
+
+**Default:**  ``None``
+
+
+--show-builtin
+""""""""""""""
+*Include builtin objects in representation of classes.*
+
+**Default:**  ``False``
+
+
+--show-stdlib
+"""""""""""""
+*Include standard library objects in representation of classes.*
+
+**Default:**  ``False``
+
+
+
+
+Display Options
+---------------
+
+--color-palette
+"""""""""""""""
+*Comma separated list of colors to use for the package depth coloring.*
+
+**Default:**  ``('#77AADD', '#99DDFF', '#44BB99', '#BBCC33', '#AAAA00', '#EEDD88', '#EE8866', '#FFAABB', '#DDDDDD')``
+
+
+--colorized
+"""""""""""
+*Use colored output. Classes/modules of the same package get the same color.*
+
+**Default:**  ``False``
+
+
+--max-color-depth
+"""""""""""""""""
+*Use separate colors up to package depth of <depth>. Higher depths will reuse colors.*
+
+**Default:**  ``2``
+
+
+--module-names
+""""""""""""""
+*Include module name in the representation of classes.*
+
+**Default:**  ``None``
+
+
+--no-standalone
+"""""""""""""""
+*Only show nodes with connections.*
+
+**Default:**  ``False``
+
+
+--only-classnames
+"""""""""""""""""
+*Don't show attributes and methods in the class boxes; this disables -f values.*
+
+**Default:**  ``False``
+
+
+
+
+Output Control
+--------------
+
+--output
+""""""""
+*Create a *.<format> output file if format is available. Available formats are: .dot, .puml, .plantuml, .mmd, .html. Any other format will be tried to be created by using the 'dot' command line tool, which requires a graphviz installation. In this case, these additional formats are available (see `Graphviz output formats <https://graphviz.org/docs/outputs/>`_).*
+
+**Default:**  ``dot``
+
+
+--output-directory
+""""""""""""""""""
+*Set the output directory path.*
+
+**Default:** ``""``
+
+
+
+
+Project Configuration
+---------------------
+
+--ignore
+""""""""
+*Files or directories to be skipped. They should be base names, not paths.*
+
+**Default:**  ``('CVS',)``
+
+
+--project
+"""""""""
+*Set the project name. This will later be appended to the output file names.*
+
+**Default:** ``""``
+
+
+--source-roots
+""""""""""""""
+*Add paths to the list of the source roots. Supports globbing patterns. The source root is an absolute path or a path relative to the current working directory used to determine a package namespace for modules located under the source root.*
+
+**Default:**  ``()``
+
+
+--verbose
+"""""""""
+*Makes pyreverse more verbose/talkative. Mostly useful for debugging.*
+
+**Default:**  ``False``
diff --git a/doc/pyreverse.rst b/doc/additional_tools/pyreverse/index.rst
similarity index 89%
rename from doc/pyreverse.rst
rename to doc/additional_tools/pyreverse/index.rst
index 7595683d75..41f74731dc 100644
--- a/doc/pyreverse.rst
+++ b/doc/additional_tools/pyreverse/index.rst
@@ -22,7 +22,6 @@ To see a full list of the available options, run::
 
    pyreverse -h
 
-
 Example Output
 ''''''''''''''
 
@@ -31,7 +30,7 @@ Example diagrams generated with the ``.puml`` output format are shown below.
 Class Diagram
 .............
 
-.. image:: media/pyreverse_example_classes.png
+.. image:: ../../media/pyreverse_example_classes.png
    :width: 625
    :height: 589
    :alt: Class diagram generated by pyreverse
@@ -41,7 +40,7 @@ Class Diagram
 Package Diagram
 ...............
 
-.. image:: media/pyreverse_example_packages.png
+.. image:: ../../media/pyreverse_example_packages.png
    :width: 344
    :height: 177
    :alt: Package diagram generated by pyreverse
@@ -60,8 +59,14 @@ For example, running::
 
 will generate the full class and package diagrams for ``pylint``, but will additionally generate a file ``pylint.checkers.classes.ClassChecker.dot``:
 
-.. image:: media/ClassChecker_diagram.png
+.. image:: ../../media/ClassChecker_diagram.png
    :width: 757
    :height: 1452
    :alt: Package diagram generated by pyreverse
    :align: center
+
+.. toctree::
+  :maxdepth: 1
+  :hidden:
+
+  configuration
diff --git a/doc/symilar.rst b/doc/additional_tools/symilar/index.rst
similarity index 100%
rename from doc/symilar.rst
rename to doc/additional_tools/symilar/index.rst
diff --git a/doc/conf.py b/doc/conf.py
index 50d891eb93..741d1975c1 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -44,6 +44,7 @@
     "pylint_extensions",
     "pylint_messages",
     "pylint_options",
+    "pyreverse_configuration",
     "sphinx.ext.autosectionlabel",
     "sphinx.ext.intersphinx",
     "sphinx_reredirects",
diff --git a/doc/exts/pyreverse_configuration.py b/doc/exts/pyreverse_configuration.py
new file mode 100644
index 0000000000..f4d77ef216
--- /dev/null
+++ b/doc/exts/pyreverse_configuration.py
@@ -0,0 +1,75 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
+
+"""Script used to generate the pyreverse configuration page."""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import NamedTuple
+
+from sphinx.application import Sphinx
+
+from pylint.pyreverse.main import OPTIONS_GROUPS, Run
+from pylint.typing import OptionDict
+from pylint.utils import get_rst_title
+
+
+class OptionsData(NamedTuple):
+    name: str
+    optdict: OptionDict
+
+
+PYREVERSE_PATH = (
+    Path(__file__).resolve().parent.parent / "additional_tools" / "pyreverse"
+)
+"""Path to the pyreverse documentation folder."""
+
+
+def _write_config_page(run: Run) -> None:
+    """Create or overwrite the configuration page."""
+    sections: list[str] = [
+        ".. This file is auto-generated. Make any changes to the associated\n"
+        ".. docs extension in 'doc/exts/pyreverse_configuration.py'.\n\n",
+        get_rst_title("Pyreverse Configuration", "^"),
+    ]
+
+    options: list[OptionsData] = [OptionsData(name, info) for name, info in run.options]
+    option_groups: dict[str, list[str]] = {g: [] for g in OPTIONS_GROUPS.values()}
+
+    for option in sorted(options, key=lambda x: x.name):
+        option_string = get_rst_title(f"--{option.name}", '"')
+        option_string += f"*{option.optdict.get('help')}*\n\n"
+
+        if option.optdict.get("default") == "":
+            option_string += '**Default:** ``""``\n\n\n'
+        else:
+            option_string += f"**Default:**  ``{option.optdict.get('default')}``\n\n\n"
+
+        option_groups[str(option.optdict.get("group"))].append(option_string)
+
+    for group_title in OPTIONS_GROUPS.values():
+        sections.append(
+            get_rst_title(group_title, "-") + "\n" + "".join(option_groups[group_title])
+        )
+
+    # Join all sections and remove the final two newlines
+    final_page = "\n\n".join(sections)[:-2]
+
+    with open(PYREVERSE_PATH / "configuration.rst", "w", encoding="utf-8") as stream:
+        stream.write(final_page)
+
+
+# pylint: disable-next=unused-argument
+def build_options_page(app: Sphinx | None) -> None:
+    # Write configuration page
+    _write_config_page(Run([]))
+
+
+def setup(app: Sphinx) -> dict[str, bool]:
+    """Connects the extension to the Sphinx process."""
+    # Register callback at the builder-inited Sphinx event
+    # See https://www.sphinx-doc.org/en/master/extdev/appapi.html
+    app.connect("builder-inited", build_options_page)
+    return {"parallel_read_safe": True}
diff --git a/doc/index.rst b/doc/index.rst
index 1e2c8f044a..8bcdeac8bb 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -33,8 +33,8 @@
    :titlesonly:
    :hidden:
 
-   pyreverse
-   symilar
+   additional_tools/pyreverse/index
+   additional_tools/symilar/index
 
 .. toctree::
    :caption: Changelog
diff --git a/doc/whatsnew/fragments/9689.breaking b/doc/whatsnew/fragments/9689.breaking
new file mode 100644
index 0000000000..5b175304a5
--- /dev/null
+++ b/doc/whatsnew/fragments/9689.breaking
@@ -0,0 +1,7 @@
+`pyreverse` `Run` was changed to no longer call `sys.exit()` in its `__init__`.
+You should now call `Run(args).run()` which will return the exit code instead.
+Having a class that always raised a `SystemExit` exception was considered a bug.
+
+Normal usage of pyreverse through the CLI will not be affected by this change.
+
+Refs #9689
diff --git a/pylint/__init__.py b/pylint/__init__.py
index 74bde8a394..d3ddf71f62 100644
--- a/pylint/__init__.py
+++ b/pylint/__init__.py
@@ -53,7 +53,7 @@ def run_pyreverse(argv: Sequence[str] | None = None) -> NoReturn:
     """
     from pylint.pyreverse.main import Run as PyreverseRun
 
-    PyreverseRun(argv or sys.argv[1:])
+    sys.exit(PyreverseRun(argv or sys.argv[1:]).run())
 
 
 def run_symilar(argv: Sequence[str] | None = None) -> NoReturn:
diff --git a/pylint/pyreverse/inspector.py b/pylint/pyreverse/inspector.py
index 8825363fa7..40fb8d6079 100644
--- a/pylint/pyreverse/inspector.py
+++ b/pylint/pyreverse/inspector.py
@@ -13,7 +13,7 @@
 import os
 import traceback
 from abc import ABC, abstractmethod
-from collections.abc import Callable
+from collections.abc import Callable, Sequence
 from typing import Optional
 
 import astroid
@@ -346,7 +346,7 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
 
 
 def project_from_files(
-    files: list[str],
+    files: Sequence[str],
     func_wrapper: _WrapperFuncT = _astroid_wrapper,
     project_name: str = "no name",
     black_list: tuple[str, ...] = constants.DEFAULT_IGNORE_LIST,
diff --git a/pylint/pyreverse/main.py b/pylint/pyreverse/main.py
index 3ba0b6c77d..972a46741f 100644
--- a/pylint/pyreverse/main.py
+++ b/pylint/pyreverse/main.py
@@ -8,7 +8,6 @@
 
 import sys
 from collections.abc import Sequence
-from typing import NoReturn
 
 from pylint import constants
 from pylint.config.arguments_manager import _ArgumentsManager
@@ -46,7 +45,17 @@
     "#DDDDDD",  # pale grey
 )
 
+
+OPTIONS_GROUPS = {
+    "FILTERING": "Filtering and Scope",
+    "DISPLAY": "Display Options",
+    "OUTPUT": "Output Control",
+    "PROJECT": "Project Configuration",
+}
+
+
 OPTIONS: Options = (
+    # Filtering and Scope options
     (
         "filter-mode",
         {
@@ -56,15 +65,12 @@
             "type": "string",
             "action": "store",
             "metavar": "<mode>",
-            "help": """filter attributes and functions according to
-    <mode>. Correct modes are :
-                            'PUB_ONLY' filter all non public attributes
-                                [DEFAULT], equivalent to PRIVATE+SPECIAL_A
-                            'ALL' no filter
-                            'SPECIAL' filter Python special functions
-                                except constructor
-                            'OTHER' filter protected and private
-                                attributes""",
+            "group": OPTIONS_GROUPS["FILTERING"],
+            "help": """Filter attributes and functions according to <mode>. Correct modes are:
+'PUB_ONLY' filter all non public attributes [DEFAULT], equivalent to PRIVATE+SPECIAL
+'ALL' no filter
+'SPECIAL' filter Python special functions except constructor
+'OTHER' filter protected and private attributes""",
         },
     ),
     (
@@ -76,7 +82,8 @@
             "type": "csv",
             "dest": "classes",
             "default": None,
-            "help": "create a class diagram with all classes related to <class>;\
+            "group": OPTIONS_GROUPS["FILTERING"],
+            "help": "Create a class diagram with all classes related to <class>;\
  this uses by default the options -ASmy",
         },
     ),
@@ -88,7 +95,8 @@
             "metavar": "<ancestor>",
             "type": "int",
             "default": None,
-            "help": "show <ancestor> generations of ancestor classes not in <projects>",
+            "group": OPTIONS_GROUPS["FILTERING"],
+            "help": "Show <ancestor> generations of ancestor classes not in <projects>.",
         },
     ),
     (
@@ -97,7 +105,8 @@
             "short": "A",
             "default": None,
             "action": "store_true",
-            "help": "show all ancestors off all classes in <projects>",
+            "group": OPTIONS_GROUPS["FILTERING"],
+            "help": "Show all ancestors of all classes in <projects>.",
         },
     ),
     (
@@ -108,7 +117,8 @@
             "metavar": "<association_level>",
             "type": "int",
             "default": None,
-            "help": "show <association_level> levels of associated classes not in <projects>",
+            "group": OPTIONS_GROUPS["FILTERING"],
+            "help": "Show <association_level> levels of associated classes not in <projects>.",
         },
     ),
     (
@@ -117,7 +127,8 @@
             "short": "S",
             "default": None,
             "action": "store_true",
-            "help": "show recursively all associated off all associated classes",
+            "group": OPTIONS_GROUPS["FILTERING"],
+            "help": "Show all classes associated with the target classes, including indirect associations.",
         },
     ),
     (
@@ -126,7 +137,8 @@
             "short": "b",
             "action": "store_true",
             "default": False,
-            "help": "include builtin objects in representation of classes",
+            "group": OPTIONS_GROUPS["FILTERING"],
+            "help": "Include builtin objects in representation of classes.",
         },
     ),
     (
@@ -135,9 +147,11 @@
             "short": "L",
             "action": "store_true",
             "default": False,
-            "help": "include standard library objects in representation of classes",
+            "group": OPTIONS_GROUPS["FILTERING"],
+            "help": "Include standard library objects in representation of classes.",
         },
     ),
+    # Display Options
     (
         "module-names",
         {
@@ -145,7 +159,8 @@
             "default": None,
             "type": "yn",
             "metavar": "<y or n>",
-            "help": "include module name in representation of classes",
+            "group": OPTIONS_GROUPS["DISPLAY"],
+            "help": "Include module name in the representation of classes.",
         },
     ),
     (
@@ -154,7 +169,8 @@
             "short": "k",
             "action": "store_true",
             "default": False,
-            "help": "don't show attributes and methods in the class boxes; this disables -f values",
+            "group": OPTIONS_GROUPS["DISPLAY"],
+            "help": "Don't show attributes and methods in the class boxes; this disables -f values.",
         },
     ),
     (
@@ -162,24 +178,8 @@
         {
             "action": "store_true",
             "default": False,
-            "help": "only show nodes with connections",
-        },
-    ),
-    (
-        "output",
-        {
-            "short": "o",
-            "dest": "output_format",
-            "action": "store",
-            "default": "dot",
-            "metavar": "<format>",
-            "type": "string",
-            "help": (
-                "create a *.<format> output file if format is available. Available "
-                f"formats are: {', '.join(DIRECTLY_SUPPORTED_FORMATS)}. Any other "
-                f"format will be tried to create by means of the 'dot' command line "
-                f"tool, which requires a graphviz installation."
-            ),
+            "group": OPTIONS_GROUPS["DISPLAY"],
+            "help": "Only show nodes with connections.",
         },
     ),
     (
@@ -188,6 +188,7 @@
             "dest": "colorized",
             "action": "store_true",
             "default": False,
+            "group": OPTIONS_GROUPS["DISPLAY"],
             "help": "Use colored output. Classes/modules of the same package get the same color.",
         },
     ),
@@ -199,7 +200,8 @@
             "default": 2,
             "metavar": "<depth>",
             "type": "int",
-            "help": "Use separate colors up to package depth of <depth>",
+            "group": OPTIONS_GROUPS["DISPLAY"],
+            "help": "Use separate colors up to package depth of <depth>. Higher depths will reuse colors.",
         },
     ),
     (
@@ -210,9 +212,43 @@
             "default": DEFAULT_COLOR_PALETTE,
             "metavar": "<color1,color2,...>",
             "type": "csv",
-            "help": "Comma separated list of colors to use",
+            "group": OPTIONS_GROUPS["DISPLAY"],
+            "help": "Comma separated list of colors to use for the package depth coloring.",
         },
     ),
+    # Output Control options
+    (
+        "output",
+        {
+            "short": "o",
+            "dest": "output_format",
+            "action": "store",
+            "default": "dot",
+            "metavar": "<format>",
+            "type": "string",
+            "group": OPTIONS_GROUPS["OUTPUT"],
+            "help": (
+                "Create a *.<format> output file if format is available. Available "
+                f"formats are: {', '.join('.' + fmt for fmt in DIRECTLY_SUPPORTED_FORMATS)}. Any other "
+                "format will be tried to be created by using the 'dot' command line "
+                "tool, which requires a graphviz installation. In this case, these additional "
+                "formats are available (see `Graphviz output formats <https://graphviz.org/docs/outputs/>`_)."
+            ),
+        },
+    ),
+    (
+        "output-directory",
+        {
+            "default": "",
+            "type": "path",
+            "short": "d",
+            "action": "store",
+            "metavar": "<output_directory>",
+            "group": OPTIONS_GROUPS["OUTPUT"],
+            "help": "Set the output directory path.",
+        },
+    ),
+    # Project Configuration options
     (
         "ignore",
         {
@@ -220,6 +256,7 @@
             "metavar": "<file[,file...]>",
             "dest": "ignore_list",
             "default": constants.DEFAULT_IGNORE_LIST,
+            "group": OPTIONS_GROUPS["PROJECT"],
             "help": "Files or directories to be skipped. They should be base names, not paths.",
         },
     ),
@@ -230,18 +267,8 @@
             "type": "string",
             "short": "p",
             "metavar": "<project name>",
-            "help": "set the project name.",
-        },
-    ),
-    (
-        "output-directory",
-        {
-            "default": "",
-            "type": "path",
-            "short": "d",
-            "action": "store",
-            "metavar": "<output_directory>",
-            "help": "set the output directory path.",
+            "group": OPTIONS_GROUPS["PROJECT"],
+            "help": "Set the project name. This will later be appended to the output file names.",
         },
     ),
     (
@@ -250,6 +277,7 @@
             "type": "glob_paths_csv",
             "metavar": "<path>[,<path>...]",
             "default": (),
+            "group": OPTIONS_GROUPS["PROJECT"],
             "help": "Add paths to the list of the source roots. Supports globbing patterns. The "
             "source root is an absolute path or a path relative to the current working directory "
             "used to determine a package namespace for modules located under the source root.",
@@ -260,19 +288,19 @@
         {
             "action": "store_true",
             "default": False,
+            "group": OPTIONS_GROUPS["PROJECT"],
             "help": "Makes pyreverse more verbose/talkative. Mostly useful for debugging.",
         },
     ),
 )
 
 
+# Base class providing common behaviour for pyreverse commands
 class Run(_ArgumentsManager, _ArgumentsProvider):
-    """Base class providing common behaviour for pyreverse commands."""
-
     options = OPTIONS
     name = "pyreverse"
 
-    def __init__(self, args: Sequence[str]) -> NoReturn:
+    def __init__(self, args: Sequence[str]) -> None:
         # Immediately exit if user asks for version
         if "--version" in args:
             print("pyreverse is included in pylint:")
@@ -284,7 +312,7 @@ def __init__(self, args: Sequence[str]) -> NoReturn:
 
         # Parse options
         insert_default_options()
-        args = self._parse_command_line_configuration(args)
+        self.args = self._parse_command_line_configuration(args)
 
         if self.config.output_format not in DIRECTLY_SUPPORTED_FORMATS:
             check_graphviz_availability()
@@ -294,19 +322,17 @@ def __init__(self, args: Sequence[str]) -> NoReturn:
             )
             check_if_graphviz_supports_format(self.config.output_format)
 
-        sys.exit(self.run(args))
-
-    def run(self, args: list[str]) -> int:
+    def run(self) -> int:
         """Checking arguments and run project."""
-        if not args:
+        if not self.args:
             print(self.help())
             return 1
         extra_packages_paths = list(
-            {discover_package_path(arg, self.config.source_roots) for arg in args}
+            {discover_package_path(arg, self.config.source_roots) for arg in self.args}
         )
         with augmented_sys_path(extra_packages_paths):
             project = project_from_files(
-                args,
+                self.args,
                 project_name=self.config.project,
                 black_list=self.config.ignore_list,
                 verbose=self.config.verbose,
@@ -319,4 +345,5 @@ def run(self, args: list[str]) -> int:
 
 
 if __name__ == "__main__":
-    Run(sys.argv[1:])
+    arguments = sys.argv[1:]
+    Run(arguments).run()


Test patch:
diff --git a/tests/pyreverse/test_main.py b/tests/pyreverse/test_main.py
index e8e46df2c1..895892d506 100644
--- a/tests/pyreverse/test_main.py
+++ b/tests/pyreverse/test_main.py
@@ -73,9 +73,7 @@ def test_graphviz_supported_image_format(
     mock_writer: mock.MagicMock, capsys: CaptureFixture[str]
 ) -> None:
     """Test that Graphviz is used if the image format is supported."""
-    with pytest.raises(SystemExit) as wrapped_sysexit:
-        # we have to catch the SystemExit so the test execution does not stop
-        main.Run(["-o", "png", TEST_DATA_DIR])
+    exit_code = main.Run(["-o", "png", TEST_DATA_DIR]).run()
     # Check that the right info message is shown to the user
     assert (
         "Format png is not supported natively. Pyreverse will try to generate it using Graphviz..."
@@ -83,7 +81,7 @@ def test_graphviz_supported_image_format(
     )
     # Check that pyreverse actually made the call to create the diagram and we exit cleanly
     mock_writer.DiagramWriter().write.assert_called_once()
-    assert wrapped_sysexit.value.code == 0
+    assert exit_code == 0
 
 
 @mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock())
@@ -95,9 +93,7 @@ def test_graphviz_cant_determine_supported_formats(
 ) -> None:
     """Test that Graphviz is used if the image format is supported."""
     mock_subprocess.run.return_value.stderr = "..."
-    with pytest.raises(SystemExit) as wrapped_sysexit:
-        # we have to catch the SystemExit so the test execution does not stop
-        main.Run(["-o", "png", TEST_DATA_DIR])
+    exit_code = main.Run(["-o", "png", TEST_DATA_DIR]).run()
     # Check that the right info message is shown to the user
     assert (
         "Unable to determine Graphviz supported output formats."
@@ -105,7 +101,7 @@ def test_graphviz_cant_determine_supported_formats(
     )
     # Check that pyreverse actually made the call to create the diagram and we exit cleanly
     mock_writer.DiagramWriter().write.assert_called_once()
-    assert wrapped_sysexit.value.code == 0
+    assert exit_code == 0
 
 
 @mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock())
@@ -134,9 +130,7 @@ def test_graphviz_unsupported_image_format(capsys: CaptureFixture) -> None:
 @pytest.mark.usefixtures("mock_graphviz")
 def test_verbose(_: mock.MagicMock, capsys: CaptureFixture[str]) -> None:
     """Test the --verbose flag."""
-    with pytest.raises(SystemExit):
-        # we have to catch the SystemExit so the test execution does not stop
-        main.Run(["--verbose", TEST_DATA_DIR])
+    main.Run(["--verbose", TEST_DATA_DIR]).run()
     assert "parsing" in capsys.readouterr().out
 
 
@@ -164,7 +158,7 @@ def test_verbose(_: mock.MagicMock, capsys: CaptureFixture[str]) -> None:
 @mock.patch("pylint.pyreverse.main.sys.exit", new=mock.MagicMock())
 def test_command_line_arguments_defaults(arg: str, expected_default: Any) -> None:
     """Test that the default arguments of all options are correct."""
-    run = main.Run([TEST_DATA_DIR])  # type: ignore[var-annotated]
+    run = main.Run([TEST_DATA_DIR])
     assert getattr(run.config, arg) == expected_default
 
 
@@ -177,9 +171,8 @@ def test_command_line_arguments_yes_no(
     Make sure that we support --module-names=yes syntax instead
     of using it as a flag.
     """
-    with pytest.raises(SystemExit) as wrapped_sysexit:
-        main.Run(["--module-names=yes", TEST_DATA_DIR])
-    assert wrapped_sysexit.value.code == 0
+    exit_code = main.Run(["--module-names=yes", TEST_DATA_DIR]).run()
+    assert exit_code == 0
 
 
 @mock.patch("pylint.pyreverse.main.writer")
@@ -191,7 +184,7 @@ def test_class_command(
 
     Make sure that we append multiple --class arguments to one option destination.
     """
-    runner = main.Run(  # type: ignore[var-annotated]
+    runner = main.Run(
         [
             "--class",
             "data.clientmodule_test.Ancestor",
diff --git a/tests/pyreverse/test_pyreverse_functional.py b/tests/pyreverse/test_pyreverse_functional.py
index 715ad3dada..ac8dab1b19 100644
--- a/tests/pyreverse/test_pyreverse_functional.py
+++ b/tests/pyreverse/test_pyreverse_functional.py
@@ -38,14 +38,13 @@ def test_class_diagrams(testfile: FunctionalPyreverseTestfile, tmp_path: Path) -
     else:
         source_roots = ""
     for output_format in testfile.options["output_formats"]:
-        with pytest.raises(SystemExit) as sys_exit:
-            args = ["-o", f"{output_format}", "-d", str(tmp_path)]
-            if source_roots:
-                args += ["--source-roots", source_roots]
-            args.extend(testfile.options["command_line_args"])
-            args += [str(input_file)]
-            Run(args)
-        assert sys_exit.value.code == 0
+        args = ["-o", f"{output_format}", "-d", str(tmp_path)]
+        if source_roots:
+            args += ["--source-roots", source_roots]
+        args.extend(testfile.options["command_line_args"])
+        args += [str(input_file)]
+        exit_code = Run(args).run()
+        assert exit_code == 0
         assert testfile.source.with_suffix(f".{output_format}").read_text(
             encoding="utf8"
         ) == (tmp_path / f"classes.{output_format}").read_text(encoding="utf8")