Skip to content

Commit 995d8b7

Browse files
BishalsarangBishal Sarangkoti
authored and
Bishal Sarangkoti
committed
feat-custom-graph: Add custom graph class for graph operations
1 parent b7da9da commit 995d8b7

File tree

6 files changed

+169
-10
lines changed

6 files changed

+169
-10
lines changed

tests/test_graph.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from visualiser import Node, Edge, Graph
2+
3+
4+
class TestGraph:
5+
def test_normal_graph_flow(self):
6+
graph = Graph('hello')
7+
8+
# Create Node
9+
A = Node('A', color='red')
10+
B = Node('B', color='green')
11+
C = Node('C', color='yellow')
12+
13+
# Add node
14+
graph.add_node(A)
15+
graph.add_node(B)
16+
graph.add_node(C)
17+
18+
# Make edge
19+
edge1 = Edge(A, B)
20+
edge2 = Edge(A, C)
21+
22+
# Add node and graph to edge
23+
graph.add_edge(edge1)
24+
graph.add_edge(edge2)
25+
26+
assert graph.to_string() == 'digraph G {\nA [label="A", color="red"];\nB [label="B", color="green"];\nC [label="C", color="yellow"];\nA -> B [];\nA -> C [];\n}'
27+
28+
def test_node_methods(self):
29+
pass
30+
31+
def test_edge_methods(self):
32+
pass
33+
34+
def test_mutations(self):
35+
pass

tests/test_node.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"""
44
Test case class for nodes
55
"""
6+
7+
68
class TestNode:
79
def test_create_new_node(self):
810
node = Node('bishal', 'Bishal label', color='red', style='filled')
@@ -36,5 +38,3 @@ def test_rename_name_label(self):
3638
assert node.name == 'Bishal renamed'
3739

3840
assert node.to_string() == 'Bishal renamed [label="Bishal renamed label", color="green", style="filled"];'
39-
40-

visualiser/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
# because we are using Node inside Edge for type annotations.
66
from .node import Node
77
from .edge import Edge
8+
from .graph import Graph
89
from visualiser import *

visualiser/edge.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import copy
2+
from typing import Union
3+
4+
from visualiser import Node
25

36

4-
from visualiser import Node
57
class Edge:
68

7-
def __init__(self, source_node: Node, destination_node: Node, label: str = '',
9+
def __init__(self, source_node: Union[Node, str], destination_node: Union[Node, str], label: str = '',
810
**attrs: str) -> None:
9-
# TODO: Remove copy after finding a better way to do this.
10-
self._source_node = copy.deepcopy(source_node)
11-
self._destination_node = copy.deepcopy(destination_node)
11+
self._source_node = Node(source_node) if isinstance(source_node, str) else copy.deepcopy(source_node)
12+
self._destination_node = Node(destination_node) if isinstance(destination_node, str) else copy.deepcopy(
13+
destination_node)
1214

15+
self._name = f"{self._source_node.name} -> {self._destination_node.name}"
1316
self._label = label
1417
self._attrs = attrs
1518

@@ -29,6 +32,22 @@ def label(self, _label: str) -> None:
2932
"""
3033
self._label = _label
3134

35+
@property
36+
def name(self) -> str:
37+
"""
38+
Get name for edge.
39+
:return: str
40+
"""
41+
return self._name
42+
43+
@name.setter
44+
def name(self, _name: str) -> None:
45+
"""
46+
Set label for edge.
47+
:param _name: str
48+
"""
49+
self._name = _name
50+
3251
@property
3352
def source_node(self) -> Node:
3453
"""
@@ -82,18 +101,19 @@ def remove_attribute(self, key: str) -> None:
82101
Remove attribute from edge.
83102
:param key: str
84103
"""
85-
del self._attrs[key]
104+
if self._attrs.get(key):
105+
del self._attrs[key]
86106

87107
def get_attributes_string(self) -> str:
88108
"""
89109
Get attributes string enclosed in []
90110
:return:
91111
"""
92112
if len(self._label) == 0:
93-
return '[' + ', '.join([f'{key}="{value}"' for key, value in self._attrs.items()]) + ']'
113+
return '[' + ', '.join([f'{key}="{value}"' for key, value in self._attrs.items()]) + '];'
94114

95115
return '[' + f'label="{self._label}", ' + ', '.join(
96-
[f'{key}="{value}"' for key, value in self._attrs.items()]) + ']'
116+
[f'{key}="{value}"' for key, value in self._attrs.items()]) + '];'
97117

98118
def to_string(self) -> str:
99119
"""

visualiser/graph.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import copy
2+
3+
from visualiser import Node, Edge
4+
5+
6+
class Graph:
7+
def __init__(self, name='', **attrs):
8+
self._name = name
9+
self._attrs = attrs
10+
11+
self._nodes = []
12+
self._edges = []
13+
14+
def get_node(self, name):
15+
filtered_nodes = list(filter(lambda node: node.name == name, self._nodes))
16+
17+
return filtered_nodes[0] if len(filtered_nodes) else None
18+
19+
def remove_node(self, _node):
20+
if isinstance(_node, Node):
21+
self._nodes = list(filter(lambda node: node.name != _node.name, self._nodes))
22+
return
23+
24+
self._nodes = list(filter(lambda node: node.name != _node, self._nodes))
25+
26+
def add_node(self, node):
27+
if self.get_node(node.name) is not None:
28+
self.remove_node(node)
29+
30+
self._nodes.append(copy.deepcopy(node))
31+
32+
def set_node_attributes(self, node, **attrs):
33+
for key, value in attrs.items():
34+
self.set_node_attribute(node, key, value)
35+
36+
def set_node_attribute(self, _node, key, value):
37+
def _set_attribute(name):
38+
node = self.get_node(name)
39+
if node:
40+
node.set_attribute(key, value)
41+
42+
if isinstance(_node, Node):
43+
_set_attribute(_node.name)
44+
return
45+
46+
_set_attribute(_node)
47+
48+
def remove_node_attribute(self, _node, key):
49+
def _remove_attribute(name):
50+
node = self.get_node(name)
51+
if node:
52+
node.remove_attribute(key)
53+
54+
if isinstance(_node, Node):
55+
_remove_attribute(_node.name)
56+
return
57+
58+
_remove_attribute(_node)
59+
60+
def add_edge(self, edge):
61+
if self.get_edge(edge.name) is not None:
62+
self.remove_edge(edge)
63+
64+
# TODO: Check if node used in node exist in graph. If not create one
65+
self._edges.append(copy.deepcopy(edge))
66+
67+
def get_edge(self, name):
68+
filtered_edges = list(filter(lambda edge: edge.name == name, self._edges))
69+
70+
return filtered_edges[0] if len(filtered_edges) else None
71+
72+
def remove_edge(self, _edge):
73+
if isinstance(_edge, Edge):
74+
self._edges = list(filter(lambda edge: edge.name != _edge.name, self._edges))
75+
return
76+
77+
self._edges = list(filter(lambda edge: edge.name != _edge, self._edges))
78+
79+
def set_edge_label(self, name, value):
80+
edge = self.get_edge(name)
81+
if edge:
82+
edge.label = value
83+
84+
def highlight_node(self, name, color):
85+
self.set_node_attribute(name, 'color', color)
86+
87+
def highlight_edge(self, name, color):
88+
edge = self.get_edge(name)
89+
if edge:
90+
edge.set_attribute('color', color)
91+
92+
def reverse_edge_orientation(self, name):
93+
pass
94+
95+
def get_nodes_string(self):
96+
return "\n".join(list(map(lambda node: node.to_string(), self._nodes)))
97+
98+
def get_edges_string(self):
99+
return "\n".join(list(map(lambda edge: edge.to_string(), self._edges)))
100+
101+
def to_string(self):
102+
return "digraph G {\n" + f"{self.get_nodes_string()}\n" + f"{self.get_edges_string()}\n" + "}"

visualiser/node.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def remove_attribute(self, key):
4242
del self._attrs[key]
4343

4444
def get_attributes_string(self):
45+
# TODO: Check if no any attributes and labels to avoid '[]'
4546
return '[' + f'label="{self._label}", ' + ', '.join(
4647
[f'{key}="{value}"' for key, value in self._attrs.items()]) + '];'
4748

0 commit comments

Comments
 (0)