Skip to content

[pyreverse] Fix duplicate arrows when class attribute is assigned more than once #10333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1796316
remove duplicate classes
Apr 9, 2025
8690160
write_classes wdit
Apr 9, 2025
8338f9c
deduplicate classes
Apr 9, 2025
9c86b3b
deduplicate relationships as well
Apr 9, 2025
364da44
attempt1 to fix
Apr 9, 2025
997afb6
use DiagramEntity title
Apr 9, 2025
5212bcd
list iteration in relationships
Apr 9, 2025
27cb36e
bugfix relationships
Apr 9, 2025
1e6d020
error fix
Apr 9, 2025
9fd85dc
default dict
Apr 9, 2025
e373aa6
Revert "remove duplicate classes"
Apr 9, 2025
c7bb438
Revert "write_classes wdit"
Apr 9, 2025
cdcdbbf
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 9, 2025
0aedc80
Made mypy suggestions and added test
Apr 10, 2025
b64b36c
Merge branch 'main' of https://github.com/pranav-data/pylint
Apr 10, 2025
4d83b3a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 10, 2025
eeccaf1
removed deprecated Tuple
Apr 10, 2025
3748219
Merge branch 'main' of https://github.com/pranav-data/pylint
Apr 10, 2025
442d2f5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 10, 2025
c0f0cc0
tuple from builtins
Apr 10, 2025
54a6e25
Merge branch 'main' of https://github.com/pranav-data/pylint
Apr 10, 2025
b85d720
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 10, 2025
0c4f401
Fixed formatting of .mmd for test
Apr 10, 2025
be38db4
Merge branch 'main' of https://github.com/pranav-data/pylint
Apr 10, 2025
0d89cc9
Passed all linter and mypy checks
Apr 10, 2025
5e8223c
Removed unnecessary block
Apr 10, 2025
608bbc0
Remove pyvenv.cfg from version control
Apr 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 97 additions & 2 deletions pylint/pyreverse/diadefslib.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@

import argparse
import warnings
from collections import defaultdict
from collections.abc import Generator, Sequence
from typing import Any

import astroid
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

Expand Down Expand Up @@ -272,6 +273,99 @@
self.config = config
self.args = args

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 _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()]

Check warning on line 297 in pylint/pyreverse/diadefslib.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/diadefslib.py#L294-L297

Added lines #L294 - L297 were not covered by tests

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)

Check warning on line 310 in pylint/pyreverse/diadefslib.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/diadefslib.py#L308-L310

Added lines #L308 - L310 were not covered by tests

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)
self._process_class_relationships(diagram, obj, unique_classes)

Check warning on line 324 in pylint/pyreverse/diadefslib.py

View check run for this annotation

Codecov / codecov/patch

pylint/pyreverse/diadefslib.py#L323-L324

Added lines #L323 - L324 were not covered by tests
else:
unique_classes[qname] = obj

# Second pass - filter out duplicates
diagram.objects = [
obj for obj in diagram.objects if obj not in duplicate_classes
]

return diagrams

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 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
"""
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)

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 = dict(unique_rels)

def get_diadefs(self, project: Project, linker: Linker) -> list[ClassDiagram]:
"""Get the diagram's configuration data.

Expand All @@ -292,4 +386,5 @@
diagrams = DefaultDiadefGenerator(linker, self).visit(project)
for diagram in diagrams:
diagram.extract_relationships()
return diagrams
self.deduplicate_relationships(diagram)
return self.deduplicate_classes(diagrams)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
classDiagram
class A {
var : int
}
class B {
a_obj
func()
}
A --* B : a_obj
Original file line number Diff line number Diff line change
@@ -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()
Loading