From 1796316f1c28ca9b4dd6a7a5187ef16ac1cd15e4 Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 11:33:29 -0400 Subject: [PATCH 01/23] remove duplicate classes --- pylint/pyreverse/diagrams.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/pyreverse/diagrams.py b/pylint/pyreverse/diagrams.py index 6db0563003..8ea7856dd9 100644 --- a/pylint/pyreverse/diagrams.py +++ b/pylint/pyreverse/diagrams.py @@ -210,7 +210,7 @@ def object_from_node(self, node: nodes.NodeNG) -> DiagramEntity: def classes(self) -> list[ClassEntity]: """Return all class nodes in the diagram.""" - return [o for o in self.objects if isinstance(o, ClassEntity)] + return list(set([o for o in self.objects if isinstance(o, ClassEntity)])) def classe(self, name: str) -> ClassEntity: """Return a class by its name, raise KeyError if not found.""" From 8690160ebb0cb2e2238791cefdf42f308b344d09 Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 12:37:21 -0400 Subject: [PATCH 02/23] write_classes wdit --- pylint/pyreverse/writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py index e822f67096..937f6bd4a3 100644 --- a/pylint/pyreverse/writer.py +++ b/pylint/pyreverse/writer.py @@ -114,7 +114,7 @@ def write_packages(self, diagram: PackageDiagram) -> None: def write_classes(self, diagram: ClassDiagram) -> None: """Write a class diagram.""" # sorted to get predictable (hence testable) results - for obj in sorted(diagram.objects, key=lambda x: x.title): + for obj in sorted(set(diagram.objects), key=lambda x: x.title): obj.fig_id = obj.node.qname() if self.config.no_standalone and not any( From 8338f9c1a1ae0e524a742a81f2b1961c9084c0a0 Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 12:47:39 -0400 Subject: [PATCH 03/23] deduplicate classes --- pylint/pyreverse/diadefslib.py | 36 +++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index c1f14ce727..cd75ada75c 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -272,6 +272,40 @@ def __init__(self, config: argparse.Namespace, args: Sequence[str]) -> None: self.config = config self.args = args + def deduplicate_classes(self, diagrams: list[ClassDiagram]) -> list[ClassDiagram]: + """Remove duplicate classes from diagrams by merging their relationships + + :param diagrams: List of class diagrams + :type diagrams: list[ClassDiagram] + :return: Deduplicated diagrams + :rtype: list[ClassDiagram] + """ + for diagram in diagrams: + # Track unique classes by qualified name + unique_classes = {} + duplicate_classes = set() + + for obj in diagram.objects: + qname = obj.node.qname() + if qname in unique_classes: + duplicate_classes.add(obj) + # Merge relationships + for rel_type in ("specialization", "association", "aggregation"): + for rel in diagram.get_relationships(rel_type): + if rel.from_object == obj: + rel.from_object = unique_classes[qname] + if rel.to_object == obj: + rel.to_object = unique_classes[qname] + else: + unique_classes[qname] = obj + + # Remove duplicates from objects list + diagram.objects = [ + obj for obj in diagram.objects if obj not in duplicate_classes + ] + + return diagrams + def get_diadefs(self, project: Project, linker: Linker) -> list[ClassDiagram]: """Get the diagram's configuration data. @@ -292,4 +326,4 @@ def get_diadefs(self, project: Project, linker: Linker) -> list[ClassDiagram]: diagrams = DefaultDiadefGenerator(linker, self).visit(project) for diagram in diagrams: diagram.extract_relationships() - return diagrams + return self.deduplicate_classes(diagrams) From 9c86b3bd35f8e5b298d717b971023c297021941f Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 14:34:33 -0400 Subject: [PATCH 04/23] deduplicate relationships as well --- pylint/pyreverse/diadefslib.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index cd75ada75c..b19a70e80c 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -306,6 +306,29 @@ def deduplicate_classes(self, diagrams: list[ClassDiagram]) -> list[ClassDiagram return diagrams + def deduplicate_relationships(self, diagram: ClassDiagram) -> None: + """Remove duplicate relationships between objects + + :param diagram: The class diagram to deduplicate + :type diagram: ClassDiagram + """ + seen = set() + unique_rels = [] + + # Track relationships by (from_qname, to_qname, type, label) + for rel in diagram.relationships: + key = ( + rel.from_object.node.qname(), + rel.to_object.node.qname(), + type(rel).__name__, # Use class name to identify relationship type + getattr(rel, "name", None), # Include label if exists + ) + if key not in seen: + seen.add(key) + unique_rels.append(rel) + + diagram.relationships = unique_rels + def get_diadefs(self, project: Project, linker: Linker) -> list[ClassDiagram]: """Get the diagram's configuration data. @@ -326,4 +349,5 @@ def get_diadefs(self, project: Project, linker: Linker) -> list[ClassDiagram]: diagrams = DefaultDiadefGenerator(linker, self).visit(project) for diagram in diagrams: diagram.extract_relationships() + self.deduplicate_relationships(diagram) return self.deduplicate_classes(diagrams) From 364da44b13799dc3366350dee8ea9997101ab359 Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 14:43:17 -0400 Subject: [PATCH 05/23] attempt1 to fix --- pylint/pyreverse/diadefslib.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index b19a70e80c..99883470e4 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -315,14 +315,22 @@ def deduplicate_relationships(self, diagram: ClassDiagram) -> None: seen = set() unique_rels = [] - # Track relationships by (from_qname, to_qname, type, label) + # Track relationships by (from_name, to_name, type, label) for rel in diagram.relationships: - key = ( - rel.from_object.node.qname(), - rel.to_object.node.qname(), - type(rel).__name__, # Use class name to identify relationship type - getattr(rel, "name", None), # Include label if exists + # Handle both object references and string class names + from_name = ( + rel.from_object.node.qname() + if hasattr(rel.from_object, "node") + else str(rel.from_object) ) + to_name = ( + rel.to_object.node.qname() + if hasattr(rel.to_object, "node") + else str(rel.to_object) + ) + + key = (from_name, to_name, type(rel).__name__, getattr(rel, "name", None)) + if key not in seen: seen.add(key) unique_rels.append(rel) From 997afb61d6232da0e5d6eddc828ca3251da6d19f Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 15:28:53 -0400 Subject: [PATCH 06/23] use DiagramEntity title --- pylint/pyreverse/diadefslib.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 99883470e4..d12a1e8053 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -316,17 +316,17 @@ def deduplicate_relationships(self, diagram: ClassDiagram) -> None: unique_rels = [] # Track relationships by (from_name, to_name, type, label) - for rel in diagram.relationships: + for rel in diagram.relationships.values(): # Handle both object references and string class names from_name = ( - rel.from_object.node.qname() - if hasattr(rel.from_object, "node") - else str(rel.from_object) + rel.from_object.title + if hasattr(rel.from_object, "title") + else str(rel.name) ) to_name = ( - rel.to_object.node.qname() - if hasattr(rel.to_object, "node") - else str(rel.to_object) + rel.to_object.title + if hasattr(rel.to_object, "title") + else str(rel.name) ) key = (from_name, to_name, type(rel).__name__, getattr(rel, "name", None)) From 5212bcdd03e9719c3857b2d6998a3254e54cb5d2 Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 15:33:25 -0400 Subject: [PATCH 07/23] list iteration in relationships --- pylint/pyreverse/diadefslib.py | 39 ++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index d12a1e8053..ece9fe06e4 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -316,24 +316,31 @@ def deduplicate_relationships(self, diagram: ClassDiagram) -> None: unique_rels = [] # Track relationships by (from_name, to_name, type, label) - for rel in diagram.relationships.values(): - # Handle both object references and string class names - from_name = ( - rel.from_object.title - if hasattr(rel.from_object, "title") - else str(rel.name) - ) - to_name = ( - rel.to_object.title - if hasattr(rel.to_object, "title") - else str(rel.name) - ) + for list_rel in diagram.relationships.values(): + for rel in list_rel: + + # Handle both object references and string class names + from_name = ( + rel.from_object.title + if hasattr(rel.from_object, "title") + else str(rel.name) + ) + to_name = ( + rel.to_object.title + if hasattr(rel.to_object, "title") + else str(rel.name) + ) - key = (from_name, to_name, type(rel).__name__, getattr(rel, "name", None)) + key = ( + from_name, + to_name, + type(rel).__name__, + getattr(rel, "name", None), + ) - if key not in seen: - seen.add(key) - unique_rels.append(rel) + if key not in seen: + seen.add(key) + unique_rels.append(rel) diagram.relationships = unique_rels From 27cb36e65e8954d8520ce1344754ce04194b949f Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 15:47:03 -0400 Subject: [PATCH 08/23] bugfix relationships --- pylint/pyreverse/diadefslib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index ece9fe06e4..07a95abd16 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -15,7 +15,7 @@ from astroid import nodes from astroid.modutils import is_stdlib_module -from pylint.pyreverse.diagrams import ClassDiagram, PackageDiagram +from pylint.pyreverse.diagrams import ClassDiagram, PackageDiagram, Relationship from pylint.pyreverse.inspector import Linker, Project from pylint.pyreverse.utils import LocalsVisitor @@ -313,10 +313,10 @@ def deduplicate_relationships(self, diagram: ClassDiagram) -> None: :type diagram: ClassDiagram """ seen = set() - unique_rels = [] + unique_rels = dict[str, list[Relationship]] = {} # Track relationships by (from_name, to_name, type, label) - for list_rel in diagram.relationships.values(): + for rel_name, list_rel in diagram.relationships.items(): for rel in list_rel: # Handle both object references and string class names @@ -340,7 +340,7 @@ def deduplicate_relationships(self, diagram: ClassDiagram) -> None: if key not in seen: seen.add(key) - unique_rels.append(rel) + unique_rels[rel_name].append(rel) diagram.relationships = unique_rels From 1e6d02073ab511e51b4b1f2e561484796faee2ec Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 15:48:53 -0400 Subject: [PATCH 09/23] error fix --- pylint/pyreverse/diadefslib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 07a95abd16..c09c9f3a47 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -313,7 +313,7 @@ def deduplicate_relationships(self, diagram: ClassDiagram) -> None: :type diagram: ClassDiagram """ seen = set() - unique_rels = dict[str, list[Relationship]] = {} + unique_rels: dict[str, list[Relationship]] = {} # Track relationships by (from_name, to_name, type, label) for rel_name, list_rel in diagram.relationships.items(): From 9fd85dc14a67c1945f7b50c44ba4c4cb797785b6 Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 15:52:05 -0400 Subject: [PATCH 10/23] default dict --- pylint/pyreverse/diadefslib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index c09c9f3a47..a22ce5c9b2 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -9,6 +9,7 @@ import argparse import warnings from collections.abc import Generator, Sequence +from collections import defaultdict from typing import Any import astroid @@ -313,7 +314,7 @@ def deduplicate_relationships(self, diagram: ClassDiagram) -> None: :type diagram: ClassDiagram """ seen = set() - unique_rels: dict[str, list[Relationship]] = {} + unique_rels: dict[str, list[Relationship]] = defaultdict(list) # Track relationships by (from_name, to_name, type, label) for rel_name, list_rel in diagram.relationships.items(): From e373aa6ffbcd5286650192fe1ff107fc7998f0eb Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 15:55:28 -0400 Subject: [PATCH 11/23] Revert "remove duplicate classes" This reverts commit 1796316f1c28ca9b4dd6a7a5187ef16ac1cd15e4. --- pylint/pyreverse/diagrams.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/pyreverse/diagrams.py b/pylint/pyreverse/diagrams.py index 8ea7856dd9..6db0563003 100644 --- a/pylint/pyreverse/diagrams.py +++ b/pylint/pyreverse/diagrams.py @@ -210,7 +210,7 @@ def object_from_node(self, node: nodes.NodeNG) -> DiagramEntity: def classes(self) -> list[ClassEntity]: """Return all class nodes in the diagram.""" - return list(set([o for o in self.objects if isinstance(o, ClassEntity)])) + return [o for o in self.objects if isinstance(o, ClassEntity)] def classe(self, name: str) -> ClassEntity: """Return a class by its name, raise KeyError if not found.""" From c7bb438197147b3ad42fc3c727909836833ba91f Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Wed, 9 Apr 2025 15:56:34 -0400 Subject: [PATCH 12/23] Revert "write_classes wdit" This reverts commit 8690160ebb0cb2e2238791cefdf42f308b344d09. --- pylint/pyreverse/writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py index 937f6bd4a3..e822f67096 100644 --- a/pylint/pyreverse/writer.py +++ b/pylint/pyreverse/writer.py @@ -114,7 +114,7 @@ def write_packages(self, diagram: PackageDiagram) -> None: def write_classes(self, diagram: ClassDiagram) -> None: """Write a class diagram.""" # sorted to get predictable (hence testable) results - for obj in sorted(set(diagram.objects), key=lambda x: x.title): + for obj in sorted(diagram.objects, key=lambda x: x.title): obj.fig_id = obj.node.qname() if self.config.no_standalone and not any( From cdcdbbf3d1845de2a30d6054f328db04a20710bc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 20:26:33 +0000 Subject: [PATCH 13/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/pyreverse/diadefslib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index a22ce5c9b2..8b04356a13 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -8,8 +8,8 @@ import argparse import warnings -from collections.abc import Generator, Sequence from collections import defaultdict +from collections.abc import Generator, Sequence from typing import Any import astroid @@ -274,7 +274,7 @@ def __init__(self, config: argparse.Namespace, args: Sequence[str]) -> None: self.args = args def deduplicate_classes(self, diagrams: list[ClassDiagram]) -> list[ClassDiagram]: - """Remove duplicate classes from diagrams by merging their relationships + """Remove duplicate classes from diagrams by merging their relationships. :param diagrams: List of class diagrams :type diagrams: list[ClassDiagram] @@ -308,7 +308,7 @@ def deduplicate_classes(self, diagrams: list[ClassDiagram]) -> list[ClassDiagram return diagrams def deduplicate_relationships(self, diagram: ClassDiagram) -> None: - """Remove duplicate relationships between objects + """Remove duplicate relationships between objects. :param diagram: The class diagram to deduplicate :type diagram: ClassDiagram From 0aedc80af825cbe01c1d85e2eca0b57188d8b210 Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Thu, 10 Apr 2025 10:40:07 -0400 Subject: [PATCH 14/23] Made mypy suggestions and added test --- pylint/pyreverse/diadefslib.py | 8 ++++---- .../aggregation/class_attribute_duplicate.mmd | 9 +++++++++ .../aggregation/class_attribute_duplicate.py | 13 +++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.mmd create mode 100644 tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.py diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index a22ce5c9b2..7f04799057 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -10,7 +10,7 @@ import warnings from collections.abc import Generator, Sequence from collections import defaultdict -from typing import Any +from typing import Any, Tuple import astroid from astroid import nodes @@ -283,8 +283,8 @@ def deduplicate_classes(self, diagrams: list[ClassDiagram]) -> list[ClassDiagram """ for diagram in diagrams: # Track unique classes by qualified name - unique_classes = {} - duplicate_classes = set() + unique_classes: dict[str, Any] = {} + duplicate_classes: Any = set() for obj in diagram.objects: qname = obj.node.qname() @@ -313,7 +313,7 @@ def deduplicate_relationships(self, diagram: ClassDiagram) -> None: :param diagram: The class diagram to deduplicate :type diagram: ClassDiagram """ - seen = set() + seen: Tuple = set() unique_rels: dict[str, list[Relationship]] = defaultdict(list) # Track relationships by (from_name, to_name, type, label) diff --git a/tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.mmd b/tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.mmd new file mode 100644 index 0000000000..8523ec1341 --- /dev/null +++ b/tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.mmd @@ -0,0 +1,9 @@ +classDiagram +class A { +var : int +} +class B { +a_obj +func() +} +A --* B : a_obj diff --git a/tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.py b/tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.py new file mode 100644 index 0000000000..eccd0aee86 --- /dev/null +++ b/tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.py @@ -0,0 +1,13 @@ +# Test for issue #9267 +class A: + def __init__(self) -> None: + self.var = 2 + + +class B: + def __init__(self) -> None: + self.a_obj = A() + + def func(self): + self.a_obj = A() + self.a_obj = A() From 4d83b3a19feeef89c482a1fdf13d5dcb50afd521 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:45:28 +0000 Subject: [PATCH 15/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/pyreverse/diadefslib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 94f0e74f58..9b8406c104 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -10,7 +10,6 @@ import warnings from collections import defaultdict from collections.abc import Generator, Sequence -from collections import defaultdict from typing import Any, Tuple import astroid From eeccaf1444a2dfff83d709370431f6ccb4a7db92 Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Thu, 10 Apr 2025 10:55:53 -0400 Subject: [PATCH 16/23] removed deprecated Tuple --- pylint/pyreverse/diadefslib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 94f0e74f58..0e448bcd66 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -11,7 +11,7 @@ from collections import defaultdict from collections.abc import Generator, Sequence from collections import defaultdict -from typing import Any, Tuple +from typing import Any, tuple import astroid from astroid import nodes @@ -314,7 +314,7 @@ def deduplicate_relationships(self, diagram: ClassDiagram) -> None: :param diagram: The class diagram to deduplicate :type diagram: ClassDiagram """ - seen: Tuple = set() + seen: tuple[str, str, str, str | None] = set() unique_rels: dict[str, list[Relationship]] = defaultdict(list) # Track relationships by (from_name, to_name, type, label) From 442d2f535b5bffe765814414ee392320450986f8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:58:15 +0000 Subject: [PATCH 17/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/pyreverse/diadefslib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 0e448bcd66..b22265b84c 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -10,7 +10,6 @@ import warnings from collections import defaultdict from collections.abc import Generator, Sequence -from collections import defaultdict from typing import Any, tuple import astroid From c0f0cc0d460616d0693538c75ae808544a73f05d Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Thu, 10 Apr 2025 11:00:41 -0400 Subject: [PATCH 18/23] tuple from builtins --- pylint/pyreverse/diadefslib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 0e448bcd66..365e9a9ed9 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -11,8 +11,7 @@ from collections import defaultdict from collections.abc import Generator, Sequence from collections import defaultdict -from typing import Any, tuple - +from typing import Any import astroid from astroid import nodes from astroid.modutils import is_stdlib_module From b85d720306ef91b6692763db4cd71f6c1b987bf3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:02:06 +0000 Subject: [PATCH 19/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pylint/pyreverse/diadefslib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 365e9a9ed9..c99da4b1bb 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -10,8 +10,8 @@ import warnings from collections import defaultdict from collections.abc import Generator, Sequence -from collections import defaultdict from typing import Any + import astroid from astroid import nodes from astroid.modutils import is_stdlib_module From 0c4f401d30f586b4fe7e1921051449213a50ce4e Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Thu, 10 Apr 2025 11:13:34 -0400 Subject: [PATCH 20/23] Fixed formatting of .mmd for test --- .../aggregation/class_attribute_duplicate.mmd | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.mmd b/tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.mmd index 8523ec1341..5e451ec3af 100644 --- a/tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.mmd +++ b/tests/pyreverse/functional/class_diagrams/aggregation/class_attribute_duplicate.mmd @@ -1,9 +1,9 @@ classDiagram -class A { -var : int -} -class B { -a_obj -func() -} -A --* B : a_obj + class A { + var : int + } + class B { + a_obj + func() + } + A --* B : a_obj From 0d89cc973d975e342d6149f03f72ac7a878c88d8 Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Thu, 10 Apr 2025 11:55:45 -0400 Subject: [PATCH 21/23] Passed all linter and mypy checks --- pylint/pyreverse/diadefslib.py | 128 +++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 47 deletions(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index c99da4b1bb..7cd6db5fe2 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -273,77 +273,111 @@ def __init__(self, config: argparse.Namespace, args: Sequence[str]) -> None: self.config = config self.args = args - def deduplicate_classes(self, diagrams: list[ClassDiagram]) -> list[ClassDiagram]: - """Remove duplicate classes from diagrams by merging their relationships. + def _get_object_name(self, obj: Any) -> str: + """Get object name safely handling both title attributes and strings. + + :param obj: Object to get name from + :return: Object name/title + :rtype: str + """ + return obj.title if hasattr(obj, "title") else str(obj) + + def _make_relationship_key( + self, rel: Relationship + ) -> tuple[str, str, str, str | None]: + """Create unique key for relationship. - :param diagrams: List of class diagrams - :type diagrams: list[ClassDiagram] - :return: Deduplicated diagrams - :rtype: list[ClassDiagram] + :param rel: Relationship object + :return: Tuple key for deduplication + :rtype: tuple """ + from_name = self._get_object_name(rel.from_object) + to_name = self._get_object_name(rel.to_object) + return (from_name, to_name, type(rel).__name__, getattr(rel, "name", None)) + + def _process_relationship( + self, relationship: Relationship, unique_classes: dict[str, Any], obj: Any + ) -> None: + """Process a single relationship for deduplication. + + :param relationship: Relationship to process + :param unique_classes: Dict of unique classes + :param obj: Current object being processed + """ + if relationship.from_object == obj: + relationship.from_object = unique_classes[obj.node.qname()] + if relationship.to_object == obj: + relationship.to_object = unique_classes[obj.node.qname()] + + def _process_class_relationships( + self, diagram: ClassDiagram, obj: Any, unique_classes: dict[str, Any] + ) -> None: + """Merge relationships for a class. + + :param diagram: Current diagram + :param obj: Object whose relationships to process + :param unique_classes: Dict of unique classes + """ + for rel_type in ("specialization", "association", "aggregation"): + for rel in diagram.get_relationships(rel_type): + self._process_relationship(rel, unique_classes, obj) + + def deduplicate_classes(self, diagrams: list[ClassDiagram]) -> list[ClassDiagram]: + """Remove duplicate classes from diagrams.""" for diagram in diagrams: # Track unique classes by qualified name unique_classes: dict[str, Any] = {} duplicate_classes: Any = set() + # First pass - identify duplicates for obj in diagram.objects: qname = obj.node.qname() if qname in unique_classes: duplicate_classes.add(obj) - # Merge relationships - for rel_type in ("specialization", "association", "aggregation"): - for rel in diagram.get_relationships(rel_type): - if rel.from_object == obj: - rel.from_object = unique_classes[qname] - if rel.to_object == obj: - rel.to_object = unique_classes[qname] + self._process_class_relationships(diagram, obj, unique_classes) else: unique_classes[qname] = obj - # Remove duplicates from objects list + # Second pass - filter out duplicates diagram.objects = [ obj for obj in diagram.objects if obj not in duplicate_classes ] return diagrams - def deduplicate_relationships(self, diagram: ClassDiagram) -> None: - """Remove duplicate relationships between objects. + def _process_relationship_type( + self, + rel_list: list[Relationship], + seen: set[tuple[str, str, str, Any | None]], + unique_rels: dict[str, list[Relationship]], + rel_name: str, + ) -> None: + """Process a list of relationships of a single type. - :param diagram: The class diagram to deduplicate - :type diagram: ClassDiagram + :param rel_list: List of relationships to process + :param seen: Set of seen relationships + :param unique_rels: Dict to store unique relationships + :param rel_name: Name of relationship type """ - seen: tuple[str, str, str, str | None] = set() - unique_rels: dict[str, list[Relationship]] = defaultdict(list) - - # Track relationships by (from_name, to_name, type, label) - for rel_name, list_rel in diagram.relationships.items(): - for rel in list_rel: - - # Handle both object references and string class names - from_name = ( - rel.from_object.title - if hasattr(rel.from_object, "title") - else str(rel.name) - ) - to_name = ( - rel.to_object.title - if hasattr(rel.to_object, "title") - else str(rel.name) - ) - - key = ( - from_name, - to_name, - type(rel).__name__, - getattr(rel, "name", None), - ) + for rel in rel_list: + key = ( + self._get_object_name(rel.from_object), + self._get_object_name(rel.to_object), + type(rel).__name__, + getattr(rel, "name", None), + ) + if key not in seen: + seen.add(key) + unique_rels[rel_name].append(rel) - if key not in seen: - seen.add(key) - unique_rels[rel_name].append(rel) + def deduplicate_relationships(self, diagram: ClassDiagram) -> None: + """Remove duplicate relationships between objects.""" + seen: set[tuple[str, str, str, Any | None]] = set() + unique_rels: dict[str, list[Relationship]] = defaultdict(list) + for rel_name, rel_list in diagram.relationships.items(): + self._process_relationship_type(rel_list, seen, unique_rels, rel_name) - diagram.relationships = unique_rels + diagram.relationships = dict(unique_rels) def get_diadefs(self, project: Project, linker: Linker) -> list[ClassDiagram]: """Get the diagram's configuration data. From 5e8223cbb0dd9d5829c9542b12304fa1dcf0ddc0 Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Thu, 10 Apr 2025 15:49:14 -0400 Subject: [PATCH 22/23] Removed unnecessary block --- pylint/pyreverse/diadefslib.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py index 7cd6db5fe2..f91a89eca8 100644 --- a/pylint/pyreverse/diadefslib.py +++ b/pylint/pyreverse/diadefslib.py @@ -282,19 +282,6 @@ def _get_object_name(self, obj: Any) -> str: """ return obj.title if hasattr(obj, "title") else str(obj) - def _make_relationship_key( - self, rel: Relationship - ) -> tuple[str, str, str, str | None]: - """Create unique key for relationship. - - :param rel: Relationship object - :return: Tuple key for deduplication - :rtype: tuple - """ - from_name = self._get_object_name(rel.from_object) - to_name = self._get_object_name(rel.to_object) - return (from_name, to_name, type(rel).__name__, getattr(rel, "name", None)) - def _process_relationship( self, relationship: Relationship, unique_classes: dict[str, Any], obj: Any ) -> None: From 608bbc0985a6e70bcc39f858af94a234beb4458f Mon Sep 17 00:00:00 2001 From: Pranav Kumar Seerala Date: Thu, 10 Apr 2025 18:00:25 -0400 Subject: [PATCH 23/23] Remove pyvenv.cfg from version control --- .gitignore | 4 ---- pyvenv.cfg | 3 --- 2 files changed, 7 deletions(-) delete mode 100644 pyvenv.cfg diff --git a/.gitignore b/.gitignore index 1acf50f1bb..d807609565 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,3 @@ build-stamp .pytest_cache/ .mypy_cache/ .benchmarks/ - -**/Include/** -**/Lib/** -**/Scripts/** diff --git a/pyvenv.cfg b/pyvenv.cfg deleted file mode 100644 index de1194b0f7..0000000000 --- a/pyvenv.cfg +++ /dev/null @@ -1,3 +0,0 @@ -home = C:\Users\seerala\AppData\Local\Programs\Python\Python310 -include-system-site-packages = true -version = 3.10.11