diff --git a/README.md b/README.md index 480a7b5..c15cb1e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ So far, the seealgo library provides visualization for the following data struct - List: `append(value)`, `insert(index, value)`, `remove(value)`, `__setitem__(index, value)` - Binary Search Tree (provided as a nested dictionary): `insert(child)`, `remove(value)` - Set: `add(value)`, `remove(value)`, `clear()`, `update(values)` +- Dictionary (nested and non-nested): `update(iterable)`, `pop(key)` ## Installation This library requires you to have `graphviz` installed on your system using the [instructions](https://graphviz.org/download/) appropriate for your system, or by using the following command if you are on a macOS device: @@ -27,6 +28,8 @@ pip install seealgo ``` ## Using seealgo +(for examples of all data structures available with seealgo, visit our [GitHub Pages](https://sarahtang7.github.io/seealgo/)!) + This is an example of using the List module to visualize appending 5 to a list: ```python diff --git a/outputFiles/initDictOutput.txt b/outputFiles/initDictOutput.txt new file mode 100644 index 0000000..f1e1e94 --- /dev/null +++ b/outputFiles/initDictOutput.txt @@ -0,0 +1,12 @@ +digraph Dict { + rankdir=LR + subgraph cluster_Dict { + node [color=white style=filled] + label=Dictionary + style=filled + color=lightgrey + key1 [label="key1: one"] + key2 [label="key2: 2"] + key3 [label="key3: three"] + } +} diff --git a/outputFiles/nestedDictOutput.txt b/outputFiles/nestedDictOutput.txt new file mode 100644 index 0000000..a437544 --- /dev/null +++ b/outputFiles/nestedDictOutput.txt @@ -0,0 +1,15 @@ +digraph Dict { + rankdir=LR + subgraph cluster_Dict { + node [color=white style=filled] + label=Dictionary + style=filled + color=lightgrey + key1 [label="key1: value1"] + key2 [label=key2 color=lightblue2 shape=rectangle style=filled] + nested_key1 [label="nested_key1: nested_value1"] + key2 -> nested_key1 + nested_key2 [label="nested_key2: nested_value2"] + key2 -> nested_key2 + } +} diff --git a/outputFiles/popDictOutput1.txt b/outputFiles/popDictOutput1.txt new file mode 100644 index 0000000..2d6ddad --- /dev/null +++ b/outputFiles/popDictOutput1.txt @@ -0,0 +1,13 @@ +digraph Dict { + rankdir=LR + subgraph cluster_Dict { + node [color=white style=filled] + label=Dictionary + style=filled + color=lightgrey + apple [label="apple: red"] + banana [label="banana: yellow"] + goldfish [label="goldfish: gold"] + kiwi [label="kiwi: green"] + } +} diff --git a/outputFiles/popDictOutput2.txt b/outputFiles/popDictOutput2.txt new file mode 100644 index 0000000..ca9952e --- /dev/null +++ b/outputFiles/popDictOutput2.txt @@ -0,0 +1,12 @@ +digraph Dict { + rankdir=LR + subgraph cluster_Dict { + node [color=white style=filled] + label=Dictionary + style=filled + color=lightgrey + apple [label="apple: red"] + banana [label="banana: yellow"] + kiwi [label="kiwi: green"] + } +} diff --git a/outputFiles/updateDictOutput1.txt b/outputFiles/updateDictOutput1.txt new file mode 100644 index 0000000..d0cef4d --- /dev/null +++ b/outputFiles/updateDictOutput1.txt @@ -0,0 +1,13 @@ +digraph Dict { + rankdir=LR + subgraph cluster_Dict { + node [color=white style=filled] + label=Dictionary + style=filled + color=lightgrey + 1 [label="1: 1"] + 2 [label="2: 4"] + 3 [label="3: 9"] + 4 [label="4: 16"] + } +} diff --git a/outputFiles/updateDictOutput2.txt b/outputFiles/updateDictOutput2.txt new file mode 100644 index 0000000..7fde866 --- /dev/null +++ b/outputFiles/updateDictOutput2.txt @@ -0,0 +1,16 @@ +digraph Dict { + rankdir=LR + subgraph cluster_Dict { + node [color=white style=filled] + label=Dictionary + style=filled + color=lightgrey + 1 [label="1: 1"] + 2 [label="2: 4"] + 3 [label="3: 9"] + 4 [label="4: 16"] + 5 [label="5: 25" color=green style=filled] + 6 [label="6: 36" color=green style=filled] + 7 [label="7: 49" color=green style=filled] + } +} diff --git a/seealgo/__init__.py b/seealgo/__init__.py index 9b33b57..3af4fd1 100644 --- a/seealgo/__init__.py +++ b/seealgo/__init__.py @@ -4,12 +4,14 @@ from seealgo.see_tree_algo import Tree from seealgo.see_set_algo import Set +from seealgo.see_dict_algo import Dict from .see_list_algo import List myList = List() myTree = Tree() mySet = Set() +myDict = Dict() __all__ = [ - 'List', 'Tree', 'Set' + 'List', 'Tree', 'Set', 'Dict' ] diff --git a/seealgo/see_dict_algo.py b/seealgo/see_dict_algo.py new file mode 100644 index 0000000..78ba568 --- /dev/null +++ b/seealgo/see_dict_algo.py @@ -0,0 +1,121 @@ +""" + +This module contains the functionality to visualize a dictionary +data structure as it changes throughout a function. + +This involves the following classes: + +* `TrackedDict(dict)`: detects changes made to a given dictionary + data structure and triggers creation of a new visualization for each change. + +* `Dict`: uses graphviz to construct a visualization of the dictionary using a table. + +""" + +from graphviz import Digraph + +class TrackedDict(dict): + """ + Tracks changes to a dictionary data structure and triggers creation of new visualization + + Args: + dict: the dictionary data structure to track + """ + + def update(self, iterable): + """ + Sets the value of a key-value pair and checks the keys of the dictionary + + Args: + key (Any): key of the key-value pair to be set + value (Any): value to be set for the specified key + """ + super().update(iterable) + keys = list(iterable.keys()) + Dict.create_viz(self, self, keys) + + + def pop(self, key): + """ + Removes a key-value pair given the key. + + Args: + key (Any): key of the key-value pair to be removed + + Raises: + KeyError: If the element to be removed is not in the dictionary. + """ + super().pop(key) + Dict.create_viz(self, self) + + +class Dict: + """ + Create graphviz visualization for dictionary data structure + """ + + filenum = 1 + + def see(self, func, data): + """ + Creates a visualization for the initial dictionary and starts tracking + a given dictionary as it changes throughout a given function. + + Args: + func (function): function that the dictionary is being altered through + data (dict): dictionary to track + """ + Dict.create_viz(self, data) + data = TrackedDict(data) + if func is not None: + func(data) + + + def create_viz(self, data, keys=None): + """ + Creates and renders a visualization of the dictionary using graphviz + + Args: + data (dict): dictionary that is being visualized + key (iterable): optional list of keys of new key-value pairs + """ + + if keys is None: + keys = {} + + di_graph = Digraph('Dict', filename=f'dict{Dict.filenum}.gv') + Dict.filenum += 1 + di_graph.attr(rankdir='LR') + + with di_graph.subgraph(name='cluster_Dict') as subgraph: + subgraph.attr(label='Dictionary') + subgraph.attr(style='filled') + subgraph.attr(color='lightgrey') + subgraph.node_attr.update(style='filled', color='white') + + # Create nodes for each key-value pair in the dictionary + for k, val in data.items(): + if isinstance(val, dict): # if value is a nested dictionary + subgraph.node(str(k), label=str(k), shape='rectangle', + style='filled', color='lightblue2') + + for nested_k, nested_v in val.items(): + if nested_k in keys: + subgraph.node(str(nested_k), label=f"{str(nested_k)}: {str(nested_v)}", + style='filled', color='green') + + else: + subgraph.node(str(nested_k), label=f"{str(nested_k)}: {str(nested_v)}") + # Add an edge from parent node to child node + subgraph.edge(str(k), str(nested_k)) + + else: # if value is not a nested dictionary + + if k in keys: + subgraph.node(str(k), label=f"{str(k)}: {str(val)}", + style='filled', color='green') + else: + subgraph.node(str(k), label=f"{str(k)}: {str(val)}") + + # Render the graph to a file + di_graph.view() diff --git a/seealgo/tests/test_dict.py b/seealgo/tests/test_dict.py new file mode 100644 index 0000000..4920693 --- /dev/null +++ b/seealgo/tests/test_dict.py @@ -0,0 +1,110 @@ +""" +This file contains unit tests for +visualizations involving dictionary data structures. +""" + +from seealgo import Dict + + +### UNIT TESTS + + +def test_initdict(): + """ + Test the visualization of an unchanged dictionary. + """ + + init_dict = {'key1': 'one', 'key2': 2, 'key3': 'three'} + viz0 = Dict() + viz0.see(None, init_dict) + + with open('dict1.gv', 'r', encoding='utf-8') as file: + viz_contents = file.read() + + with open('outputFiles/initDictOutput.txt', 'r', encoding='utf-8') as true_file: + true_contents = true_file.read() + + assert viz_contents == true_contents + + +def test_nesteddict(): + """ + Test the visualization of a nested dictionary. + """ + + nested_dict = { + 'key1': 'value1', + 'key2': { + 'nested_key1': 'nested_value1', + 'nested_key2': 'nested_value2' + } + } + viz_nested = Dict() + viz_nested.see(None, nested_dict) + + with open('dict2.gv', 'r', encoding='utf-8') as file: + viz_contents = file.read() + + with open('outputFiles/nestedDictOutput.txt', 'r', encoding='utf-8') as true_file: + true_contents = true_file.read() + + assert viz_contents == true_contents + + +def test_updatedict(): + """ + Test the visualization of adding + a key:value pair to a dictionary. + """ + + dict1 = {'1': 1, '2': 4, '3': 9, '4':16} + dict_new = {'5': 25, '6': 36, '7': 49} + viz1 = Dict() + + def update_func(user_dict): + user_dict.update(dict_new) + return user_dict + + viz1.see(update_func, dict1) + + with open('dict3.gv', 'r', encoding='utf-8') as file: + viz_contents1 = file.read() + with open('dict4.gv', 'r', encoding='utf-8') as file: + viz_contents2 = file.read() + + with open('outputFiles/updateDictOutput1.txt', 'r', encoding='utf-8') as true_file: + true_contents1 = true_file.read() + with open('outputFiles/updateDictOutput2.txt', 'r', encoding='utf-8') as true_file: + true_contents2 = true_file.read() + + assert viz_contents1 == true_contents1 + assert viz_contents2 == true_contents2 + + +def test_popfromdict(): + """ + Test the visualization of removing + a value from a dictionary. + """ + + dict2 = {'apple': 'red', 'banana': 'yellow', 'goldfish': 'gold', 'kiwi': 'green'} + viz2 = Dict() + + def pop_func(user_dict): + user_dict.pop('goldfish') + return user_dict + + viz2.see(pop_func, dict2) + + with open('dict5.gv', 'r', encoding='utf-8') as file: + viz_contents1 = file.read() + with open('dict6.gv', 'r', encoding='utf-8') as file: + viz_contents2 = file.read() + + with open('outputFiles/popDictOutput1.txt', 'r', encoding='utf-8') as true_file: + true_contents1 = true_file.read() + with open('outputFiles/popDictOutput2.txt', 'r', encoding='utf-8') as true_file: + true_contents2 = true_file.read() + + assert viz_contents1 == true_contents1 + assert viz_contents2 == true_contents2