diff --git a/doc/whatsnew/fragments/8476.feature b/doc/whatsnew/fragments/8476.feature
new file mode 100644
index 0000000000..56c0be2019
--- /dev/null
+++ b/doc/whatsnew/fragments/8476.feature
@@ -0,0 +1,3 @@
+Add Pyreverse option to exclude standalone nodes from diagrams with `--no-standalone`.
+
+Closes #8476
diff --git a/pylint/pyreverse/main.py b/pylint/pyreverse/main.py
index 975e432e43..58128bb57a 100644
--- a/pylint/pyreverse/main.py
+++ b/pylint/pyreverse/main.py
@@ -157,6 +157,14 @@
"help": "don't show attributes and methods in the class boxes; this disables -f values",
},
),
+ (
+ "no-standalone",
+ {
+ "action": "store_true",
+ "default": False,
+ "help": "only show nodes with connections",
+ },
+ ),
(
"output",
{
diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py
index cb711dd104..b5cab0a66b 100644
--- a/pylint/pyreverse/writer.py
+++ b/pylint/pyreverse/writer.py
@@ -57,6 +57,12 @@ def write_packages(self, diagram: PackageDiagram) -> None:
# sorted to get predictable (hence testable) results
for module in sorted(diagram.modules(), key=lambda x: x.title):
module.fig_id = module.node.qname()
+ if self.config.no_standalone and not any(
+ module in (rel.from_object, rel.to_object)
+ for rel in diagram.get_relationships("depends")
+ ):
+ continue
+
self.printer.emit_node(
module.fig_id,
type_=NodeType.PACKAGE,
@@ -75,6 +81,13 @@ def write_classes(self, diagram: ClassDiagram) -> None:
# sorted to get predictable (hence testable) results
for obj in sorted(diagram.objects, key=lambda x: x.title): # type: ignore[no-any-return]
obj.fig_id = obj.node.qname()
+ if self.config.no_standalone and not any(
+ obj in (rel.from_object, rel.to_object)
+ for rel_type in ("specialization", "association", "aggregation")
+ for rel in diagram.get_relationships(rel_type)
+ ):
+ continue
+
self.printer.emit_node(
obj.fig_id,
type_=NodeType.CLASS,
diff --git a/pylint/testutils/pyreverse.py b/pylint/testutils/pyreverse.py
index ba79ebf8bc..24fddad770 100644
--- a/pylint/testutils/pyreverse.py
+++ b/pylint/testutils/pyreverse.py
@@ -37,6 +37,7 @@ def __init__(
all_ancestors: bool | None = None,
show_associated: int | None = None,
all_associated: bool | None = None,
+ no_standalone: bool = False,
show_builtin: bool = False,
show_stdlib: bool = False,
module_names: bool | None = None,
@@ -59,6 +60,7 @@ def __init__(
self.all_ancestors = all_ancestors
self.show_associated = show_associated
self.all_associated = all_associated
+ self.no_standalone = no_standalone
self.show_builtin = show_builtin
self.show_stdlib = show_stdlib
self.module_names = module_names
diff --git a/tests/pyreverse/conftest.py b/tests/pyreverse/conftest.py
index b281c5bee5..9e1741f0a8 100644
--- a/tests/pyreverse/conftest.py
+++ b/tests/pyreverse/conftest.py
@@ -28,6 +28,14 @@ def colorized_dot_config() -> PyreverseConfig:
)
+@pytest.fixture()
+def no_standalone_dot_config() -> PyreverseConfig:
+ return PyreverseConfig(
+ output_format="dot",
+ no_standalone=True,
+ )
+
+
@pytest.fixture()
def puml_config() -> PyreverseConfig:
return PyreverseConfig(
diff --git a/tests/pyreverse/data/classes_no_standalone.dot b/tests/pyreverse/data/classes_no_standalone.dot
new file mode 100644
index 0000000000..7cffc0a38f
--- /dev/null
+++ b/tests/pyreverse/data/classes_no_standalone.dot
@@ -0,0 +1,12 @@
+digraph "classes_no_standalone" {
+rankdir=BT
+charset="utf-8"
+"data.clientmodule_test.Ancestor" [color="black", fontcolor="black", label=<{Ancestor|attr : str
cls_member
|get_value()
set_value(value)
}>, shape="record", style="solid"];
+"data.suppliermodule_test.DoNothing" [color="black", fontcolor="black", label=<{DoNothing|
|}>, shape="record", style="solid"];
+"data.suppliermodule_test.DoNothing2" [color="black", fontcolor="black", label=<{DoNothing2|
|}>, shape="record", style="solid"];
+"data.clientmodule_test.Specialization" [color="black", fontcolor="black", label=<{Specialization|TYPE : str
relation
relation2
top : str
|from_value(value: int)
increment_value(): None
transform_value(value: int): int
}>, shape="record", style="solid"];
+"data.clientmodule_test.Specialization" -> "data.clientmodule_test.Ancestor" [arrowhead="empty", arrowtail="none"];
+"data.suppliermodule_test.DoNothing" -> "data.clientmodule_test.Ancestor" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="cls_member", style="solid"];
+"data.suppliermodule_test.DoNothing" -> "data.clientmodule_test.Specialization" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation", style="solid"];
+"data.suppliermodule_test.DoNothing2" -> "data.clientmodule_test.Specialization" [arrowhead="odiamond", arrowtail="none", fontcolor="green", label="relation2", style="solid"];
+}
diff --git a/tests/pyreverse/data/packages_no_standalone.dot b/tests/pyreverse/data/packages_no_standalone.dot
new file mode 100644
index 0000000000..008c9c5d0a
--- /dev/null
+++ b/tests/pyreverse/data/packages_no_standalone.dot
@@ -0,0 +1,7 @@
+digraph "packages_no_standalone" {
+rankdir=BT
+charset="utf-8"
+"data.clientmodule_test" [color="black", label=, shape="box", style="solid"];
+"data.suppliermodule_test" [color="black", label=, shape="box", style="solid"];
+"data.clientmodule_test" -> "data.suppliermodule_test" [arrowhead="open", arrowtail="none"];
+}
diff --git a/tests/pyreverse/functional/class_diagrams/inheritance/no_standalone.mmd b/tests/pyreverse/functional/class_diagrams/inheritance/no_standalone.mmd
new file mode 100644
index 0000000000..646d8220d5
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/inheritance/no_standalone.mmd
@@ -0,0 +1,6 @@
+classDiagram
+ class A {
+ }
+ class B {
+ }
+ B --|> A
diff --git a/tests/pyreverse/functional/class_diagrams/inheritance/no_standalone.py b/tests/pyreverse/functional/class_diagrams/inheritance/no_standalone.py
new file mode 100644
index 0000000000..3d881d4c0d
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/inheritance/no_standalone.py
@@ -0,0 +1,5 @@
+class A: pass
+
+class B(A): pass
+
+class C: pass
diff --git a/tests/pyreverse/functional/class_diagrams/inheritance/no_standalone.rc b/tests/pyreverse/functional/class_diagrams/inheritance/no_standalone.rc
new file mode 100644
index 0000000000..c17e41cfb6
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/inheritance/no_standalone.rc
@@ -0,0 +1,2 @@
+[testoptions]
+command_line_args=--no-standalone
diff --git a/tests/pyreverse/test_writer.py b/tests/pyreverse/test_writer.py
index 2897ca054c..37a4b4f195 100644
--- a/tests/pyreverse/test_writer.py
+++ b/tests/pyreverse/test_writer.py
@@ -35,6 +35,7 @@
"show_stdlib": False,
"only_classnames": False,
"output_directory": "",
+ "no_standalone": False,
}
TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
@@ -45,6 +46,7 @@
COLORIZED_PUML_FILES = ["packages_colorized.puml", "classes_colorized.puml"]
MMD_FILES = ["packages_No_Name.mmd", "classes_No_Name.mmd"]
HTML_FILES = ["packages_No_Name.html", "classes_No_Name.html"]
+NO_STANDALONE_FILES = ["classes_no_standalone.dot", "packages_no_standalone.dot"]
class Config:
@@ -87,6 +89,15 @@ def setup_colorized_dot(
yield from _setup(project, colorized_dot_config, writer)
+@pytest.fixture()
+def setup_no_standalone_dot(
+ no_standalone_dot_config: PyreverseConfig, get_project: GetProjectCallable
+) -> Iterator[None]:
+ writer = DiagramWriter(no_standalone_dot_config)
+ project = get_project(TEST_DATA_DIR, name="no_standalone")
+ yield from _setup(project, no_standalone_dot_config, writer)
+
+
@pytest.fixture()
def setup_puml(
puml_config: PyreverseConfig, get_project: GetProjectCallable
@@ -138,6 +149,7 @@ def _setup(
for fname in (
DOT_FILES
+ COLORIZED_DOT_FILES
+ + NO_STANDALONE_FILES
+ PUML_FILES
+ COLORIZED_PUML_FILES
+ MMD_FILES
@@ -161,6 +173,12 @@ def test_colorized_dot_files(generated_file: str) -> None:
_assert_files_are_equal(generated_file)
+@pytest.mark.usefixtures("setup_no_standalone_dot")
+@pytest.mark.parametrize("generated_file", NO_STANDALONE_FILES)
+def test_no_standalone_dot_files(generated_file: str) -> None:
+ _assert_files_are_equal(generated_file)
+
+
@pytest.mark.usefixtures("setup_puml")
@pytest.mark.parametrize("generated_file", PUML_FILES)
def test_puml_files(generated_file: str) -> None: