Edge attributes support (#48)

* Edges support

* Documentation and additional style attribute

* Better example and review changes

* Fix xlabel

* Fix docs

* Add Edge default properties

* Fix edge docs

Co-authored-by: Andrew Selivanov <a_selivanov@wargaming.net>
pull/61/head
Andrew Selivanov 6 years ago committed by GitHub
parent f5ba01faba
commit ca1e7ec02a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,7 +3,7 @@ import os
from hashlib import md5 from hashlib import md5
from pathlib import Path from pathlib import Path
from random import getrandbits from random import getrandbits
from typing import List, Union from typing import List, Union, Dict
from graphviz import Digraph from graphviz import Digraph
@ -129,6 +129,9 @@ class Diagram:
self.show = show self.show = show
def __str__(self) -> str:
return str(self.dot)
def __enter__(self): def __enter__(self):
setdiagram(self) setdiagram(self)
return self return self
@ -160,15 +163,9 @@ class Diagram:
"""Create a new node.""" """Create a new node."""
self.dot.node(hashid, label=label, **attrs) self.dot.node(hashid, label=label, **attrs)
def connect(self, node: "Node", node2: "Node", directed=True) -> None: def connect(self, node: "Node", node2: "Node", edge: "Edge") -> None:
"""Connect the two Nodes.""" """Connect the two Nodes."""
attrs = {"dir": "none"} if not directed else {} self.dot.edge(node.hashid, node2.hashid, **edge.attrs)
self.dot.edge(node.hashid, node2.hashid, **attrs)
def reverse(self, node: "Node", node2: "Node", directed=True) -> None:
"""Connect the two Nodes in reverse direction."""
attrs = {"dir": "none"} if not directed else {"dir": "back"}
self.dot.edge(node.hashid, node2.hashid, **attrs)
def subgraph(self, dot: Digraph) -> None: def subgraph(self, dot: Digraph) -> None:
"""Create a subgraph for clustering""" """Create a subgraph for clustering"""
@ -302,54 +299,70 @@ class Node:
_name = self.__class__.__name__ _name = self.__class__.__name__
return f"<{self._provider}.{self._type}.{_name}>" return f"<{self._provider}.{self._type}.{_name}>"
def __sub__(self, other: Union["Node", List["Node"]]): def __sub__(self, other: Union["Node", List["Node"], "Edge"]):
"""Implement Self - Node and Self - [Nodes]""" """Implement Self - Node, Self - [Nodes] and Self - Edge."""
if not isinstance(other, list): if isinstance(other, list):
return self.connect(other, directed=False)
for node in other: for node in other:
self.connect(node, directed=False) self.connect(node, Edge(self))
return other
elif isinstance(other, Node):
return self.connect(other, Edge(self))
else:
other.node = self
return other return other
def __rsub__(self, other: List["Node"]): def __rsub__(self, other: Union[List["Node"], List["Edge"]]):
""" """ Called for [Nodes] and [Edges] - Self because list don't have __sub__ operators. """
Called for [Nodes] - Self because list of Nodes don't have for o in other:
__sub__ operators. if isinstance(o, Edge):
""" o.connect(self)
self.__sub__(other) else:
o.connect(self, Edge(self))
return self return self
def __rshift__(self, other: Union["Node", List["Node"]]): def __rshift__(self, other: Union["Node", List["Node"], "Edge"]):
"""Implements Self >> Node and Self >> [Nodes].""" """Implements Self >> Node, Self >> [Nodes] and Self Edge."""
if not isinstance(other, list): if isinstance(other, list):
return self.connect(other)
for node in other: for node in other:
self.connect(node) self.connect(node, Edge(self, forward=True))
return other return other
elif isinstance(other, Node):
def __lshift__(self, other: Union["Node", List["Node"]]): return self.connect(other, Edge(self, forward=True))
"""Implements Self << Node and Self << [Nodes].""" else:
if not isinstance(other, list): other.forward = True
return self.reverse(other) other.node = self
for node in other:
self.reverse(node)
return other return other
def __rrshift__(self, other: List["Node"]): def __lshift__(self, other: Union["Node", List["Node"], "Edge"]):
""" """Implements Self << Node, Self << [Nodes] and Self << Edge."""
Called for [Nodes] >> Self because list of Nodes don't have if isinstance(other, list):
__rshift__ operators.
"""
for node in other: for node in other:
node.connect(self) self.connect(node, Edge(self, reverse=True))
return other
elif isinstance(other, Node):
return self.connect(other, Edge(self, reverse=True))
else:
other.reverse = True
return other.connect(self)
def __rrshift__(self, other: Union[List["Node"], List["Edge"]]):
"""Called for [Nodes] and [Edges] >> Self because list don't have __rshift__ operators."""
for o in other:
if isinstance(o, Edge):
o.forward = True
o.connect(self)
else:
o.connect(self, Edge(self, forward=True))
return self return self
def __rlshift__(self, other: List["Node"]): def __rlshift__(self, other: Union[List["Node"], List["Edge"]]):
""" """Called for [Nodes] << Self because list of Nodes don't have __lshift__ operators."""
Called for [Nodes] << Self because list of Nodes don't have for o in other:
__lshift__ operators. if isinstance(o, Edge):
""" o.reverse = True
for node in other: o.connect(self)
node.reverse(self) else:
o.connect(self, Edge(self, reverse=True))
return self return self
@property @property
@ -357,30 +370,19 @@ class Node:
return self._hash return self._hash
# TODO: option for adding flow description to the connection edge # TODO: option for adding flow description to the connection edge
def connect(self, node: "Node", directed=True): def connect(self, node: "Node", edge: "Edge"):
"""Connect to other node. """Connect to other node.
:param node: Other node instance. :param node: Other node instance.
:param directed: Whether the flow is directed or not. :param edge: Type of the edge.
:return: Connected node.
"""
if not isinstance(node, Node):
ValueError(f"{node} is not a valid Node")
# An edge must be added on the global diagrams, not a cluster.
self._diagram.connect(self, node, directed)
return node
def reverse(self, node: "Node", directed=True):
"""Connect to other node in reverse direction.
:param node: Other node instance.
:param directed: Whether the flow is directed or not.
:return: Connected node. :return: Connected node.
""" """
if not isinstance(node, Node): if not isinstance(node, Node):
ValueError(f"{node} is not a valid Node") ValueError(f"{node} is not a valid 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. # An edge must be added on the global diagrams, not a cluster.
self._diagram.reverse(self, node, directed) self._diagram.connect(self, node, edge)
return node return node
@staticmethod @staticmethod
@ -392,4 +394,125 @@ class Node:
return os.path.join(basedir.parent, self._icon_dir, self._icon) return os.path.join(basedir.parent, self._icon_dir, self._icon)
class Edge:
"""Edge represents an edge between two nodes."""
_default_edge_attrs = {
"fontcolor": "#2D3436",
"fontname": "Sans-Serif",
"fontsize": "13",
}
def __init__(self,
node: "Node" = None,
forward: bool = False,
reverse: bool = False,
xlabel: str = "",
label: str = "",
color: str = "",
style: str = "",
fontcolor: str = "",
fontname: str = "",
fontsize: str = "",
):
"""Edge represents an edge between two nodes.
:param node: Parent node.
:param forward: Points forward.
:param reverse: Points backward.
:param label: Edge label.
:param color: Edge color.
:param style: Edge style.
:param label: Edge font color.
:param color: Edge font name.
:param style: Edge font size.
"""
if node is not None:
assert isinstance(node, Node)
self.node = node
self.forward = forward
self.reverse = reverse
self._attrs = {}
# Set attributes.
for k, v in self._default_edge_attrs.items():
self._attrs[k] = v
# Graphviz complaining about using label for edges, so replace it with xlabel.
self._attrs["xlabel"] = label if label else xlabel
self._attrs["color"] = color
self._attrs["style"] = style
self._attrs["fontcolor"] = fontcolor
self._attrs["fontname"] = fontname
self._attrs["fontsize"] = fontsize
def __sub__(self, other: Union["Node", "Edge", List["Node"]]):
"""Implement Self - Node or Edge and Self - [Nodes]"""
return self.connect(other)
def __rsub__(self, other: Union[List["Node"], List["Edge"]]) -> List["Edge"]:
"""Called for [Nodes] or [Edges] - Self because list don't have __sub__ operators."""
return self.append(other)
def __rshift__(self, other: Union["Node", "Edge", List["Node"]]):
"""Implements Self >> Node or Edge and Self >> [Nodes]."""
self.forward = True
return self.connect(other)
def __lshift__(self, other: Union["Node", "Edge", List["Node"]]):
"""Implements Self << Node or Edge and Self << [Nodes]."""
self.reverse = True
return self.connect(other)
def __rrshift__(self, other: Union[List["Node"], List["Edge"]]) -> List["Edge"]:
"""Called for [Nodes] or [Edges] >> Self because list of Edges don't have __rshift__ operators."""
return self.append(other, forward=True)
def __rlshift__(self, other: Union[List["Node"], List["Edge"]]) -> List["Edge"]:
"""Called for [Nodes] or [Edges] << Self because list of Edges don't have __lshift__ operators."""
return self.append(other, reverse=True)
def append(self, other: Union[List["Node"], List["Edge"]], forward=None, reverse=None) -> List["Edge"]:
result = []
for o in other:
if isinstance(o, Edge):
o.forward = forward if forward is not None else o.forward
o.reverse = forward if forward is not None else o.reverse
self._attrs = o._attrs.copy()
result.append(o)
else:
result.append(Edge(o, forward=forward, reverse=reverse, **self._attrs))
return result
def connect(self, other: Union["Node", "Edge", List["Node"]]):
if isinstance(other, list):
for node in other:
self.node.connect(node, self)
return other
elif isinstance(other, Edge):
self._attrs = other._attrs.copy()
return self
else:
if self.node is not None:
return self.node.connect(other, self)
else:
self.node = other
return self
@property
def attrs(self) -> Dict:
if self.forward and self.reverse:
direction = 'both'
elif self.forward:
direction = 'forward'
elif self.reverse:
direction = 'back'
else:
direction = 'none'
return {**self._attrs, 'dir': direction}
Group = Cluster Group = Cluster

@ -213,6 +213,49 @@ with Diagram("Advanced Web Service with On-Premise", show=False):
![advanced web service with on-premise diagram](/img/advanced_web_service_with_on-premise.png) ![advanced web service with on-premise diagram](/img/advanced_web_service_with_on-premise.png)
## Advanced Web Service with On-Premise (with colors and labels)
```python
from diagrams import Cluster, Diagram, Edge
from diagrams.onprem.analytics import Spark
from diagrams.onprem.compute import Server
from diagrams.onprem.database import PostgreSQL
from diagrams.onprem.inmemory import Redis
from diagrams.onprem.logging import Fluentd
from diagrams.onprem.monitoring import Grafana, Prometheus
from diagrams.onprem.network import Nginx
from diagrams.onprem.queue import Kafka
with Diagram(name="Advanced Web Service with On-Premise (colored)", show=False):
ingress = Nginx("ingress")
metrics = Prometheus("metric")
metrics << Edge(color="firebrick", style="dashed") << Grafana("monitoring")
with Cluster("Service Cluster"):
grpcsvc = [
Server("grpc1"),
Server("grpc2"),
Server("grpc3")]
with Cluster("Sessions HA"):
master = Redis("session")
master - Edge(color="brown", style="dashed") - Redis("replica") << Edge(label="collect") << metrics
grpcsvc >> Edge(color="brown") >> master
with Cluster("Database HA"):
master = PostgreSQL("users")
master - Edge(color="brown", style="dotted") - PostgreSQL("slave") << Edge(label="collect") << metrics
grpcsvc >> Edge(color="black") >> master
aggregator = Fluentd("logging")
aggregator >> Edge(label="parse") >> Kafka("stream") >> Edge(color="black", style="bold") >> Spark("analytics")
ingress >> Edge(color="darkgreen") << grpcsvc >> Edge(color="darkorange") >> aggregator
```
![advanced web service with on-premise diagram colored](/img/advanced_web_service_with_on-premise_colored.png)
## RabbitMQ Consumers with Custom Nodes ## RabbitMQ Consumers with Custom Nodes
```python ```python
@ -240,6 +283,6 @@ with Diagram("Broker Consumers", show=False):
queue = Custom("Message queue", rabbitmq_icon) queue = Custom("Message queue", rabbitmq_icon)
queue >> consumers >> Aurora("Database") queue >> consumers >> Aurora("Database")
```` ```
![rabbitmq consumers diagram](/img/rabbitmq_consumers_diagram.png) ![rabbitmq consumers diagram](/img/rabbitmq_consumers_diagram.png)

@ -0,0 +1,53 @@
---
id: edge
title: Edges
---
Edge is representing an edge between Nodes.
## Basic
Edge is an object representing a connection between Nodes with some additional properties.
An edge object contains three attributes: **label**, **color** and **style** which mirror corresponding graphviz edge attributes.
```python
from diagrams import Cluster, Diagram, Edge
from diagrams.onprem.analytics import Spark
from diagrams.onprem.compute import Server
from diagrams.onprem.database import PostgreSQL
from diagrams.onprem.inmemory import Redis
from diagrams.onprem.logging import Fluentd
from diagrams.onprem.monitoring import Grafana, Prometheus
from diagrams.onprem.network import Nginx
from diagrams.onprem.queue import Kafka
with Diagram(name="Advanced Web Service with On-Premise (colored)", show=False):
ingress = Nginx("ingress")
metrics = Prometheus("metric")
metrics << Edge(color="firebrick", style="dashed") << Grafana("monitoring")
with Cluster("Service Cluster"):
grpcsvc = [
Server("grpc1"),
Server("grpc2"),
Server("grpc3")]
with Cluster("Sessions HA"):
master = Redis("session")
master - Edge(color="brown", style="dashed") - Redis("replica") << Edge(label="collect") << metrics
grpcsvc >> Edge(color="brown") >> master
with Cluster("Database HA"):
master = PostgreSQL("users")
master - Edge(color="brown", style="dotted") - PostgreSQL("slave") << Edge(label="collect") << metrics
grpcsvc >> Edge(color="black") >> master
aggregator = Fluentd("logging")
aggregator >> Edge(label="parse") >> Kafka("stream") >> Edge(color="black", style="bold") >> Spark("analytics")
ingress >> Edge(color="darkgreen") << grpcsvc >> Edge(color="darkorange") >> aggregator
```
![advanced web service with on-premise diagram colored](/img/advanced_web_service_with_on-premise_colored.png)

@ -1,18 +1,23 @@
import os import os
import shutil
import unittest import unittest
from diagrams import Cluster, Diagram, Node from diagrams import Cluster, Diagram, Node, Edge
from diagrams import getcluster, getdiagram, setcluster, setdiagram from diagrams import getcluster, getdiagram, setcluster, setdiagram
class DiagramTest(unittest.TestCase): class DiagramTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.name = "test" self.name = "diagram_test"
def tearDown(self): def tearDown(self):
setdiagram(None) setdiagram(None)
setcluster(None) setcluster(None)
# Only some tests generate the image file. # Only some tests generate the image file.
try:
shutil.rmtree(self.name)
except OSError:
# Consider it file
try: try:
os.remove(self.name + ".png") os.remove(self.name + ".png")
except FileNotFoundError: except FileNotFoundError:
@ -40,7 +45,7 @@ class DiagramTest(unittest.TestCase):
def test_with_global_context(self): def test_with_global_context(self):
self.assertIsNone(getdiagram()) self.assertIsNone(getdiagram())
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'with_global_context'), show=False):
self.assertIsNotNone(getdiagram()) self.assertIsNotNone(getdiagram())
self.assertIsNone(getdiagram()) self.assertIsNone(getdiagram())
@ -50,7 +55,7 @@ class DiagramTest(unittest.TestCase):
Node("node") Node("node")
def test_node_to_node(self): def test_node_to_node(self):
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'node_to_node'), show=False):
node1 = Node("node1") node1 = Node("node1")
node2 = Node("node2") node2 = Node("node2")
self.assertEqual(node1 - node2, node2) self.assertEqual(node1 - node2, node2)
@ -58,7 +63,7 @@ class DiagramTest(unittest.TestCase):
self.assertEqual(node1 << node2, node2) self.assertEqual(node1 << node2, node2)
def test_node_to_nodes(self): def test_node_to_nodes(self):
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'node_to_nodes'), show=False):
node1 = Node("node1") node1 = Node("node1")
nodes = [Node("node2"), Node("node3")] nodes = [Node("node2"), Node("node3")]
self.assertEqual(node1 - nodes, nodes) self.assertEqual(node1 - nodes, nodes)
@ -66,7 +71,7 @@ class DiagramTest(unittest.TestCase):
self.assertEqual(node1 << nodes, nodes) self.assertEqual(node1 << nodes, nodes)
def test_nodes_to_node(self): def test_nodes_to_node(self):
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'nodes_to_node'), show=False):
node1 = Node("node1") node1 = Node("node1")
nodes = [Node("node2"), Node("node3")] nodes = [Node("node2"), Node("node3")]
self.assertEqual(nodes - node1, node1) self.assertEqual(nodes - node1, node1)
@ -88,38 +93,38 @@ class DiagramTest(unittest.TestCase):
class ClusterTest(unittest.TestCase): class ClusterTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.name = "test" self.name = "cluster_test"
def tearDown(self): def tearDown(self):
setdiagram(None) setdiagram(None)
setcluster(None) setcluster(None)
# Only some tests generate the image file. # Only some tests generate the image file.
try: try:
os.remove(self.name + ".png") shutil.rmtree(self.name)
except FileNotFoundError: except OSError:
pass pass
def test_validate_direction(self): def test_validate_direction(self):
# Normal directions. # Normal directions.
for dir in ("TB", "BT", "LR", "RL"): for dir in ("TB", "BT", "LR", "RL"):
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'validate_direction'), show=False):
Cluster(direction=dir) Cluster(direction=dir)
# Invalid directions. # Invalid directions.
for dir in ("BR", "TL", "Unknown"): for dir in ("BR", "TL", "Unknown"):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'validate_direction'), show=False):
Cluster(direction=dir) Cluster(direction=dir)
def test_with_global_context(self): def test_with_global_context(self):
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'with_global_context'), show=False):
self.assertIsNone(getcluster()) self.assertIsNone(getcluster())
with Cluster(): with Cluster():
self.assertIsNotNone(getcluster()) self.assertIsNotNone(getcluster())
self.assertIsNone(getcluster()) self.assertIsNone(getcluster())
def test_with_nested_cluster(self): def test_with_nested_cluster(self):
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'with_nested_cluster'), show=False):
self.assertIsNone(getcluster()) self.assertIsNone(getcluster())
with Cluster() as c1: with Cluster() as c1:
self.assertEqual(c1, getcluster()) self.assertEqual(c1, getcluster())
@ -134,7 +139,7 @@ class ClusterTest(unittest.TestCase):
Node("node") Node("node")
def test_node_to_node(self): def test_node_to_node(self):
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'node_to_node'), show=False):
with Cluster(): with Cluster():
node1 = Node("node1") node1 = Node("node1")
node2 = Node("node2") node2 = Node("node2")
@ -143,7 +148,7 @@ class ClusterTest(unittest.TestCase):
self.assertEqual(node1 << node2, node2) self.assertEqual(node1 << node2, node2)
def test_node_to_nodes(self): def test_node_to_nodes(self):
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'node_to_nodes'), show=False):
with Cluster(): with Cluster():
node1 = Node("node1") node1 = Node("node1")
nodes = [Node("node2"), Node("node3")] nodes = [Node("node2"), Node("node3")]
@ -152,10 +157,111 @@ class ClusterTest(unittest.TestCase):
self.assertEqual(node1 << nodes, nodes) self.assertEqual(node1 << nodes, nodes)
def test_nodes_to_node(self): def test_nodes_to_node(self):
with Diagram(name=self.name, show=False): with Diagram(name=os.path.join(self.name, 'nodes_to_node'), show=False):
with Cluster(): with Cluster():
node1 = Node("node1") node1 = Node("node1")
nodes = [Node("node2"), Node("node3")] nodes = [Node("node2"), Node("node3")]
self.assertEqual(nodes - node1, node1) self.assertEqual(nodes - node1, node1)
self.assertEqual(nodes >> node1, node1) self.assertEqual(nodes >> node1, node1)
self.assertEqual(nodes << node1, node1) self.assertEqual(nodes << node1, node1)
class EdgeTest(unittest.TestCase):
def setUp(self):
self.name = "edge_test"
def tearDown(self):
setdiagram(None)
setcluster(None)
# Only some tests generate the image file.
try:
shutil.rmtree(self.name)
except OSError:
pass
def test_node_to_node(self):
with Diagram(name=os.path.join(self.name, 'node_to_node'), show=False):
node1 = Node("node1")
node2 = Node("node2")
self.assertEqual(node1 - Edge(color='red') - node2, node2)
def test_node_to_nodes(self):
with Diagram(name=os.path.join(self.name, 'node_to_nodes'), show=False):
with Cluster():
node1 = Node("node1")
nodes = [Node("node2"), Node("node3")]
self.assertEqual(node1 - Edge(color='red') - nodes, nodes)
def test_nodes_to_node(self):
with Diagram(name=os.path.join(self.name, 'nodes_to_node'), show=False):
with Cluster():
node1 = Node("node1")
nodes = [Node("node2"), Node("node3")]
self.assertEqual(nodes - Edge(color='red') - node1, node1)
def test_nodes_to_node_with_additional_attributes(self):
with Diagram(name=os.path.join(self.name, 'nodes_to_node_with_additional_attributes'), show=False):
with Cluster():
node1 = Node("node1")
nodes = [Node("node2"), Node("node3")]
self.assertEqual(nodes - Edge(color='red') - Edge(color='green') - node1, node1)
def test_node_to_node_with_attributes(self):
with Diagram(name=os.path.join(self.name, 'node_to_node_with_attributes'), show=False):
with Cluster():
node1 = Node("node1")
node2 = Node("node2")
self.assertEqual(node1 << Edge(color='red', label='1.1') << node2, node2)
self.assertEqual(node1 >> Edge(color='green', label='1.2') >> node2, node2)
self.assertEqual(node1 << Edge(color='blue', label='1.3') >> node2, node2)
def test_node_to_node_with_additional_attributes(self):
with Diagram(name=os.path.join(self.name, 'node_to_node_with_additional_attributes'), show=False):
with Cluster():
node1 = Node("node1")
node2 = Node("node2")
self.assertEqual(node1 << Edge(color='red', label='2.1') << Edge(color='blue') << node2, node2)
self.assertEqual(node1 >> Edge(color='green', label='2.2') >> Edge(color='red') >> node2, node2)
self.assertEqual(node1 << Edge(color='blue', label='2.3') >> Edge(color='black') >> node2, node2)
def test_nodes_to_node_with_attributes_loop(self):
with Diagram(name=os.path.join(self.name, 'nodes_to_node_with_attributes_loop'), show=False):
with Cluster():
node = Node("node")
self.assertEqual(node >> Edge(color='red', label='3.1') >> node, node)
self.assertEqual(node << Edge(color='green', label='3.2') << node, node)
self.assertEqual(node >> Edge(color='blue', label='3.3') << node, node)
self.assertEqual(node << Edge(color='pink', label='3.4') >> node, node)
def test_nodes_to_node_with_attributes_bothdirectional(self):
with Diagram(name=os.path.join(self.name, 'nodes_to_node_with_attributes_bothdirectional'), show=False) as diagram:
with Cluster():
node1 = Node("node1")
nodes = [Node("node2"), Node("node3")]
self.assertEqual(nodes << Edge(color='green', label='4') >> node1, node1)
def test_nodes_to_node_with_attributes_bidirectional(self):
with Diagram(name=os.path.join(self.name, 'nodes_to_node_with_attributes_bidirectional'), show=False):
with Cluster():
node1 = Node("node1")
nodes = [Node("node2"), Node("node3")]
self.assertEqual(nodes << Edge(color='blue', label='5') >> node1, node1)
def test_nodes_to_node_with_attributes_onedirectional(self):
with Diagram(name=os.path.join(self.name, 'nodes_to_node_with_attributes_onedirectional'), show=False):
with Cluster():
node1 = Node("node1")
nodes = [Node("node2"), Node("node3")]
self.assertEqual(nodes >> Edge(color='red', label='6.1') >> node1, node1)
self.assertEqual(nodes << Edge(color='green', label='6.2') << node1, node1)
def test_nodes_to_node_with_additional_attributes_directional(self):
with Diagram(name=os.path.join(self.name, 'nodes_to_node_with_additional_attributes_directional'), show=False):
with Cluster():
node1 = Node("node1")
nodes = [Node("node2"), Node("node3")]
self.assertEqual(nodes
>> Edge(color='red', label='6.1') >> Edge(color='blue', label='6.2') >> node1, node1)
self.assertEqual(nodes
<< Edge(color='green', label='6.3') << Edge(color='pink', label='6.4') << node1, node1)

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Loading…
Cancel
Save