summaryrefslogtreecommitdiff
path: root/pylint/pyreverse
diff options
context:
space:
mode:
authorAlvaro Frias <alvaro.frias@eclypsium.com>2022-11-27 12:18:43 -0300
committerGitHub <noreply@github.com>2022-11-27 16:18:43 +0100
commitb950567b872c7177f976066e9b8982f08e62eb6c (patch)
treeee0a1c30ded4eb4ce2267adcc52b9dcfb9865039 /pylint/pyreverse
parent978d1ab95603fa337e686aac8956366556c23080 (diff)
downloadpylint-git-b950567b872c7177f976066e9b8982f08e62eb6c.tar.gz
Feature: distinct Composition and Aggregation arrows (#7836)
* Update Linker to add aggregations_type and associations_type to nodes Update logic of nodes to check what kind of relationship does nodes have (association, aggregation) Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update ClassDiagram's extrac_relationship method to add aggregations links Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update DiagramWriter to generate AGGREGATION edges Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update printers to show aggregations Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update tests with changes on aggregations Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update Linker to add aggregations_type and associations_type to nodes Update logic of nodes to check what kind of relationship does nodes have (association, aggregation) Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update ClassDiagram's extrac_relationship method to add aggregations links Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update DiagramWriter to generate AGGREGATION edges Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update tests with changes on aggregations Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Apply pylint pre-commit correction Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Apply mypy corrections Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add towrncrier fragment Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update towncrier fragment Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update towncrier fragment Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update doc/whatsnew/fragments/6543.feature Co-authored-by: Andreas Finkler <3929834+DudeNr33@users.noreply.github.com> * Add documentation Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * fix typo Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Add type hints Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * Update pylint/pyreverse/diagrams.py Co-authored-by: Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update type hints Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update fragment * Update fragment Signed-off-by: Alvaro Frias Garay <alvaro.frias@eclypsium.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Andreas Finkler <3929834+DudeNr33@users.noreply.github.com> Co-authored-by: Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
Diffstat (limited to 'pylint/pyreverse')
-rw-r--r--pylint/pyreverse/diagrams.py37
-rw-r--r--pylint/pyreverse/dot_printer.py6
-rw-r--r--pylint/pyreverse/inspector.py67
-rw-r--r--pylint/pyreverse/mermaidjs_printer.py1
-rw-r--r--pylint/pyreverse/plantuml_printer.py1
-rw-r--r--pylint/pyreverse/printer.py1
-rw-r--r--pylint/pyreverse/vcg_printer.py5
-rw-r--r--pylint/pyreverse/writer.py8
8 files changed, 115 insertions, 11 deletions
diff --git a/pylint/pyreverse/diagrams.py b/pylint/pyreverse/diagrams.py
index a9dcd1c5e..382d76bf7 100644
--- a/pylint/pyreverse/diagrams.py
+++ b/pylint/pyreverse/diagrams.py
@@ -214,20 +214,35 @@ class ClassDiagram(Figure, FilterMixIn):
self.add_relationship(obj, impl_obj, "implements")
except KeyError:
continue
- # associations link
- for name, values in list(node.instance_attrs_type.items()) + list(
+
+ # associations & aggregations links
+ for name, values in list(node.aggregations_type.items()):
+ for value in values:
+ self.assign_association_relationship(
+ value, obj, name, "aggregation"
+ )
+
+ for name, values in list(node.associations_type.items()) + list(
node.locals_type.items()
):
+
for value in values:
- if value is astroid.Uninferable:
- continue
- if isinstance(value, astroid.Instance):
- value = value._proxied
- try:
- associated_obj = self.object_from_node(value)
- self.add_relationship(associated_obj, obj, "association", name)
- except KeyError:
- continue
+ self.assign_association_relationship(
+ value, obj, name, "association"
+ )
+
+ def assign_association_relationship(
+ self, value: astroid.NodeNG, obj: ClassEntity, name: str, type_relationship: str
+ ) -> None:
+ if value is astroid.Uninferable:
+ return
+ if isinstance(value, astroid.Instance):
+ value = value._proxied
+ try:
+ associated_obj = self.object_from_node(value)
+ self.add_relationship(associated_obj, obj, type_relationship, name)
+ except KeyError:
+ return
class PackageDiagram(ClassDiagram):
diff --git a/pylint/pyreverse/dot_printer.py b/pylint/pyreverse/dot_printer.py
index 7579d2877..1d5f2c32b 100644
--- a/pylint/pyreverse/dot_printer.py
+++ b/pylint/pyreverse/dot_printer.py
@@ -39,6 +39,12 @@ ARROWS: dict[EdgeType, dict[str, str]] = {
"arrowhead": "diamond",
"style": "solid",
},
+ EdgeType.AGGREGATION: {
+ "fontcolor": "green",
+ "arrowtail": "none",
+ "arrowhead": "odiamond",
+ "style": "solid",
+ },
EdgeType.USES: {"arrowtail": "none", "arrowhead": "open"},
}
diff --git a/pylint/pyreverse/inspector.py b/pylint/pyreverse/inspector.py
index 042d3845e..8c403ffc6 100644
--- a/pylint/pyreverse/inspector.py
+++ b/pylint/pyreverse/inspector.py
@@ -13,6 +13,7 @@ import collections
import os
import traceback
import warnings
+from abc import ABC, abstractmethod
from collections.abc import Generator
from typing import Any, Callable, Optional
@@ -123,6 +124,12 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor):
* instance_attrs_type
as locals_type but for klass member attributes (only on astroid.Class)
+ * associations_type
+ as instance_attrs_type but for association relationships
+
+ * aggregations_type
+ as instance_attrs_type but for aggregations relationships
+
* implements,
list of implemented interface _objects_ (only on astroid.Class nodes)
"""
@@ -134,6 +141,8 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor):
self.tag = tag
# visited project
self.project = project
+ self.associations_handler = AggregationsHandler()
+ self.associations_handler.set_next(OtherAssociationsHandler())
def visit_project(self, node: Project) -> None:
"""Visit a pyreverse.utils.Project node.
@@ -178,9 +187,12 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor):
baseobj.specializations = specializations
# resolve instance attributes
node.instance_attrs_type = collections.defaultdict(list)
+ node.aggregations_type = collections.defaultdict(list)
+ node.associations_type = collections.defaultdict(list)
for assignattrs in tuple(node.instance_attrs.values()):
for assignattr in assignattrs:
if not isinstance(assignattr, nodes.Unknown):
+ self.associations_handler.handle(assignattr, node)
self.handle_assignattr_type(assignattr, node)
# resolve implemented interface
try:
@@ -313,6 +325,61 @@ class Linker(IdGeneratorMixIn, utils.LocalsVisitor):
mod_paths.append(mod_path)
+class AssociationHandlerInterface(ABC):
+ @abstractmethod
+ def set_next(
+ self, handler: AssociationHandlerInterface
+ ) -> AssociationHandlerInterface:
+ pass
+
+ @abstractmethod
+ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
+ pass
+
+
+class AbstractAssociationHandler(AssociationHandlerInterface):
+ """
+ Chain of Responsibility for handling types of association, useful
+ to expand in the future if we want to add more distinct associations.
+
+ Every link of the chain checks if it's a certain type of association.
+ If no association is found it's set as a generic association in `associations_type`.
+
+ The default chaining behavior is implemented inside the base handler
+ class.
+ """
+
+ _next_handler: AssociationHandlerInterface
+
+ def set_next(
+ self, handler: AssociationHandlerInterface
+ ) -> AssociationHandlerInterface:
+ self._next_handler = handler
+ return handler
+
+ @abstractmethod
+ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
+ if self._next_handler:
+ self._next_handler.handle(node, parent)
+
+
+class AggregationsHandler(AbstractAssociationHandler):
+ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
+ if isinstance(node.parent.value, astroid.node_classes.Name):
+ current = set(parent.aggregations_type[node.attrname])
+ parent.aggregations_type[node.attrname] = list(
+ current | utils.infer_node(node)
+ )
+ else:
+ super().handle(node, parent)
+
+
+class OtherAssociationsHandler(AbstractAssociationHandler):
+ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
+ current = set(parent.associations_type[node.attrname])
+ parent.associations_type[node.attrname] = list(current | utils.infer_node(node))
+
+
def project_from_files(
files: list[str],
func_wrapper: _WrapperFuncT = _astroid_wrapper,
diff --git a/pylint/pyreverse/mermaidjs_printer.py b/pylint/pyreverse/mermaidjs_printer.py
index 07cad97be..a8f3c576b 100644
--- a/pylint/pyreverse/mermaidjs_printer.py
+++ b/pylint/pyreverse/mermaidjs_printer.py
@@ -24,6 +24,7 @@ class MermaidJSPrinter(Printer):
EdgeType.INHERITS: "--|>",
EdgeType.IMPLEMENTS: "..|>",
EdgeType.ASSOCIATION: "--*",
+ EdgeType.AGGREGATION: "--o",
EdgeType.USES: "-->",
}
diff --git a/pylint/pyreverse/plantuml_printer.py b/pylint/pyreverse/plantuml_printer.py
index f3f639f5c..56463165d 100644
--- a/pylint/pyreverse/plantuml_printer.py
+++ b/pylint/pyreverse/plantuml_printer.py
@@ -24,6 +24,7 @@ class PlantUmlPrinter(Printer):
EdgeType.INHERITS: "--|>",
EdgeType.IMPLEMENTS: "..|>",
EdgeType.ASSOCIATION: "--*",
+ EdgeType.AGGREGATION: "--o",
EdgeType.USES: "-->",
}
diff --git a/pylint/pyreverse/printer.py b/pylint/pyreverse/printer.py
index 55ce2c8b1..cdbf7e3c8 100644
--- a/pylint/pyreverse/printer.py
+++ b/pylint/pyreverse/printer.py
@@ -25,6 +25,7 @@ class EdgeType(Enum):
INHERITS = "inherits"
IMPLEMENTS = "implements"
ASSOCIATION = "association"
+ AGGREGATION = "aggregation"
USES = "uses"
diff --git a/pylint/pyreverse/vcg_printer.py b/pylint/pyreverse/vcg_printer.py
index 6f28a24e8..b9e2e94f3 100644
--- a/pylint/pyreverse/vcg_printer.py
+++ b/pylint/pyreverse/vcg_printer.py
@@ -177,6 +177,11 @@ ARROWS: dict[EdgeType, dict[str, str | int]] = {
"backarrowstyle": "none",
"textcolor": "green",
},
+ EdgeType.AGGREGATION: {
+ "arrowstyle": "solid",
+ "backarrowstyle": "none",
+ "textcolor": "green",
+ },
}
ORIENTATION: dict[Layout, str] = {
Layout.LEFT_TO_RIGHT: "left_to_right",
diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py
index b19286a87..68a49eea1 100644
--- a/pylint/pyreverse/writer.py
+++ b/pylint/pyreverse/writer.py
@@ -120,6 +120,14 @@ class DiagramWriter:
label=rel.name,
type_=EdgeType.ASSOCIATION,
)
+ # generate aggregations
+ for rel in diagram.get_relationships("aggregation"):
+ self.printer.emit_edge(
+ rel.from_object.fig_id,
+ rel.to_object.fig_id,
+ label=rel.name,
+ type_=EdgeType.AGGREGATION,
+ )
def set_printer(self, file_name: str, basename: str) -> None:
"""Set printer."""