Repository: pylint-dev/pylint

Issue:
Pyreverse: Aggregations aren't filtered according to filter mode (PUB_ONLY, etc.)
### Bug description


When using Pyreverse with filter modes like PUB_ONLY, relationships between classes (arrows in diagrams) are still shown even when the attributes themselves are filtered out due to visibility settings.
For example, with this code:
```python
class P:
    pass

class A:
    x: P = P()

class B:
    __x: P = P()
```
And running with default settings (PUB_ONLY), the diagram shows:
```
classDiagram
  class A {
    x
  }
  class B {
  }
  class P {
  }
  P --* A : x
  P --* B : __x
```

Now __x is correctly filtered out from class B's attributes due to being private, but the relationship arrow P --* B : __x is still displayed.

When using --filter-mode=ALL, both attributes and relationships are shown correctly:
```
classDiagram
  class A {
    x
  }
  class B {
    __x
  }
  class P {
  }
  P --* A : x
  P --* B : __x
```

The relationships (arrows) should be filtered according to the same visibility rules as attributes for consistency.

### Command used

```shell
pyreverse -o mmd example.py
```

### Pylint output

```python
Generates diagrams with relationships that don't respect the filter mode.
```

### Expected behavior

If an attribute is filtered out due to visibility (private, protected, etc.), its relationship arrow should also be filtered out.

### Pylint version

```shell
pylint 4.0.0-dev0
astroid 4.0.0a0
Python 3.13.2 (main, Feb 12 2025, 14:49:53) [MSC v.1942 64 bit (AMD64)]
```



Discussion:
Hi @Julfried 

I’m working on a fix for the issue where aggregations and associations aren’t being filtered according to the selected filter mode (PUB_ONLY, ALL, etc.) in Pyreverse.

To fix this, I added a check in the extract_relationships() method to filter out aggregations and associations based on visibility before adding them to the diagram. I used `self.show_attr(name)` to handle this filtering.

Before I submit a PR, I wanted to check if this is the right approach. Does this look good, or is there something else I should consider for filtering these relationships based on the filter mode?



Patch:
diff --git a/doc/whatsnew/fragments/10373.bugfix b/doc/whatsnew/fragments/10373.bugfix
new file mode 100644
index 0000000000..d7ba3a1893
--- /dev/null
+++ b/doc/whatsnew/fragments/10373.bugfix
@@ -0,0 +1,3 @@
+Fix a bug in Pyreverse where aggregations and associations were included in diagrams regardless of the selected --filter-mode (such as PUB_ONLY, ALL, etc.).
+
+Closes #10373
diff --git a/pylint/pyreverse/diagrams.py b/pylint/pyreverse/diagrams.py
index 6db0563003..a4fb8ce130 100644
--- a/pylint/pyreverse/diagrams.py
+++ b/pylint/pyreverse/diagrams.py
@@ -237,6 +237,9 @@ def extract_relationships(self) -> None:
             # associations & aggregations links
             for name, values in list(node.aggregations_type.items()):
                 for value in values:
+                    if not self.show_attr(name):
+                        continue
+
                     self.assign_association_relationship(
                         value, obj, name, "aggregation"
                     )
@@ -249,6 +252,9 @@ def extract_relationships(self) -> None:
 
             for name, values in associations.items():
                 for value in values:
+                    if not self.show_attr(name):
+                        continue
+
                     self.assign_association_relationship(
                         value, obj, name, "association"
                     )


Test patch:
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.mmd b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.mmd
new file mode 100644
index 0000000000..3b445288a1
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.mmd
@@ -0,0 +1,25 @@
+classDiagram
+  class P {
+    name : str
+    __init__(name: str)
+  }
+  class PrivateAttr {
+    __x
+    __init__()
+  }
+  class ProtectedAttr {
+    _x
+    __init__()
+  }
+  class PublicAttr {
+    x
+    __init__()
+  }
+  class SpecialAttr {
+    __x__
+    __init__()
+  }
+  P --* PrivateAttr : __x
+  P --* ProtectedAttr : _x
+  P --* PublicAttr : x
+  P --* SpecialAttr : __x__
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.py b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.py
new file mode 100644
index 0000000000..609f4376be
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.py
@@ -0,0 +1,23 @@
+class P:
+    def __init__(self, name: str):
+        self.name = name
+
+
+class PrivateAttr:
+    def __init__(self):
+        self.__x = P("private")
+
+
+class ProtectedAttr:
+    def __init__(self):
+        self._x = P("protected")
+
+
+class PublicAttr:
+    def __init__(self):
+        self.x = P("public")
+
+
+class SpecialAttr:
+    def __init__(self):
+        self.__x__ = P("special")
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.rc b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.rc
new file mode 100644
index 0000000000..08fbb3d02d
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/all.rc
@@ -0,0 +1,2 @@
+[testoptions]
+command_line_args=--filter-mode=ALL
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.mmd b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.mmd
new file mode 100644
index 0000000000..6cfcae244f
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.mmd
@@ -0,0 +1,21 @@
+classDiagram
+  class P {
+    name : str
+    __init__(name: str)
+  }
+  class PrivateAttr {
+    __init__()
+  }
+  class ProtectedAttr {
+    __init__()
+  }
+  class PublicAttr {
+    x
+    __init__()
+  }
+  class SpecialAttr {
+    __x__
+    __init__()
+  }
+  P --* PublicAttr : x
+  P --* SpecialAttr : __x__
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.py b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.py
new file mode 100644
index 0000000000..609f4376be
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.py
@@ -0,0 +1,23 @@
+class P:
+    def __init__(self, name: str):
+        self.name = name
+
+
+class PrivateAttr:
+    def __init__(self):
+        self.__x = P("private")
+
+
+class ProtectedAttr:
+    def __init__(self):
+        self._x = P("protected")
+
+
+class PublicAttr:
+    def __init__(self):
+        self.x = P("public")
+
+
+class SpecialAttr:
+    def __init__(self):
+        self.__x__ = P("special")
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.rc b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.rc
new file mode 100644
index 0000000000..8e0e615079
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/other.rc
@@ -0,0 +1,2 @@
+[testoptions]
+command_line_args=--filter-mode=OTHER
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/pub_only.mmd b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/pub_only.mmd
new file mode 100644
index 0000000000..f29000b5c8
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/pub_only.mmd
@@ -0,0 +1,14 @@
+classDiagram
+  class P {
+    name : str
+  }
+  class PrivateAttr {
+  }
+  class ProtectedAttr {
+  }
+  class PublicAttr {
+    x
+  }
+  class SpecialAttr {
+  }
+  P --* PublicAttr : x
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/pub_only.py b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/pub_only.py
new file mode 100644
index 0000000000..609f4376be
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/pub_only.py
@@ -0,0 +1,23 @@
+class P:
+    def __init__(self, name: str):
+        self.name = name
+
+
+class PrivateAttr:
+    def __init__(self):
+        self.__x = P("private")
+
+
+class ProtectedAttr:
+    def __init__(self):
+        self._x = P("protected")
+
+
+class PublicAttr:
+    def __init__(self):
+        self.x = P("public")
+
+
+class SpecialAttr:
+    def __init__(self):
+        self.__x__ = P("special")
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/pub_only.rc b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/pub_only.rc
new file mode 100644
index 0000000000..4dddc11448
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/pub_only.rc
@@ -0,0 +1,2 @@
+[testoptions]
+command_line_args=--filter-mode=PUB_ONLY
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.mmd b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.mmd
new file mode 100644
index 0000000000..149b0a694a
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.mmd
@@ -0,0 +1,18 @@
+classDiagram
+  class P {
+    name : str
+  }
+  class PrivateAttr {
+    __x
+  }
+  class ProtectedAttr {
+    _x
+  }
+  class PublicAttr {
+    x
+  }
+  class SpecialAttr {
+  }
+  P --* PrivateAttr : __x
+  P --* ProtectedAttr : _x
+  P --* PublicAttr : x
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.py b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.py
new file mode 100644
index 0000000000..609f4376be
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.py
@@ -0,0 +1,23 @@
+class P:
+    def __init__(self, name: str):
+        self.name = name
+
+
+class PrivateAttr:
+    def __init__(self):
+        self.__x = P("private")
+
+
+class ProtectedAttr:
+    def __init__(self):
+        self._x = P("protected")
+
+
+class PublicAttr:
+    def __init__(self):
+        self.x = P("public")
+
+
+class SpecialAttr:
+    def __init__(self):
+        self.__x__ = P("special")
diff --git a/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.rc b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.rc
new file mode 100644
index 0000000000..4e60bfdf4d
--- /dev/null
+++ b/tests/pyreverse/functional/class_diagrams/aggregation_filtering/special.rc
@@ -0,0 +1,2 @@
+[testoptions]
+command_line_args=--filter-mode=SPECIAL