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: