Skip to content

Code refactor: Refactor code and create custom class for node, edge, graph, animation etc, #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
test*
.idea
.idea
img
out*
*frames*

packaging.txt

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down Expand Up @@ -632,6 +630,3 @@ MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/

!/packaging.txt
!/packaging.txt
5 changes: 0 additions & 5 deletions packaging.txt

This file was deleted.

19 changes: 19 additions & 0 deletions tests/test_edge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from visualiser import Edge, Node

"""
Test case class for nodes
"""


class TestEdge:
def test_create_new_edge(self):
# Create Node
A = Node('A', color='red', style='filled')
B = Node('B', label='B label', color='red', style='filled')

# Create Edge
edge = Edge(A, B, label='Test Label')

Comment on lines +9 to +16

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider adding more assertions to the test case to ensure that all properties of the Edge class are functioning as expected.

Suggested change
def test_create_new_edge(self):
# Create Node
A = Node('A', color='red', style='filled')
B = Node('B', label='B label', color='red', style='filled')
# Create Edge
edge = Edge(A, B, label='Test Label')
def test_create_new_edge(self):
# Create Node
A = Node('A', color='red', style='filled')
B = Node('B', label='B label', color='red', style='filled')
# Create Edge
edge = Edge(A, B, label='Test Label')
assert edge.label == 'Test Label'
assert edge.source_node.label == 'A'
assert edge.source_node == A
assert edge.destination_node.label == 'B label'
assert edge.destination_node == B

assert edge.label == 'Test Label'
assert edge.source_node.label == 'A'
assert edge.source_node == A
35 changes: 35 additions & 0 deletions tests/test_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from visualiser import Node, Edge, Graph


class TestGraph:
def test_normal_graph_flow(self):
graph = Graph('hello')

# Create Node
A = Node('A', color='red')
B = Node('B', color='green')
C = Node('C', color='yellow')

# Add node
graph.add_node(A)
graph.add_node(B)
graph.add_node(C)

# Make edge
edge1 = Edge(A, B)
edge2 = Edge(A, C)

# Add node and graph to edge
graph.add_edge(edge1)
graph.add_edge(edge2)

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}'

def test_node_methods(self):
pass

def test_edge_methods(self):
pass

def test_mutations(self):
pass
Comment on lines +28 to +35

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider adding test cases for the test_node_methods, test_edge_methods, and test_mutations methods. Currently, these methods are empty and do not test anything.

Suggested change
def test_node_methods(self):
pass
def test_edge_methods(self):
pass
def test_mutations(self):
pass
def test_node_methods(self):
# Add test cases for node methods
def test_edge_methods(self):
# Add test cases for edge methods
def test_mutations(self):
# Add test cases for mutations

40 changes: 40 additions & 0 deletions tests/test_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from visualiser import *

"""
Test case class for nodes
"""


class TestNode:
def test_create_new_node(self):
node = Node('bishal', 'Bishal label', color='red', style='filled')

assert node.label == 'Bishal label'
assert node.name == 'bishal'
assert node.get_attribute('color') == 'red'
assert node.get_attribute('style') == 'filled'
assert node.to_string() == 'bishal [label="Bishal label", color="red", style="filled"];'

def test_set_attributes(self):
node = Node('bishal', 'Bishal label', color='red', style='filled')
assert node.get_attribute('color') == 'red'
node.set_attribute('color', 'green')
assert node.get_attribute('color') == 'green'
node.set_attribute('background', 'grey')
assert node.get_attribute('background') == 'grey'
assert node.to_string() == 'bishal [label="Bishal label", color="green", style="filled", background="grey"];'

def test_rename_name_label(self):
node = Node('bishal', 'Bishal label', color='red', style='filled')

node.set_attribute('color', 'green')

assert node.label == 'Bishal label'
node.label = 'Bishal renamed label'
assert node.label == 'Bishal renamed label'

assert node.name == 'bishal'
node.name = 'Bishal renamed'
assert node.name == 'Bishal renamed'

assert node.to_string() == 'Bishal renamed [label="Bishal renamed label", color="green", style="filled"];'
12 changes: 10 additions & 2 deletions visualiser/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@

# Version of the recursion-visualiser
__version__ = "1.0.1"
__version__ = "1.0.1"

# Maintain this order to avoid circular import
# because we are using Node inside Edge for type annotations.
from .node import Node
from .edge import Edge
from .graph import Graph
from .animation import Animation

from visualiser import *
68 changes: 68 additions & 0 deletions visualiser/animation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import copy

import pydot
import glob
import os
import shutil
import imageio

from visualiser import Node, Edge, Graph


class Animation:
def __init__(self):
self.frames = []
self._frame_id = 0

def next_step(self, frame):
self.frames.append(copy.deepcopy(frame))
self._frame_id += 1

@staticmethod
def make_directory():
if not os.path.exists("frames"):
os.makedirs("frames")

def next_frame(self):
pass

def previous_frame(self):
pass

def get_frame(self, frame_id):
return self.frames[frame_id]
Comment on lines +32 to +33

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider adding error handling for when the frame_id is not of type int. This will prevent the program from crashing when an invalid type is passed.

Suggested change
def get_frame(self, frame_id):
return self.frames[frame_id]
def get_frame(self, frame_id):
if not isinstance(frame_id, int):
raise TypeError('frame_id must be of type int')
return self.frames[frame_id]


def get_frames(self):
return self.frames[::]

def write_frame(self, frame_id):
frame = self.get_frame(frame_id)

dot_graph, *rest = pydot.graph_from_dot_data(frame)
dot_graph.write_png(f"frames/temp_{frame_id}.png")

def write_file(self):
self.make_directory()

for frame_id in range(len(self.get_frames())):
self.write_frame(frame_id)

def write_gif(self, name="out.gif", delay=3):
self.write_file()

images = []

# sort frames images in ascending order to number in image filename
# image filename: frames/temp_1.png
sorted_images = sorted(
glob.glob("frames/*.png"),
key=lambda fn: int(fn.split("_")[1].split(".")[0])
)

for filename in sorted_images:
images.append(imageio.imread(filename))

imageio.mimsave(name, images, duration=delay)
# Delete temporary directory
shutil.rmtree("frames")

123 changes: 123 additions & 0 deletions visualiser/edge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import copy
from typing import Union

from visualiser import Node


class Edge:

def __init__(self, source_node: Union[Node, str], destination_node: Union[Node, str], label: str = '',
**attrs: str) -> None:
self._source_node = Node(source_node) if isinstance(source_node, str) else copy.deepcopy(source_node)
self._destination_node = Node(destination_node) if isinstance(destination_node, str) else copy.deepcopy(
destination_node)
Comment on lines +9 to +13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider adding error handling for when the source_node or destination_node is not of type Node or str. This will prevent the program from crashing when an invalid type is passed.

Suggested change
def __init__(self, source_node: Union[Node, str], destination_node: Union[Node, str], label: str = '',
**attrs: str) -> None:
self._source_node = Node(source_node) if isinstance(source_node, str) else copy.deepcopy(source_node)
self._destination_node = Node(destination_node) if isinstance(destination_node, str) else copy.deepcopy(
destination_node)
def __init__(self, source_node: Union[Node, str], destination_node: Union[Node, str], label: str = '',
**attrs: str) -> None:
if not isinstance(source_node, (Node, str)) or not isinstance(destination_node, (Node, str)):
raise TypeError('source_node and destination_node must be of type Node or str')
self._source_node = Node(source_node) if isinstance(source_node, str) else copy.deepcopy(source_node)
self._destination_node = Node(destination_node) if isinstance(destination_node, str) else copy.deepcopy(
destination_node)


self._name = f"{self._source_node.name} -> {self._destination_node.name}"
self._label = label
self._attrs = attrs

@property
def label(self) -> str:
"""
Get label for edge.
:return: str
"""
return self._label

@label.setter
def label(self, _label: str) -> None:
"""
Set label for edge.
:param _label: str
"""
self._label = _label

@property
def name(self) -> str:
"""
Get name for edge.
:return: str
"""
return self._name

@name.setter
def name(self, _name: str) -> None:
"""
Set label for edge.
:param _name: str
"""
self._name = _name

@property
def source_node(self) -> Node:
"""
Get source node
:return: Node
"""
return self._source_node

@source_node.setter
def source_node(self, _source_node: Node) -> None:
"""
Set source node.
:param _source_node: Node
"""
self._source_node = _source_node

@property
def destination_node(self) -> Node:
"""
Get destination node.
:return: Node
"""
return self._destination_node

@destination_node.setter
def destination_node(self, _destination_node: Node) -> None:
"""
Sets destination node.
:param _destination_node: Node
"""
self._destination_node = _destination_node

def get_attribute(self, key: str) -> str:
"""
Get attribute for edge.
:param key: str
:return: The value of attribute with key
"""
return self._attrs.get(key)

def set_attribute(self, key: str, value: str) -> None:
"""
Set attribute for edge
:param key: str
:param value: str
"""
self._attrs[key] = value

def remove_attribute(self, key: str) -> None:
"""
Remove attribute from edge.
:param key: str
"""
if self._attrs.get(key):
del self._attrs[key]

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

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

def to_string(self) -> str:
"""
Converts dot string equivalent of the current edge.
:return: Str
"""
return f'{self._source_node.name} -> {self._destination_node.name} {self.get_attributes_string()}'
Loading