From 45ffea92543d1358b87cd759b35b766cc5086d0e Mon Sep 17 00:00:00 2001 From: Georgi Ivanov Date: Sat, 4 Apr 2020 01:15:28 +0100 Subject: [PATCH 01/20] ability to add icons to the cluster label --- diagrams/__init__.py | 33 +++++++++++++++++++++++++++++++-- docs/guides/cluster.md | 25 ++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 9626ea6..f2f034f 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -37,6 +37,10 @@ def getcluster(): def setcluster(cluster): __cluster.set(cluster) +def new_init(cls, init): + def reset_init(*args, **kwargs): + cls.__init__ = init + return reset_init class Diagram: __directions = ("TB", "BT", "LR", "RL") @@ -194,7 +198,13 @@ class Cluster: # FIXME: # Cluster direction does not work now. Graphviz couldn't render # correctly for a subgraph that has a different rank direction. - def __init__(self, label: str = "cluster", direction: str = "LR"): + def __init__( + self, + label: str = "cluster", + direction: str = "LR", + icon: object = None, + icon_size: int = 30 + ): """Cluster represents a cluster context. :param label: Cluster label. @@ -202,14 +212,25 @@ class Cluster: """ self.label = label self.name = "cluster_" + self.label + self.icon = icon + self.icon_size = icon_size self.dot = Digraph(self.name) # Set attributes. for k, v in self._default_graph_attrs.items(): self.dot.graph_attr[k] = v - self.dot.graph_attr["label"] = self.label + # if an icon is set, try to find and instantiate a Node without calling __init__() + # then find it's icon by calling _load_icon() + if self.icon: + _node = self.icon(_no_init=True) + if isinstance(_node,Node): + self.icon_label = '<
' + self.label + '
>' + self.dot.graph_attr["label"] = self.icon_label + else: + self.dot.graph_attr["label"] = self.label + if not self._validate_direction(direction): raise ValueError(f'"{direction}" is not a valid direction') self.dot.graph_attr["rankdir"] = direction @@ -262,6 +283,14 @@ class Node: _height = 1.9 + def __new__(cls, *args, **kwargs): + instance = object.__new__(cls) + lazy = kwargs.pop('_no_init', False) + if not lazy: + return instance + cls.__init__ = new_init(cls, cls.__init__) + return instance + def __init__(self, label: str = ""): """Node represents a system component. diff --git a/docs/guides/cluster.md b/docs/guides/cluster.md index 6cbf820..5001597 100644 --- a/docs/guides/cluster.md +++ b/docs/guides/cluster.md @@ -66,6 +66,29 @@ with Diagram("Event Processing", show=False): handlers >> dw ``` +## Clusters with icons in the label + +You can add a Node icon before the cluster label (and specify its size as well). You need to import the used Node class first. + +```python +from diagrams import Cluster, Diagram +from diagrams.aws.compute import ECS +from diagrams.aws.database import RDS, Aurora +from diagrams.aws.network import Route53, VPC + +with Diagram("Simple Web Service with DB Cluster", show=False): + dns = Route53("dns") + web = ECS("service") + + with Cluster(label='VPC',icon=VPC): + with Cluster("DB Cluster",icon=Aurora,icon_size=30): + db_master = RDS("master") + db_master - [RDS("slave1"), + RDS("slave2")] + + dns >> web >> db_master +``` + ![event processing diagram](/img/event_processing_diagram.png) -> There is no depth limit of nesting. Feel free to create nested clusters as deep as you want. \ No newline at end of file +> There is no depth limit of nesting. Feel free to create nested clusters as deep as you want. From 62a244140123bfd7d70bbec94008015c75ce1cc4 Mon Sep 17 00:00:00 2001 From: Dan Aharon-Shalom Date: Sat, 12 Dec 2020 22:25:05 +0200 Subject: [PATCH 02/20] AWS Branded clusters --- diagrams/__init__.py | 17 +++++--- diagrams/aws/__init__.py | 2 +- diagrams/aws/cluster.py | 104 +++++++++++++++++++++++++++++++++++++++++++++ diagrams/onprem/cluster.py | 15 +++++++ examples/__init__.py | 0 examples/aws.png | Bin 0 -> 68660 bytes examples/aws.py | 26 ++++++++++++ 7 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 diagrams/aws/cluster.py create mode 100644 diagrams/onprem/cluster.py create mode 100644 examples/__init__.py create mode 100644 examples/aws.png create mode 100644 examples/aws.py diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 57c9213..2e41b4e 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -209,6 +209,9 @@ class Cluster: "fontsize": "12", } + _icon = None + _icon_size = 0 + # fmt: on # FIXME: @@ -230,8 +233,10 @@ class Cluster: """ self.label = label self.name = "cluster_" + self.label - self.icon = icon - self.icon_size = icon_size + if not self._icon: + self.icon = icon + if not self._icon_size: + self._icon_size = icon_size self.dot = Digraph(self.name) @@ -241,11 +246,11 @@ class Cluster: # if an icon is set, try to find and instantiate a Node without calling __init__() # then find it's icon by calling _load_icon() - if self.icon: - _node = self.icon(_no_init=True) + if self._icon: + _node = self._icon(_no_init=True) if isinstance(_node,Node): - self.icon_label = '<
' + self.label + '
>' - self.dot.graph_attr["label"] = self.icon_label + self._icon_label = '<
' + self.label + '
>' + self.dot.graph_attr["label"] = self._icon_label else: self.dot.graph_attr["label"] = self.label diff --git a/diagrams/aws/__init__.py b/diagrams/aws/__init__.py index 1550a0d..8c912ba 100644 --- a/diagrams/aws/__init__.py +++ b/diagrams/aws/__init__.py @@ -2,7 +2,7 @@ AWS provides a set of services for Amazon Web Service provider. """ -from diagrams import Node +from diagrams import Node, Cluster class _AWS(Node): diff --git a/diagrams/aws/cluster.py b/diagrams/aws/cluster.py new file mode 100644 index 0000000..6ecbabc --- /dev/null +++ b/diagrams/aws/cluster.py @@ -0,0 +1,104 @@ +from diagrams import Cluster +from diagrams.aws.compute import EC2, ApplicationAutoScaling +from diagrams.aws.network import VPC, PrivateSubnet, PublicSubnet + +class Region(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "dotted", + "labeljust": "l", + "pencolor": "#AEB6BE", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + +class AvailabilityZone(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "dashed", + "labeljust": "l", + "pencolor": "#27a0ff", + "fontname": "sans-serif", + "fontsize": "12", + } + # fmt: on + +class VirtualPrivateCloud(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "", + "labeljust": "l", + "pencolor": "#00D110", + "fontname": "sans-serif", + "fontsize": "12", + } + # fmt: on + _icon = VPC + +class PrivateSubnet(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "", + "labeljust": "l", + "pencolor": "#329CFF", + "fontname": "sans-serif", + "fontsize": "12", + } + # fmt: on + _icon = PrivateSubnet + +class PublicSubnet(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "", + "labeljust": "l", + "pencolor": "#00D110", + "fontname": "sans-serif", + "fontsize": "12", + } + # fmt: on + _icon = PublicSubnet + +class SecurityGroup(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "dashed", + "labeljust": "l", + "pencolor": "#FF361E", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + +class AutoScalling(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "dashed", + "labeljust": "l", + "pencolor": "#FF7D1E", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + _icon = ApplicationAutoScaling + +class EC2Contents(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "", + "labeljust": "l", + "pencolor": "#FFB432", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + _icon = EC2 diff --git a/diagrams/onprem/cluster.py b/diagrams/onprem/cluster.py new file mode 100644 index 0000000..4fd62f0 --- /dev/null +++ b/diagrams/onprem/cluster.py @@ -0,0 +1,15 @@ +from diagrams import Cluster +from diagrams.onprem.compute import Server + +class ServerContents(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "rounded,dotted", + "labeljust": "l", + "pencolor": "#A0A0A0", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + _icon = Server diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/aws.png b/examples/aws.png new file mode 100644 index 0000000000000000000000000000000000000000..f762f76a2de50a9a00c9b90b3a1ebd77cfceaa95 GIT binary patch literal 68660 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

ENW$(cZfl2f#t{5sDg!TJTQ&+$xKYpCJP3dV~u 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 Edge(forward=True, reverse=True) >> d1 + lb >> Edge(forward=True, reverse=True) >> d2 From 676db8edffbe2cd6dad9adafb3cb5d5de8936663 Mon Sep 17 00:00:00 2001 From: Dan Aharon-Shalom Date: Thu, 24 Dec 2020 08:09:33 +0200 Subject: [PATCH 03/20] CR changes --- diagrams/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 2e41b4e..8829a01 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -234,7 +234,7 @@ class Cluster: self.label = label self.name = "cluster_" + self.label if not self._icon: - self.icon = icon + self._icon = icon if not self._icon_size: self._icon_size = icon_size From d6f73ec1c3bc8c771215d4b4d49a6038b4644415 Mon Sep 17 00:00:00 2001 From: Dan Aharon-Shalom Date: Thu, 24 Dec 2020 08:33:14 +0200 Subject: [PATCH 04/20] Azure base clusters --- diagrams/azure/cluster.py | 143 ++++++++++++++++++++++++++++++++++++++++++++++ examples/azure.png | Bin 0 -> 52059 bytes examples/azure.py | 24 ++++++++ 3 files changed, 167 insertions(+) create mode 100644 diagrams/azure/cluster.py create mode 100644 examples/azure.png create mode 100644 examples/azure.py diff --git a/diagrams/azure/cluster.py b/diagrams/azure/cluster.py new file mode 100644 index 0000000..73bfcda --- /dev/null +++ b/diagrams/azure/cluster.py @@ -0,0 +1,143 @@ +from diagrams import Cluster +from diagrams.azure.compute import VM, VMWindows, VMLinux #, VMScaleSet # Depends on PR-404 +from diagrams.azure.network import VirtualNetworks, Subnets, NetworkSecurityGroupsClassic + +class Subscription(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "dotted", + "labeljust": "l", + "pencolor": "#AEB6BE", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + +class Region(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "dotted", + "labeljust": "l", + "pencolor": "#AEB6BE", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + +class AvailabilityZone(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "dashed", + "labeljust": "l", + "pencolor": "#27a0ff", + "fontname": "sans-serif", + "fontsize": "12", + } + # fmt: on + +class VirtualNetwork(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "", + "labeljust": "l", + "pencolor": "#00D110", + "fontname": "sans-serif", + "fontsize": "12", + } + # fmt: on + _icon = VirtualNetworks + +class SubnetWithNSG(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "", + "labeljust": "l", + "pencolor": "#329CFF", + "fontname": "sans-serif", + "fontsize": "12", + } + # fmt: on + _icon = NetworkSecurityGroupsClassic + +class Subnet(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "", + "labeljust": "l", + "pencolor": "#00D110", + "fontname": "sans-serif", + "fontsize": "12", + } + # fmt: on + _icon = Subnets + +class SecurityGroup(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "dashed", + "labeljust": "l", + "pencolor": "#FF361E", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + +class VMContents(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "", + "labeljust": "l", + "pencolor": "#FFB432", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + _icon = VM + +class VMLinuxContents(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "", + "labeljust": "l", + "pencolor": "#FFB432", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + _icon = VMLinux + +class VMWindowsContents(Cluster): + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "", + "labeljust": "l", + "pencolor": "#FFB432", + "fontname": "Sans-Serif", + "fontsize": "12", + } + # fmt: on + _icon = VMWindows + +# Depends on PR-404 +# class VMSS(Cluster): +# # fmt: off +# _default_graph_attrs = { +# "shape": "box", +# "style": "dashed", +# "labeljust": "l", +# "pencolor": "#FF7D1E", +# "fontname": "Sans-Serif", +# "fontsize": "12", +# } +# # fmt: on +# _icon = VMScaleSet diff --git a/examples/azure.png b/examples/azure.png new file mode 100644 index 0000000000000000000000000000000000000000..04af158c1380ba1d9d4de4950bb998f1b40d7c2d GIT binary patch literal 52059 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 literal 0 HcmV?d00001 diff --git a/examples/azure.py b/examples/azure.py new file mode 100644 index 0000000..e094887 --- /dev/null +++ b/examples/azure.py @@ -0,0 +1,24 @@ +from diagrams import Diagram, Edge +from diagrams.azure.cluster import * +from diagrams.azure.compute import VM +from diagrams.onprem.container import Docker +from diagrams.onprem.cluster import * +from diagrams.azure.network import LoadBalancers + +with Diagram(name="", filename="azure", direction="TB", show=True): + with Cluster("Azure"): + with Region("East US2"): + with AvailabilityZone("Zone 2"): + with VirtualNetwork(""): + with SubnetWithNSG("Private"): + # with VMScaleSet(""): # Depends on PR-404 + with VMContents("A"): + d1 = Docker("Container") + with ServerContents("A1"): + d2 = Docker("Container") + + with Subnet("Public"): + lb = LoadBalancers() + + lb >> Edge(forward=True, reverse=True) >> d1 + lb >> Edge(forward=True, reverse=True) >> d2 From dc330102d569671cf0f117739962de8bd75f9cf6 Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Thu, 24 Dec 2020 19:05:09 -0300 Subject: [PATCH 05/20] Allow nodes to be user as cluster --- diagrams/__init__.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++--- docs/guides/cluster.md | 7 ++++ 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 8829a01..556e043 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -112,7 +112,9 @@ class Diagram: elif not filename: filename = "_".join(self.name.split()).lower() self.filename = filename + self.dot = Digraph(self.name, filename=self.filename) + self._nodes = {} # Set attributes. for k, v in self._default_graph_attrs.items(): @@ -150,6 +152,9 @@ class Diagram: return self def __exit__(self, exc_type, exc_value, traceback): + for nodeid, node in self._nodes.items(): + self.dot.node(nodeid, label=node['label'], **node['attrs']) + self.render() # Remove the graphviz file leaving only the image. os.remove(self.filename) @@ -181,7 +186,10 @@ class Diagram: def node(self, nodeid: str, label: str, **attrs) -> None: """Create a new node.""" - self.dot.node(nodeid, label=label, **attrs) + self._nodes[nodeid] = {'label': label, 'attrs': attrs} + + def remove_node(self, nodeid: str) -> None: + del self._nodes[nodeid] def connect(self, node: "Node", node2: "Node", edge: "Edge") -> None: """Connect the two Nodes.""" @@ -239,6 +247,7 @@ class Cluster: self._icon_size = icon_size self.dot = Digraph(self.name) + self._nodes = {} # Set attributes. for k, v in self._default_graph_attrs.items(): @@ -277,13 +286,16 @@ class Cluster: return self def __exit__(self, exc_type, exc_value, traceback): + for nodeid, node in self._nodes.items(): + self.dot.node(nodeid, label=node['label'], **node['attrs']) + if self._parent: self._parent.subgraph(self.dot) else: self._diagram.subgraph(self.dot) setcluster(self._parent) - def _validate_direction(self, direction: str): + def _validate_direction(self, direction: str) -> bool: direction = direction.upper() for v in self.__directions: if v == direction: @@ -292,7 +304,10 @@ class Cluster: def node(self, nodeid: str, label: str, **attrs) -> None: """Create a new node in the cluster.""" - self.dot.node(nodeid, label=label, **attrs) + self._nodes[nodeid] = {'label': label, 'attrs': attrs} + + def remove_node(self, nodeid: str) -> None: + del self._nodes[nodeid] def subgraph(self, dot: Digraph) -> None: self.dot.subgraph(dot) @@ -300,15 +315,30 @@ class Cluster: class Node: """Node represents a node for a specific backend service.""" + __directions = ("TB", "BT", "LR", "RL") + __bgcolors = ("#E5F5FD", "#EBF3E7", "#ECE8F6", "#FDF7E3") + + # fmt: off + _default_graph_attrs = { + "shape": "box", + "style": "rounded", + "labeljust": "l", + "pencolor": "#AEB6BE", + "fontname": "Sans-Serif", + "fontsize": "12", + } _provider = None _type = None _icon_dir = None _icon = None - + _icon_size = 30 + _direction = "TB" _height = 1.9 + # fmt: on + def __new__(cls, *args, **kwargs): instance = object.__new__(cls) lazy = kwargs.pop('_no_init', False) @@ -317,7 +347,11 @@ class Node: cls.__init__ = new_init(cls, cls.__init__) return instance - def __init__(self, label: str = "", **attrs: Dict): + def __init__( + self, + label: str = "", + **attrs: Dict + ): """Node represents a system component. :param label: Node label. @@ -352,6 +386,65 @@ class Node: else: self._diagram.node(self._id, self.label, **self._attrs) + def __enter__(self): + setcluster(self) + self.name = "cluster_" + self.label + self.dot = Digraph(self.name) + self._nodes = {} + + if self._cluster: + self._cluster.remove_node(self._id) + else: + self._diagram.remove_node(self._id) + + # Set attributes. + for k, v in self._default_graph_attrs.items(): + self.dot.graph_attr[k] = v + + if self._icon: + self.dot.graph_attr["label"] = '<'\ + ''\ + '
'\ + '' + self.label + '
>' + + if not self._validate_direction(self._direction): + raise ValueError(f'"{self._direction}" is not a valid direction') + self.dot.graph_attr["rankdir"] = self._direction + + # Set cluster depth for distinguishing the background color + self.depth = self._cluster.depth + 1 if self._cluster else 0 + coloridx = self.depth % len(self.__bgcolors) + self.dot.graph_attr["bgcolor"] = self.__bgcolors[coloridx] + + return self + + def __exit__(self, exc_type, exc_value, traceback): + for nodeid, node in self._nodes.items(): + self.dot.node(nodeid, label=node['label'], **node['attrs']) + + if self._cluster: + self._cluster.subgraph(self.dot) + else: + self._diagram.subgraph(self.dot) + setcluster(self._cluster) + + def _validate_direction(self, direction: str): + direction = direction.upper() + for v in self.__directions: + if v == direction: + return True + return False + + def node(self, nodeid: str, label: str, **attrs) -> None: + """Create a new node in the cluster.""" + self._nodes[nodeid] = {'label': label, 'attrs': attrs} + + def remove_node(self, nodeid: str) -> None: + del self._nodes[nodeid] + + def subgraph(self, dot: Digraph) -> None: + self.dot.subgraph(dot) + def __repr__(self): _name = self.__class__.__name__ return f"<{self._provider}.{self._type}.{_name}>" diff --git a/docs/guides/cluster.md b/docs/guides/cluster.md index 5001597..7594e28 100644 --- a/docs/guides/cluster.md +++ b/docs/guides/cluster.md @@ -70,6 +70,9 @@ with Diagram("Event Processing", show=False): You can add a Node icon before the cluster label (and specify its size as well). You need to import the used Node class first. +It's also possible to use the node in the `with` context adding `cluster=True` to +make it behave like a cluster. + ```python from diagrams import Cluster, Diagram from diagrams.aws.compute import ECS @@ -85,6 +88,10 @@ with Diagram("Simple Web Service with DB Cluster", show=False): db_master = RDS("master") db_master - [RDS("slave1"), RDS("slave2")] + with Aurora("DB Cluster", cluster=True): + db_master = RDS("master") + db_master - [RDS("slave1"), + RDS("slave2")] dns >> web >> db_master ``` From 496a69cb4f8ec8cfe5a6d6f4d82b9db14215682a Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Mon, 28 Dec 2020 16:52:17 -0300 Subject: [PATCH 06/20] Allow node to cluster edges --- diagrams/__init__.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 556e043..4b19d9b 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -115,7 +115,9 @@ class Diagram: self.dot = Digraph(self.name, filename=self.filename) self._nodes = {} + self._edges = {} + self.dot.attr(compound="true") # Set attributes. for k, v in self._default_graph_attrs.items(): self.dot.graph_attr[k] = v @@ -155,6 +157,17 @@ class Diagram: for nodeid, node in self._nodes.items(): self.dot.node(nodeid, label=node['label'], **node['attrs']) + for nodes, edge in self._edges.items(): + node1, node2 = nodes + nodeid1, nodeid2 = node1.nodeid, node2.nodeid + if hasattr(node1, '_nodes') and node1._nodes: + edge._attrs['ltail'] = nodeid1 + nodeid1 = next(iter(node1._nodes.keys())) + if hasattr(node2, '_nodes') and node2._nodes: + edge._attrs['lhead'] = nodeid2 + nodeid2 = next(iter(node2._nodes.keys())) + self.dot.edge(nodeid1, nodeid2, **edge.attrs) + self.render() # Remove the graphviz file leaving only the image. os.remove(self.filename) @@ -193,7 +206,7 @@ class Diagram: def connect(self, node: "Node", node2: "Node", edge: "Edge") -> None: """Connect the two Nodes.""" - self.dot.edge(node.nodeid, node2.nodeid, **edge.attrs) + self._edges[(node, node2)] = edge def subgraph(self, dot: Digraph) -> None: """Create a subgraph for clustering""" @@ -387,16 +400,16 @@ class Node: self._diagram.node(self._id, self.label, **self._attrs) def __enter__(self): - setcluster(self) - self.name = "cluster_" + self.label - self.dot = Digraph(self.name) - self._nodes = {} - if self._cluster: self._cluster.remove_node(self._id) else: self._diagram.remove_node(self._id) + setcluster(self) + self._id = "cluster_" + self.label + self.dot = Digraph(self._id) + self._nodes = {} + # Set attributes. for k, v in self._default_graph_attrs.items(): self.dot.graph_attr[k] = v @@ -421,7 +434,7 @@ class Node: def __exit__(self, exc_type, exc_value, traceback): for nodeid, node in self._nodes.items(): self.dot.node(nodeid, label=node['label'], **node['attrs']) - + if self._cluster: self._cluster.subgraph(self.dot) else: From 0115211b3440bb732bf7552db0532ff2cbd8388d Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Tue, 29 Dec 2020 10:27:24 -0300 Subject: [PATCH 07/20] Allow Node as Cluster with same label --- diagrams/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 4b19d9b..55f6389 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -406,7 +406,7 @@ class Node: self._diagram.remove_node(self._id) setcluster(self) - self._id = "cluster_" + self.label + self._id = "cluster_" + self._id self.dot = Digraph(self._id) self._nodes = {} From fc9ac5bfae369ad7c2805951d4b3fd81fdd784e6 Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Tue, 29 Dec 2020 11:33:21 -0300 Subject: [PATCH 08/20] Allow for empty "Node as Cluster" to render as Node --- diagrams/__init__.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 55f6389..e9da803 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -116,9 +116,10 @@ class Diagram: self.dot = Digraph(self.name, filename=self.filename) self._nodes = {} self._edges = {} + self._subgraphs = [] - self.dot.attr(compound="true") # Set attributes. + self.dot.attr(compound="true") for k, v in self._default_graph_attrs.items(): self.dot.graph_attr[k] = v self.dot.graph_attr["label"] = self.name @@ -156,6 +157,9 @@ class Diagram: def __exit__(self, exc_type, exc_value, traceback): for nodeid, node in self._nodes.items(): self.dot.node(nodeid, label=node['label'], **node['attrs']) + + for dot in self._subgraphs: + self.dot.subgraph(dot) for nodes, edge in self._edges.items(): node1, node2 = nodes @@ -210,7 +214,7 @@ class Diagram: def subgraph(self, dot: Digraph) -> None: """Create a subgraph for clustering""" - self.dot.subgraph(dot) + self._subgraphs.append(dot) def render(self) -> None: self.dot.render(format=self.outformat, view=self.show, quiet=True) @@ -261,6 +265,7 @@ class Cluster: self.dot = Digraph(self.name) self._nodes = {} + self._subgraphs = [] # Set attributes. for k, v in self._default_graph_attrs.items(): @@ -301,6 +306,9 @@ class Cluster: def __exit__(self, exc_type, exc_value, traceback): for nodeid, node in self._nodes.items(): self.dot.node(nodeid, label=node['label'], **node['attrs']) + + for dot in self._subgraphs: + self.dot.subgraph(dot) if self._parent: self._parent.subgraph(self.dot) @@ -323,7 +331,7 @@ class Cluster: del self._nodes[nodeid] def subgraph(self, dot: Digraph) -> None: - self.dot.subgraph(dot) + self._subgraphs.append(dot) class Node: @@ -400,15 +408,10 @@ class Node: self._diagram.node(self._id, self.label, **self._attrs) def __enter__(self): - if self._cluster: - self._cluster.remove_node(self._id) - else: - self._diagram.remove_node(self._id) - setcluster(self) - self._id = "cluster_" + self._id - self.dot = Digraph(self._id) + self.dot = Digraph() self._nodes = {} + self._subgraphs = [] # Set attributes. for k, v in self._default_graph_attrs.items(): @@ -432,8 +435,22 @@ class Node: return self def __exit__(self, exc_type, exc_value, traceback): + if not (self._nodes or self._subgraphs): + return + + if self._cluster: + self._cluster.remove_node(self._id) + else: + self._diagram.remove_node(self._id) + + self._id = "cluster_" + self._id + self.dot.name = self._id + for nodeid, node in self._nodes.items(): self.dot.node(nodeid, label=node['label'], **node['attrs']) + + for dot in self._subgraphs: + self.dot.subgraph(dot) if self._cluster: self._cluster.subgraph(self.dot) @@ -456,7 +473,7 @@ class Node: del self._nodes[nodeid] def subgraph(self, dot: Digraph) -> None: - self.dot.subgraph(dot) + self._subgraphs.append(dot) def __repr__(self): _name = self.__class__.__name__ From bf6a41a512caba6efa5d54025677c7cf2f5321c1 Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Tue, 29 Dec 2020 15:25:55 -0300 Subject: [PATCH 09/20] Convert the "Cluster" class to a "Node" and extract commons to a base class --- diagrams/__init__.py | 322 ++++++++++++++++++--------------------------------- 1 file changed, 113 insertions(+), 209 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index e9da803..df6a8f1 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -19,7 +19,7 @@ def getdiagram(): try: return __diagram.get() except LookupError: - return None + raise EnvironmentError("Global diagrams context not set up") def setdiagram(diagram): @@ -41,8 +41,60 @@ def new_init(cls, init): cls.__init__ = init return reset_init -class Diagram: +class _Cluster: __directions = ("TB", "BT", "LR", "RL") + + def __init__(self, name=None, **kwargs): + self.dot = Digraph(name, **kwargs) + self.depth = 0 + self.nodes = {} + self.subgraphs = [] + + try: + self._parent = getcluster() or getdiagram() + except EnvironmentError: + self._parent = None + + + def __enter__(self): + setcluster(self) + return self + + def __exit__(self, exc_type, exc_value, traceback): + setcluster(self._parent) + + for nodeid, node in self.nodes.items(): + self.dot.node(nodeid, label=node['label'], **node['attrs']) + + for dot in self.subgraphs: + self.dot.subgraph(dot) + + if self._parent: + self._parent.subgraph(self.dot) + + def node(self, nodeid: str, label: str, **attrs) -> None: + """Create a new node.""" + self.nodes[nodeid] = {'label': label, 'attrs': attrs} + + def remove_node(self, nodeid: str) -> None: + del self.nodes[nodeid] + + def subgraph(self, dot: Digraph) -> None: + """Create a subgraph for clustering""" + self.subgraphs.append(dot) + + def _validate_direction(self, direction: str): + direction = direction.upper() + for v in self.__directions: + if v == direction: + return True + return False + + def __str__(self) -> str: + return str(self.dot) + + +class Diagram(_Cluster): __curvestyles = ("ortho", "curved") __outformats = ("png", "jpg", "svg", "pdf") @@ -106,6 +158,7 @@ class Diagram: :param node_attr: Provide node_attr dot config attributes. :param edge_attr: Provide edge_attr dot config attributes. """ + self.name = name if not name and not filename: filename = "diagrams_image" @@ -113,10 +166,8 @@ class Diagram: filename = "_".join(self.name.split()).lower() self.filename = filename - self.dot = Digraph(self.name, filename=self.filename) - self._nodes = {} - self._edges = {} - self._subgraphs = [] + super().__init__(self.name, filename=self.filename) + self.edges = {} # Set attributes. self.dot.attr(compound="true") @@ -147,46 +198,33 @@ class Diagram: self.show = show - def __str__(self) -> str: - return str(self.dot) - def __enter__(self): setdiagram(self) + super().__enter__() return self def __exit__(self, exc_type, exc_value, traceback): - for nodeid, node in self._nodes.items(): - self.dot.node(nodeid, label=node['label'], **node['attrs']) - - for dot in self._subgraphs: - self.dot.subgraph(dot) + super().__exit__(exc_type, exc_value, traceback) + setdiagram(None) - for nodes, edge in self._edges.items(): + for nodes, edge in self.edges.items(): node1, node2 = nodes nodeid1, nodeid2 = node1.nodeid, node2.nodeid - if hasattr(node1, '_nodes') and node1._nodes: + if node1.nodes: edge._attrs['ltail'] = nodeid1 - nodeid1 = next(iter(node1._nodes.keys())) - if hasattr(node2, '_nodes') and node2._nodes: + nodeid1 = next(iter(node1.nodes.keys())) + if node2.nodes: edge._attrs['lhead'] = nodeid2 - nodeid2 = next(iter(node2._nodes.keys())) + nodeid2 = next(iter(node2.nodes.keys())) self.dot.edge(nodeid1, nodeid2, **edge.attrs) self.render() # Remove the graphviz file leaving only the image. os.remove(self.filename) - setdiagram(None) def _repr_png_(self): return self.dot.pipe(format="png") - def _validate_direction(self, direction: str) -> bool: - direction = direction.upper() - for v in self.__directions: - if v == direction: - return True - return False - def _validate_curvestyle(self, curvestyle: str) -> bool: curvestyle = curvestyle.lower() for v in self.__curvestyles: @@ -201,142 +239,16 @@ class Diagram: return True return False - def node(self, nodeid: str, label: str, **attrs) -> None: - """Create a new node.""" - self._nodes[nodeid] = {'label': label, 'attrs': attrs} - - def remove_node(self, nodeid: str) -> None: - del self._nodes[nodeid] - def connect(self, node: "Node", node2: "Node", edge: "Edge") -> None: """Connect the two Nodes.""" - self._edges[(node, node2)] = edge - - def subgraph(self, dot: Digraph) -> None: - """Create a subgraph for clustering""" - self._subgraphs.append(dot) + self.edges[(node, node2)] = edge def render(self) -> None: self.dot.render(format=self.outformat, view=self.show, quiet=True) -class Cluster: - __directions = ("TB", "BT", "LR", "RL") - __bgcolors = ("#E5F5FD", "#EBF3E7", "#ECE8F6", "#FDF7E3") - - # fmt: off - _default_graph_attrs = { - "shape": "box", - "style": "rounded", - "labeljust": "l", - "pencolor": "#AEB6BE", - "fontname": "Sans-Serif", - "fontsize": "12", - } - - _icon = None - _icon_size = 0 - - # fmt: on - - # FIXME: - # Cluster direction does not work now. Graphviz couldn't render - # correctly for a subgraph that has a different rank direction. - def __init__( - self, - label: str = "cluster", - direction: str = "LR", - graph_attr: dict = {}, - icon: object = None, - icon_size: int = 30 - ): - """Cluster represents a cluster context. - - :param label: Cluster label. - :param direction: Data flow direction. Default is 'left to right'. - :param graph_attr: Provide graph_attr dot config attributes. - """ - self.label = label - self.name = "cluster_" + self.label - if not self._icon: - self._icon = icon - if not self._icon_size: - self._icon_size = icon_size - - self.dot = Digraph(self.name) - self._nodes = {} - self._subgraphs = [] - - # Set attributes. - for k, v in self._default_graph_attrs.items(): - self.dot.graph_attr[k] = v - - # if an icon is set, try to find and instantiate a Node without calling __init__() - # then find it's icon by calling _load_icon() - if self._icon: - _node = self._icon(_no_init=True) - if isinstance(_node,Node): - self._icon_label = '<
' + self.label + '
>' - self.dot.graph_attr["label"] = self._icon_label - else: - self.dot.graph_attr["label"] = self.label - - if not self._validate_direction(direction): - raise ValueError(f'"{direction}" is not a valid direction') - self.dot.graph_attr["rankdir"] = direction - - # Node must be belong to a diagrams. - self._diagram = getdiagram() - if self._diagram is None: - raise EnvironmentError("Global diagrams context not set up") - self._parent = getcluster() - - # Set cluster depth for distinguishing the background color - self.depth = self._parent.depth + 1 if self._parent else 0 - coloridx = self.depth % len(self.__bgcolors) - self.dot.graph_attr["bgcolor"] = self.__bgcolors[coloridx] - - # Merge passed in attributes - self.dot.graph_attr.update(graph_attr) - - def __enter__(self): - setcluster(self) - return self - - def __exit__(self, exc_type, exc_value, traceback): - for nodeid, node in self._nodes.items(): - self.dot.node(nodeid, label=node['label'], **node['attrs']) - - for dot in self._subgraphs: - self.dot.subgraph(dot) - - if self._parent: - self._parent.subgraph(self.dot) - else: - self._diagram.subgraph(self.dot) - setcluster(self._parent) - - def _validate_direction(self, direction: str) -> bool: - direction = direction.upper() - for v in self.__directions: - if v == direction: - return True - return False - - def node(self, nodeid: str, label: str, **attrs) -> None: - """Create a new node in the cluster.""" - self._nodes[nodeid] = {'label': label, 'attrs': attrs} - - def remove_node(self, nodeid: str) -> None: - del self._nodes[nodeid] - - def subgraph(self, dot: Digraph) -> None: - self._subgraphs.append(dot) - - -class Node: +class Node(_Cluster): """Node represents a node for a specific backend service.""" - __directions = ("TB", "BT", "LR", "RL") __bgcolors = ("#E5F5FD", "#EBF3E7", "#ECE8F6", "#FDF7E3") # fmt: off @@ -381,46 +293,39 @@ class Node: self._id = self._rand_id() self.label = label + super().__init__() + # fmt: off # If a node has an icon, increase the height slightly to avoid # that label being spanned between icon image and white space. # Increase the height by the number of new lines included in the label. padding = 0.4 * (label.count('\n')) + icon = self._load_icon() self._attrs = { "shape": "none", "height": str(self._height + padding), - "image": self._load_icon(), - } if self._icon else {} + "image": icon, + } if icon else {} # fmt: on self._attrs.update(attrs) - # Node must be belong to a diagrams. - self._diagram = getdiagram() - if self._diagram is None: - raise EnvironmentError("Global diagrams context not set up") - self._cluster = getcluster() - # If a node is in the cluster context, add it to cluster. - if self._cluster: - self._cluster.node(self._id, self.label, **self._attrs) - else: - self._diagram.node(self._id, self.label, **self._attrs) + self._parent.node(self._id, self.label, **self._attrs) def __enter__(self): + super().__enter__() setcluster(self) - self.dot = Digraph() - self._nodes = {} - self._subgraphs = [] # Set attributes. for k, v in self._default_graph_attrs.items(): self.dot.graph_attr[k] = v - if self._icon: + icon = self._load_icon() + if icon: self.dot.graph_attr["label"] = '<'\ ''\ + ''\ '
'\ - '' + self.label + '
>' if not self._validate_direction(self._direction): @@ -428,52 +333,23 @@ class Node: self.dot.graph_attr["rankdir"] = self._direction # Set cluster depth for distinguishing the background color - self.depth = self._cluster.depth + 1 if self._cluster else 0 + self.depth = self._parent.depth + 1 coloridx = self.depth % len(self.__bgcolors) self.dot.graph_attr["bgcolor"] = self.__bgcolors[coloridx] return self def __exit__(self, exc_type, exc_value, traceback): - if not (self._nodes or self._subgraphs): + if not (self.nodes or self.subgraphs): return - if self._cluster: - self._cluster.remove_node(self._id) - else: - self._diagram.remove_node(self._id) - + self._parent.remove_node(self._id) + self._id = "cluster_" + self._id self.dot.name = self._id - for nodeid, node in self._nodes.items(): - self.dot.node(nodeid, label=node['label'], **node['attrs']) - - for dot in self._subgraphs: - self.dot.subgraph(dot) - - if self._cluster: - self._cluster.subgraph(self.dot) - else: - self._diagram.subgraph(self.dot) - setcluster(self._cluster) + super().__exit__(exc_type, exc_value, traceback) - def _validate_direction(self, direction: str): - direction = direction.upper() - for v in self.__directions: - if v == direction: - return True - return False - - def node(self, nodeid: str, label: str, **attrs) -> None: - """Create a new node in the cluster.""" - self._nodes[nodeid] = {'label': label, 'attrs': attrs} - - def remove_node(self, nodeid: str) -> None: - del self._nodes[nodeid] - - def subgraph(self, dot: Digraph) -> None: - self._subgraphs.append(dot) def __repr__(self): _name = self.__class__.__name__ @@ -562,7 +438,7 @@ class Node: if not isinstance(node, Edge): ValueError(f"{node} is not a valid Edge") # An edge must be added on the global diagrams, not a cluster. - self._diagram.connect(self, node, edge) + getdiagram().connect(self, node, edge) return node @staticmethod @@ -570,8 +446,36 @@ class Node: return uuid.uuid4().hex def _load_icon(self): - basedir = Path(os.path.abspath(os.path.dirname(__file__))) - return os.path.join(basedir.parent, self._icon_dir, self._icon) + if self._icon and self._icon_dir: + basedir = Path(os.path.abspath(os.path.dirname(__file__))) + return os.path.join(basedir.parent, self._icon_dir, self._icon) + return None + + +class Cluster(Node): + def __init__( + self, + label: str = "", + direction: str = "LR", + icon: object = None, + icon_size: int = 30, + **attrs: Dict + ): + """Cluster represents a cluster context. + + :param label: Cluster label. + :param direction: Data flow direction. Default is "LR" (left to right). + :param icon: Custom icon for tihs cluster. Must be a node class or reference. + :param icon_size: The icon size. Default is 30. + """ + self._direction = direction + if icon: + _node = icon(_no_init=True) + self._icon = _node._icon + self._icon_dir = _node._icon_dir + if icon_size: + self._icon_size = icon_size + super().__init__(label, **attrs) class Edge: From 678587a2bb15c78c0b5ce9734ce17e977cd42342 Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Tue, 29 Dec 2020 15:53:35 -0300 Subject: [PATCH 10/20] Allow Node icon to be modified --- diagrams/__init__.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index df6a8f1..c307cb2 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -283,11 +283,15 @@ class Node(_Cluster): def __init__( self, label: str = "", + icon: object = None, + icon_size: int = None, **attrs: Dict ): """Node represents a system component. :param label: Node label. + :param icon: Custom icon for tihs cluster. Must be a node class or reference. + :param icon_size: The icon size when used as a Cluster. Default is 30. """ # Generates an ID for identifying a node. self._id = self._rand_id() @@ -295,6 +299,13 @@ class Node(_Cluster): super().__init__() + if icon: + _node = icon(_no_init=True) + self._icon = _node._icon + self._icon_dir = _node._icon_dir + if icon_size: + self._icon_size = icon_size + # fmt: off # If a node has an icon, increase the height slightly to avoid # that label being spanned between icon image and white space. @@ -458,7 +469,7 @@ class Cluster(Node): label: str = "", direction: str = "LR", icon: object = None, - icon_size: int = 30, + icon_size: int = None, **attrs: Dict ): """Cluster represents a cluster context. @@ -469,13 +480,7 @@ class Cluster(Node): :param icon_size: The icon size. Default is 30. """ self._direction = direction - if icon: - _node = icon(_no_init=True) - self._icon = _node._icon - self._icon_dir = _node._icon_dir - if icon_size: - self._icon_size = icon_size - super().__init__(label, **attrs) + super().__init__(label, icon, icon_size, **attrs) class Edge: From 91b57985db4041075b08647bf3a00b47d677ca8b Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Tue, 29 Dec 2020 17:39:18 -0300 Subject: [PATCH 11/20] Fixed issue with nested Clusters and Cluster >> Node edges --- diagrams/__init__.py | 78 +++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index c307cb2..1f953e6 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -60,28 +60,40 @@ class _Cluster: setcluster(self) return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, *args): setcluster(self._parent) - for nodeid, node in self.nodes.items(): - self.dot.node(nodeid, label=node['label'], **node['attrs']) + if not (self.nodes or self.subgraphs): + return - for dot in self.subgraphs: - self.dot.subgraph(dot) + for node in self.nodes.values(): + self.dot.node(node.nodeid, label=node.label, **node._attrs) + + for subgraph in self.subgraphs: + self.dot.subgraph(subgraph.dot) if self._parent: - self._parent.subgraph(self.dot) + self._parent.remove_node(self.nodeid) + self._parent.subgraph(self) - def node(self, nodeid: str, label: str, **attrs) -> None: + def node(self, node: "Node") -> None: """Create a new node.""" - self.nodes[nodeid] = {'label': label, 'attrs': attrs} + self.nodes[node.nodeid] = node def remove_node(self, nodeid: str) -> None: del self.nodes[nodeid] - def subgraph(self, dot: Digraph) -> None: + def subgraph(self, subgraph: "_Cluster") -> None: """Create a subgraph for clustering""" - self.subgraphs.append(dot) + self.subgraphs.append(subgraph) + + @property + def nodes_iter(self): + if self.nodes: + yield from self.nodes.values() + if self.subgraphs: + for subgraph in self.subgraphs: + yield from subgraph.nodes_iter def _validate_direction(self, direction: str): direction = direction.upper() @@ -202,21 +214,21 @@ class Diagram(_Cluster): setdiagram(self) super().__enter__() return self - - def __exit__(self, exc_type, exc_value, traceback): - super().__exit__(exc_type, exc_value, traceback) + + def __exit__(self, *args): + super().__exit__(*args) setdiagram(None) - for nodes, edge in self.edges.items(): - node1, node2 = nodes - nodeid1, nodeid2 = node1.nodeid, node2.nodeid - if node1.nodes: - edge._attrs['ltail'] = nodeid1 - nodeid1 = next(iter(node1.nodes.keys())) - if node2.nodes: - edge._attrs['lhead'] = nodeid2 - nodeid2 = next(iter(node2.nodes.keys())) - self.dot.edge(nodeid1, nodeid2, **edge.attrs) + for (node1, node2), edge in self.edges.items(): + cluster_node1 = next(node1.nodes_iter, None) + if cluster_node1: + edge._attrs['ltail'] = node1.nodeid + node1 = cluster_node1 + cluster_node2 = next(node2.nodes_iter, None) + if cluster_node2: + edge._attrs['lhead'] = node2.nodeid + node2 = cluster_node2 + self.dot.edge(node1.nodeid, node2.nodeid, **edge.attrs) self.render() # Remove the graphviz file leaving only the image. @@ -322,11 +334,10 @@ class Node(_Cluster): self._attrs.update(attrs) # If a node is in the cluster context, add it to cluster. - self._parent.node(self._id, self.label, **self._attrs) + self._parent.node(self) def __enter__(self): super().__enter__() - setcluster(self) # Set attributes. for k, v in self._default_graph_attrs.items(): @@ -350,17 +361,10 @@ class Node(_Cluster): return self - def __exit__(self, exc_type, exc_value, traceback): - if not (self.nodes or self.subgraphs): - return - - self._parent.remove_node(self._id) - - self._id = "cluster_" + self._id - self.dot.name = self._id - - super().__exit__(exc_type, exc_value, traceback) - + def __exit__(self, *args): + super().__exit__(*args) + self._id = "cluster_" + self.nodeid + self.dot.name = self.nodeid def __repr__(self): _name = self.__class__.__name__ @@ -435,7 +439,7 @@ class Node(_Cluster): @property def nodeid(self): return self._id - + # TODO: option for adding flow description to the connection edge def connect(self, node: "Node", edge: "Edge"): """Connect to other node. From 417047dae79c902c91b48940bb36214f1fd9dff0 Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Tue, 29 Dec 2020 18:19:27 -0300 Subject: [PATCH 12/20] Escape HTML entities of Cluster label --- diagrams/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 1f953e6..19b2315 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -1,4 +1,5 @@ import contextvars +import html import os import uuid from pathlib import Path @@ -348,7 +349,7 @@ class Node(_Cluster): self.dot.graph_attr["label"] = '<'\ ''\ - '
'\ '' + self.label + '
>' + '' + html.escape(self.label) + '>' if not self._validate_direction(self._direction): raise ValueError(f'"{self._direction}" is not a valid direction') From 070308c25427ab0a288aa0222716f761e6cd7420 Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Tue, 29 Dec 2020 21:24:53 -0300 Subject: [PATCH 13/20] Better handling of multiline Cluster labels --- diagrams/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 19b2315..ca84e75 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -346,10 +346,12 @@ class Node(_Cluster): icon = self._load_icon() if icon: - self.dot.graph_attr["label"] = '<'\ - ''\ - '
'\ - '' + html.escape(self.label) + '
>' + lines = iter(html.escape(self.label).split("\n")) + self.dot.graph_attr["label"] = '<' +\ + f'' +\ + f'' +\ + ''.join(f'' for line in lines) +\ + '
{next(lines)}
{line}
>' if not self._validate_direction(self._direction): raise ValueError(f'"{self._direction}" is not a valid direction') From 8266f7d84c9458a506aa037364fee2e9f300c92c Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Wed, 30 Dec 2020 11:03:57 -0300 Subject: [PATCH 14/20] Set Node tooltip to the Class name --- diagrams/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index ca84e75..6ac564f 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -324,12 +324,14 @@ class Node(_Cluster): # that label being spanned between icon image and white space. # Increase the height by the number of new lines included in the label. padding = 0.4 * (label.count('\n')) - icon = self._load_icon() + icon_path = self._load_icon() self._attrs = { "shape": "none", "height": str(self._height + padding), - "image": icon, - } if icon else {} + "image": icon_path, + } if icon_path else {} + + self._attrs['tooltip'] = (icon if icon else self).__class__.__name__ # fmt: on self._attrs.update(attrs) @@ -343,6 +345,7 @@ class Node(_Cluster): # Set attributes. for k, v in self._default_graph_attrs.items(): self.dot.graph_attr[k] = v + self.dot.graph_attr['tooltip'] = self._attrs['tooltip'] icon = self._load_icon() if icon: @@ -536,6 +539,7 @@ class Edge: # Graphviz complaining about using label for edges, so replace it with xlabel. # Update: xlabel option causes the misaligned label position: https://github.com/mingrammer/diagrams/issues/83 self._attrs["label"] = label + self._attrs["tooltip"] = label if color: self._attrs["color"] = color if style: From fb6a2dd40eb6a81506ce8eed4fc9b3c9e9e6d388 Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Wed, 30 Dec 2020 15:08:41 -0300 Subject: [PATCH 15/20] Fix Node Cluster "direction", despite it don't work --- diagrams/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 6ac564f..669c434 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -280,7 +280,7 @@ class Node(_Cluster): _icon_dir = None _icon = None _icon_size = 30 - _direction = "TB" + _direction = "LR" _height = 1.9 # fmt: on @@ -296,6 +296,7 @@ class Node(_Cluster): def __init__( self, label: str = "", + direction: str = None, icon: object = None, icon_size: int = None, **attrs: Dict @@ -303,6 +304,7 @@ class Node(_Cluster): """Node represents a system component. :param label: Node label. + :param direction: Data flow direction. Default is "LR" (left to right). :param icon: Custom icon for tihs cluster. Must be a node class or reference. :param icon_size: The icon size when used as a Cluster. Default is 30. """ @@ -312,6 +314,10 @@ class Node(_Cluster): super().__init__() + if direction: + if not self._validate_direction(direction): + raise ValueError(f'"{direction}" is not a valid direction') + self._direction = direction if icon: _node = icon(_no_init=True) self._icon = _node._icon @@ -356,8 +362,6 @@ class Node(_Cluster): ''.join(f'{line}' for line in lines) +\ '>' - if not self._validate_direction(self._direction): - raise ValueError(f'"{self._direction}" is not a valid direction') self.dot.graph_attr["rankdir"] = self._direction # Set cluster depth for distinguishing the background color @@ -477,7 +481,7 @@ class Cluster(Node): def __init__( self, label: str = "", - direction: str = "LR", + direction: str = None, icon: object = None, icon_size: int = None, **attrs: Dict @@ -489,8 +493,7 @@ class Cluster(Node): :param icon: Custom icon for tihs cluster. Must be a node class or reference. :param icon_size: The icon size. Default is 30. """ - self._direction = direction - super().__init__(label, icon, icon_size, **attrs) + super().__init__(label, direction, icon, icon_size, **attrs) class Edge: From f7dad6bb0e4dc0b52dd4f91a1efdaef8348ce357 Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Wed, 30 Dec 2020 15:29:30 -0300 Subject: [PATCH 16/20] Make Cluster and alias of Node --- diagrams/__init__.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 669c434..0cfc3dc 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -477,25 +477,6 @@ class Node(_Cluster): return None -class Cluster(Node): - def __init__( - self, - label: str = "", - direction: str = None, - icon: object = None, - icon_size: int = None, - **attrs: Dict - ): - """Cluster represents a cluster context. - - :param label: Cluster label. - :param direction: Data flow direction. Default is "LR" (left to right). - :param icon: Custom icon for tihs cluster. Must be a node class or reference. - :param icon_size: The icon size. Default is 30. - """ - super().__init__(label, direction, icon, icon_size, **attrs) - - class Edge: """Edge represents an edge between two nodes.""" @@ -615,4 +596,4 @@ class Edge: return {**self._attrs, "dir": direction} -Group = Cluster +Group = Cluster = Node From d3ee0b66bc631d93d559fbe3ecac10c695e93bec Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Wed, 30 Dec 2020 15:47:41 -0300 Subject: [PATCH 17/20] Fix node attrs not being copied to Cluster --- diagrams/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 0cfc3dc..cd8b89b 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -351,7 +351,8 @@ class Node(_Cluster): # Set attributes. for k, v in self._default_graph_attrs.items(): self.dot.graph_attr[k] = v - self.dot.graph_attr['tooltip'] = self._attrs['tooltip'] + for k, v in self._attrs.items(): + self.dot.graph_attr[k] = v icon = self._load_icon() if icon: From 1067cf50db2547560164a747df8af15bfad7a26e Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Wed, 30 Dec 2020 16:14:56 -0300 Subject: [PATCH 18/20] Allow multiline labels be defined as a sequence --- diagrams/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index cd8b89b..6261441 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -3,7 +3,7 @@ import html import os import uuid from pathlib import Path -from typing import List, Union, Dict +from typing import List, Union, Dict, Sequence from graphviz import Digraph @@ -310,7 +310,12 @@ class Node(_Cluster): """ # Generates an ID for identifying a node. self._id = self._rand_id() - self.label = label + if isinstance(label, str): + self.label = label + elif isinstance(label, Sequence): + self.label = "\n".join(label) + else: + self.label = str(label) super().__init__() @@ -329,7 +334,7 @@ class Node(_Cluster): # If a node has an icon, increase the height slightly to avoid # that label being spanned between icon image and white space. # Increase the height by the number of new lines included in the label. - padding = 0.4 * (label.count('\n')) + padding = 0.4 * (self.label.count('\n')) icon_path = self._load_icon() self._attrs = { "shape": "none", From 854fff5947b783949d9590306a7e106a6aff490c Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Mon, 4 Jan 2021 10:20:15 -0300 Subject: [PATCH 19/20] Fix unit tests --- diagrams/__init__.py | 9 ++++----- tests/test_diagram.py | 10 +++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 6261441..6610a78 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -17,10 +17,7 @@ __cluster = contextvars.ContextVar("cluster") def getdiagram(): - try: - return __diagram.get() - except LookupError: - raise EnvironmentError("Global diagrams context not set up") + return __diagram.get() def setdiagram(diagram): @@ -53,7 +50,7 @@ class _Cluster: try: self._parent = getcluster() or getdiagram() - except EnvironmentError: + except LookupError: self._parent = None @@ -348,6 +345,8 @@ class Node(_Cluster): self._attrs.update(attrs) # If a node is in the cluster context, add it to cluster. + if not self._parent: + raise EnvironmentError("Global diagrams context not set up") self._parent.node(self) def __enter__(self): diff --git a/tests/test_diagram.py b/tests/test_diagram.py index ad8558c..0242e9a 100644 --- a/tests/test_diagram.py +++ b/tests/test_diagram.py @@ -135,20 +135,20 @@ class ClusterTest(unittest.TestCase): def test_with_global_context(self): with Diagram(name=os.path.join(self.name, "with_global_context"), show=False): - self.assertIsNone(getcluster()) + self.assertEqual(getcluster(), getdiagram()) with Cluster(): - self.assertIsNotNone(getcluster()) - self.assertIsNone(getcluster()) + self.assertNotEqual(getcluster(), getdiagram()) + self.assertEqual(getcluster(), getdiagram()) def test_with_nested_cluster(self): with Diagram(name=os.path.join(self.name, "with_nested_cluster"), show=False): - self.assertIsNone(getcluster()) + self.assertEqual(getcluster(), getdiagram()) with Cluster() as c1: self.assertEqual(c1, getcluster()) with Cluster() as c2: self.assertEqual(c2, getcluster()) self.assertEqual(c1, getcluster()) - self.assertIsNone(getcluster()) + self.assertEqual(getcluster(), getdiagram()) def test_node_not_in_diagram(self): # Node must be belong to a diagrams. From c7a10b742f579b4e81b6750300092e5fc3b9a94a Mon Sep 17 00:00:00 2001 From: Bruno Meneguello <1322552+bkmeneguello@users.noreply.github.com> Date: Wed, 10 Feb 2021 11:19:41 -0300 Subject: [PATCH 20/20] Fix missing cluster label when Node is used --- diagrams/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 6610a78..eea39c3 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -366,6 +366,8 @@ class Node(_Cluster): f'{next(lines)}' +\ ''.join(f'{line}' for line in lines) +\ '>' + else: + self.dot.graph_attr["label"] = self.label self.dot.graph_attr["rankdir"] = self._direction