memory-graph 0.3.30__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- memory_graph/__init__.py +322 -0
- memory_graph/call_stack.py +9 -0
- memory_graph/config.py +35 -0
- memory_graph/config_default.py +108 -0
- memory_graph/config_helpers.py +56 -0
- memory_graph/extension_numpy.py +30 -0
- memory_graph/extension_pandas.py +26 -0
- memory_graph/html_table.py +137 -0
- memory_graph/list_view.py +57 -0
- memory_graph/memory_to_nodes.py +200 -0
- memory_graph/node_base.py +136 -0
- memory_graph/node_key_value.py +124 -0
- memory_graph/node_leaf.py +23 -0
- memory_graph/node_linear.py +100 -0
- memory_graph/node_table.py +83 -0
- memory_graph/sequence.py +111 -0
- memory_graph/slicer.py +46 -0
- memory_graph/slices.py +163 -0
- memory_graph/slices_iterator.py +55 -0
- memory_graph/slices_table_iterator.py +79 -0
- memory_graph/test.py +245 -0
- memory_graph/test_max_graph_depth.py +27 -0
- memory_graph/test_memory_graph.py +15 -0
- memory_graph/test_memory_to_nodes.py +15 -0
- memory_graph/test_sequence.py +37 -0
- memory_graph/test_slicer.py +50 -0
- memory_graph/test_slices.py +91 -0
- memory_graph/test_slices_iterator.py +28 -0
- memory_graph/utils.py +105 -0
- memory_graph-0.3.30.dist-info/METADATA +897 -0
- memory_graph-0.3.30.dist-info/RECORD +34 -0
- memory_graph-0.3.30.dist-info/WHEEL +5 -0
- memory_graph-0.3.30.dist-info/licenses/LICENSE.txt +25 -0
- memory_graph-0.3.30.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# This file is part of memory_graph.
|
|
2
|
+
# Copyright (c) 2023, Bas Terwijn.
|
|
3
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
|
|
5
|
+
from memory_graph.node_base import Node_Base
|
|
6
|
+
import memory_graph.node_base
|
|
7
|
+
import memory_graph.config as config
|
|
8
|
+
import html
|
|
9
|
+
|
|
10
|
+
def html_table_frame(s, border, color, spacing=5):
|
|
11
|
+
""" Helper function to add the HTML table frame to the string s setting the 'border' and 'color'. """
|
|
12
|
+
return (f'<\n<TABLE BORDER="{border}" CELLBORDER="1" CELLSPACING="{spacing}" CELLPADDING="0" BGCOLOR="{color}" PORT="table">\n <TR>' +
|
|
13
|
+
s + '</TR>\n</TABLE>\n>')
|
|
14
|
+
|
|
15
|
+
def format_string(s):
|
|
16
|
+
""" Helper function to format the string s to be shown in the graph. Setting the max_string_length and escaping html characters. """
|
|
17
|
+
s = config.to_string(s)
|
|
18
|
+
s = (s[:config.max_string_length] + '...') if len(s) > config.max_string_length else s
|
|
19
|
+
return html.escape(s)
|
|
20
|
+
|
|
21
|
+
class HTML_Table:
|
|
22
|
+
"""
|
|
23
|
+
The HTML_Table class is used to create a table of data that can be visualized in the graph.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""
|
|
28
|
+
Create an HTML_Table object.
|
|
29
|
+
"""
|
|
30
|
+
self.html = ''
|
|
31
|
+
self.add_new_line_flag = False
|
|
32
|
+
self.is_empty = True
|
|
33
|
+
self.col_count = 0
|
|
34
|
+
self.row_count = 0
|
|
35
|
+
self.ref_count = 0
|
|
36
|
+
self.max_col_count = 0
|
|
37
|
+
self.edges = []
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
""" Get the string representation of the HTML_Table object. """
|
|
41
|
+
return self.html
|
|
42
|
+
|
|
43
|
+
def add_new_line(self):
|
|
44
|
+
""" Set the 'add_new_line_flag' to add a new line to the table when adding the next table element. """
|
|
45
|
+
self.add_new_line_flag = True
|
|
46
|
+
self.row_count += 1
|
|
47
|
+
if self.col_count > self.max_col_count:
|
|
48
|
+
self.max_col_count = self.col_count
|
|
49
|
+
self.col_count = 0
|
|
50
|
+
|
|
51
|
+
def check_add_new_line(self):
|
|
52
|
+
""" Check if a new line should be added to the table, and if so add it and sets the 'add_new_line_flag' to False."""
|
|
53
|
+
if self.add_new_line_flag:
|
|
54
|
+
self.html += '</TR>\n <TR>'
|
|
55
|
+
self.add_new_line_flag = False
|
|
56
|
+
|
|
57
|
+
def add_string(self, s, border=0):
|
|
58
|
+
""" Add a string s to the table. """
|
|
59
|
+
self.html += f'<TD BORDER="{border}">'+format_string(s)+'</TD>'
|
|
60
|
+
self.is_empty = False
|
|
61
|
+
|
|
62
|
+
def add_index(self, s):
|
|
63
|
+
""" Add an index s to the table. """
|
|
64
|
+
self.check_add_new_line()
|
|
65
|
+
self.html += f'<TD BORDER="0"><font color="#505050">{str(s)}</font></TD>'
|
|
66
|
+
self.col_count += 1
|
|
67
|
+
|
|
68
|
+
def add_entry(self, node, nodes, child, id_to_slices, rounded=False, border=1, dashed=False):
|
|
69
|
+
""" Add child to the table either as reference if it is a Node_Base or as a value otherwise. """
|
|
70
|
+
#print('child:', child)
|
|
71
|
+
child_id = id(child)
|
|
72
|
+
if child_id in nodes:
|
|
73
|
+
child = nodes[child_id]
|
|
74
|
+
if child_id in id_to_slices:
|
|
75
|
+
self.add_reference(node, child, rounded, border, dashed)
|
|
76
|
+
else:
|
|
77
|
+
self.add_value(config.graph_cut_symbol, rounded, border)
|
|
78
|
+
else:
|
|
79
|
+
self.add_value(child, rounded, border)
|
|
80
|
+
|
|
81
|
+
def add_value(self, s, rounded=False, border=1):
|
|
82
|
+
""" Helper function to add a value s to the table. """
|
|
83
|
+
self.check_add_new_line()
|
|
84
|
+
r = ' STYLE="ROUNDED"' if rounded else ''
|
|
85
|
+
self.html += f'<TD BORDER="{border}"{r}> {format_string(s)} </TD>'
|
|
86
|
+
self.col_count += 1
|
|
87
|
+
|
|
88
|
+
def add_reference(self, node, child, rounded=False, border=1, dashed=False):
|
|
89
|
+
""" Helper function to add a reference to the table. """
|
|
90
|
+
self.check_add_new_line()
|
|
91
|
+
r = ' STYLE="ROUNDED"' if rounded else ''
|
|
92
|
+
self.html += f'<TD BORDER="{border}" PORT="ref{self.ref_count}"{r}> </TD>'
|
|
93
|
+
self.edges.append( (f'{node.get_name()}:ref{self.ref_count}',
|
|
94
|
+
child.get_name(), dashed) )
|
|
95
|
+
self.ref_count += 1
|
|
96
|
+
self.col_count += 1
|
|
97
|
+
|
|
98
|
+
def add_dots(self, rounded=False, border=1):
|
|
99
|
+
""" Helper function to add dots to the table. """
|
|
100
|
+
self.check_add_new_line()
|
|
101
|
+
r = 'STYLE="ROUNDED"' if rounded else ''
|
|
102
|
+
self.html += f'<TD BORDER="{border}" {r}>...</TD>'
|
|
103
|
+
self.col_count += 1
|
|
104
|
+
|
|
105
|
+
def to_string(self, border=1, color='white'):
|
|
106
|
+
""" Construct the HTML table string with the 'border' and 'color' settings. """
|
|
107
|
+
if self.col_count == 0 and self.row_count == 0:
|
|
108
|
+
if self.is_empty:
|
|
109
|
+
self.add_string(' ')
|
|
110
|
+
return html_table_frame(self.html, border, color, spacing=0)
|
|
111
|
+
return html_table_frame(self.html, border, color)
|
|
112
|
+
|
|
113
|
+
def get_column(self):
|
|
114
|
+
""" Get the number of columns in the table. """
|
|
115
|
+
return self.col_count
|
|
116
|
+
|
|
117
|
+
def get_max_column(self):
|
|
118
|
+
""" Get the maximum value of the number of columns of rows in the table. """
|
|
119
|
+
return self.max_col_count
|
|
120
|
+
|
|
121
|
+
def get_row(self):
|
|
122
|
+
""" Get the number of rows in the table. """
|
|
123
|
+
return self.row_count
|
|
124
|
+
|
|
125
|
+
def get_edges(self):
|
|
126
|
+
""" Get the edges that need to be added to connect the table to other tables in the graph. """
|
|
127
|
+
return self.edges
|
|
128
|
+
|
|
129
|
+
if __name__ == '__main__':
|
|
130
|
+
table = HTML_Table()
|
|
131
|
+
rows = 4
|
|
132
|
+
columns = 5
|
|
133
|
+
for r in range(rows):
|
|
134
|
+
for c in range(columns):
|
|
135
|
+
table.add_value(f'{c},{r}')
|
|
136
|
+
table.add_new_line()
|
|
137
|
+
print(table.to_string())
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# This file is part of memory_graph.
|
|
2
|
+
# Copyright (c) 2023, Bas Terwijn.
|
|
3
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class List_View:
|
|
7
|
+
def __init__(self, base_list, begin, end):
|
|
8
|
+
self.base_list = base_list
|
|
9
|
+
self.begin = max(0, begin)
|
|
10
|
+
self.end = min(end, len(base_list))
|
|
11
|
+
|
|
12
|
+
def __getitem__(self, index):
|
|
13
|
+
if isinstance(index, slice):
|
|
14
|
+
# Calculate new begin and end indices within the bounds of the current view
|
|
15
|
+
start, stop, step = index.indices(self.end - self.begin)
|
|
16
|
+
if step != 1:
|
|
17
|
+
raise ValueError("List_View does not support slices with steps other than 1")
|
|
18
|
+
# Adjust the indices relative to the base list
|
|
19
|
+
new_start = self.begin + start
|
|
20
|
+
new_end = self.begin + stop
|
|
21
|
+
return List_View(self.base_list, new_start, new_end)
|
|
22
|
+
elif isinstance(index, int):
|
|
23
|
+
if index < 0 or index >= (self.end - self.begin):
|
|
24
|
+
raise IndexError("list index out of range")
|
|
25
|
+
return self.base_list[self.begin + index]
|
|
26
|
+
else:
|
|
27
|
+
raise TypeError("Invalid index type")
|
|
28
|
+
|
|
29
|
+
def __setitem__(self, index, value):
|
|
30
|
+
if index < 0 or index >= (self.end - self.begin):
|
|
31
|
+
raise IndexError("list index out of range")
|
|
32
|
+
self.base_list[self.begin + index] = value
|
|
33
|
+
|
|
34
|
+
def __len__(self):
|
|
35
|
+
return self.end - self.begin
|
|
36
|
+
|
|
37
|
+
def __iter__(self):
|
|
38
|
+
for i in range(self.begin, self.end):
|
|
39
|
+
yield self.base_list[i]
|
|
40
|
+
|
|
41
|
+
def __repr__(self):
|
|
42
|
+
return f"List_View({self.base_list[self.begin:self.end]})"
|
|
43
|
+
|
|
44
|
+
def test_list_vew():
|
|
45
|
+
original_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
46
|
+
list_view = List_View(original_list, 3, 8)
|
|
47
|
+
print(list_view) # Output: List_View([3, 4, 5, 6, 7])
|
|
48
|
+
print(list_view[1:4]) # Output: List_View([4, 5, 6])
|
|
49
|
+
# 2D array
|
|
50
|
+
n = 4
|
|
51
|
+
data = [i for i in range(n*n)]
|
|
52
|
+
list_views = [List_View(data, i, i+n) for i in range(0,len(data),n)]
|
|
53
|
+
for row in list_views:
|
|
54
|
+
print(row)
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
test_list_vew()
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# This file is part of memory_graph.
|
|
2
|
+
# Copyright (c) 2023, Bas Terwijn.
|
|
3
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
|
|
5
|
+
from memory_graph.node_leaf import Node_Leaf
|
|
6
|
+
from memory_graph.node_linear import Node_Linear
|
|
7
|
+
from memory_graph.node_key_value import Node_Key_Value
|
|
8
|
+
|
|
9
|
+
import memory_graph.utils as utils
|
|
10
|
+
import memory_graph.config as config
|
|
11
|
+
|
|
12
|
+
import graphviz
|
|
13
|
+
|
|
14
|
+
def read_nodes(data):
|
|
15
|
+
|
|
16
|
+
def data_to_node(data_type, data):
|
|
17
|
+
if data_type in config.type_to_node: # for predefined types
|
|
18
|
+
return config.type_to_node[data_type](data)
|
|
19
|
+
elif utils.has_dict_attributes(data): # for user defined classes
|
|
20
|
+
return Node_Key_Value(data, utils.filter_dict(utils.get_dict_attributes(data)) )
|
|
21
|
+
elif utils.is_finite_iterable(data): # for lists, tuples, sets, ...
|
|
22
|
+
return Node_Linear(data, data)
|
|
23
|
+
else:
|
|
24
|
+
return Node_Leaf(data, data)
|
|
25
|
+
|
|
26
|
+
def memory_to_nodes_recursive(nodes, data, parent, parent_index):
|
|
27
|
+
data_type = type(data)
|
|
28
|
+
if not data_type in config.not_node_types or parent is None:
|
|
29
|
+
data_id = id(data)
|
|
30
|
+
if data_id in nodes:
|
|
31
|
+
node = nodes[data_id]
|
|
32
|
+
else:
|
|
33
|
+
node = data_to_node(data_type, data)
|
|
34
|
+
nodes[data_id] = node
|
|
35
|
+
for index in node.get_children().indices_all():
|
|
36
|
+
child = node.get_children()[index]
|
|
37
|
+
memory_to_nodes_recursive(nodes, child, node, index)
|
|
38
|
+
if not parent is None:
|
|
39
|
+
node.add_parent_index(parent, parent_index)
|
|
40
|
+
|
|
41
|
+
nodes = {}
|
|
42
|
+
memory_to_nodes_recursive(nodes, data, None, None)
|
|
43
|
+
root_id = id(data)
|
|
44
|
+
return nodes, root_id
|
|
45
|
+
|
|
46
|
+
# --------------------------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def get_max_type_depth(node_id, node):
|
|
49
|
+
if node_id in config.type_to_depth:
|
|
50
|
+
return config.type_to_depth[node_id]
|
|
51
|
+
elif node.get_type() in config.type_to_depth:
|
|
52
|
+
return config.type_to_depth[node.get_type()]
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def slice_nodes(nodes, root_id, max_graph_depth):
|
|
56
|
+
|
|
57
|
+
def slice_nodes_recursive(nodes, node_id, id_to_slices, max_graph_depth):
|
|
58
|
+
if max_graph_depth == 0 or node_id in id_to_slices:
|
|
59
|
+
return
|
|
60
|
+
if node_id in nodes:
|
|
61
|
+
node = nodes[node_id]
|
|
62
|
+
children = node.get_children()
|
|
63
|
+
if children.is_empty():
|
|
64
|
+
id_to_slices[node_id] = None
|
|
65
|
+
else:
|
|
66
|
+
slicer = node.get_slicer()
|
|
67
|
+
slices = children.slice(slicer)
|
|
68
|
+
id_to_slices[node_id] = slices
|
|
69
|
+
if not node.is_hidden_node():
|
|
70
|
+
max_graph_depth -= 1
|
|
71
|
+
max_type_depth = get_max_type_depth(node_id, node)
|
|
72
|
+
if max_type_depth:
|
|
73
|
+
max_graph_depth = min(max_type_depth, max_graph_depth)
|
|
74
|
+
for index in slices:
|
|
75
|
+
slice_nodes_recursive(nodes, id(children[index]), id_to_slices, max_graph_depth)
|
|
76
|
+
id_to_slices = {}
|
|
77
|
+
slice_nodes_recursive(nodes, root_id, id_to_slices, max_graph_depth)
|
|
78
|
+
return id_to_slices
|
|
79
|
+
|
|
80
|
+
# --------------------------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def add_parent_indices(nodes, type_to_parent_indices, id_to_slices, max_missing_edges):
|
|
83
|
+
#print('add_parent_indices type_to_parent_indices:',type_to_parent_indices)
|
|
84
|
+
for _, parent_indices in type_to_parent_indices.items():
|
|
85
|
+
dashed = len(parent_indices) > max_missing_edges
|
|
86
|
+
for parent, index in parent_indices[0:max_missing_edges]:
|
|
87
|
+
new_parent = False
|
|
88
|
+
parent_id = parent.get_id()
|
|
89
|
+
if not parent_id in id_to_slices:
|
|
90
|
+
new_parent = True
|
|
91
|
+
id_to_slices[parent_id] = parent.get_children().empty_slices()
|
|
92
|
+
slices = id_to_slices[parent_id]
|
|
93
|
+
slices.add_index(index, dashed=dashed)
|
|
94
|
+
if new_parent:
|
|
95
|
+
add_indices_to_parents(nodes, parent_id, id_to_slices, max_missing_edges)
|
|
96
|
+
|
|
97
|
+
def add_indices_to_parents(nodes, node_id, id_to_slices, max_missing_edges):
|
|
98
|
+
#print('add_indices_to_parents node_id:',node_id)
|
|
99
|
+
type_to_parent_indices = {}
|
|
100
|
+
parent_indices = nodes[node_id].get_parent_indices()
|
|
101
|
+
for parent, indices in parent_indices.items():
|
|
102
|
+
if parent is None:
|
|
103
|
+
continue
|
|
104
|
+
parent_type = parent.get_type()
|
|
105
|
+
parent_id = parent.get_id()
|
|
106
|
+
if (parent_type in type_to_parent_indices and
|
|
107
|
+
len(type_to_parent_indices[parent_type]) > max_missing_edges): # early stop
|
|
108
|
+
continue
|
|
109
|
+
parent_slices = None
|
|
110
|
+
if parent_id in id_to_slices:
|
|
111
|
+
parent_slices = id_to_slices[parent_id]
|
|
112
|
+
for index in indices:
|
|
113
|
+
if parent_slices is None or not parent_slices.has_index(index):
|
|
114
|
+
if not parent_type in type_to_parent_indices:
|
|
115
|
+
type_to_parent_indices[parent_type] = []
|
|
116
|
+
parent_indices = type_to_parent_indices[parent_type]
|
|
117
|
+
if len(parent_indices) > max_missing_edges:
|
|
118
|
+
break
|
|
119
|
+
else:
|
|
120
|
+
parent_indices.append((parent, index))
|
|
121
|
+
add_parent_indices(nodes, type_to_parent_indices, id_to_slices, max_missing_edges)
|
|
122
|
+
|
|
123
|
+
def add_missing_edges(nodes, id_to_slices, max_missing_edges=3):
|
|
124
|
+
old_id_to_slices_keys = set(id_to_slices.keys())
|
|
125
|
+
for node_id in old_id_to_slices_keys:
|
|
126
|
+
add_indices_to_parents(nodes, node_id, id_to_slices, max_missing_edges)
|
|
127
|
+
return id_to_slices
|
|
128
|
+
|
|
129
|
+
# --------------------------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
import memory_graph.config_helpers as config_helpers
|
|
132
|
+
|
|
133
|
+
def create_depth_of_nodes(nodes, nodes_at_depth):
|
|
134
|
+
depth_of_nodes = {}
|
|
135
|
+
for node_id, depth in nodes_at_depth.items():
|
|
136
|
+
node = nodes[node_id]
|
|
137
|
+
if node_id in nodes and not node.is_hidden_node():
|
|
138
|
+
if not depth in depth_of_nodes:
|
|
139
|
+
depth_of_nodes[depth] = []
|
|
140
|
+
depth_of_nodes[depth].append(node)
|
|
141
|
+
return depth_of_nodes
|
|
142
|
+
|
|
143
|
+
def add_subgraph(graphviz_graph, nodes_to_subgraph):
|
|
144
|
+
new_node_names = [node.get_name() for node in nodes_to_subgraph]
|
|
145
|
+
if len(new_node_names) > 1:
|
|
146
|
+
graphviz_graph.body.append('subgraph { rank=same; '+ ' -> '.join(new_node_names) + '[weight='+str(config.graph_stability)+', style=invis]; }\n')
|
|
147
|
+
|
|
148
|
+
def add_to_graphviz_graph(graphviz_graph, nodes, node, slices, id_to_slices, subgraphed_nodes, depth):
|
|
149
|
+
html_table = node.get_html_table(nodes, slices, id_to_slices)
|
|
150
|
+
edges = html_table.get_edges()
|
|
151
|
+
color = config_helpers.get_color(node)
|
|
152
|
+
border = 3 if node.is_root() else 1
|
|
153
|
+
graphviz_graph.node(node.get_name(),
|
|
154
|
+
html_table.to_string(border, color),
|
|
155
|
+
xlabel=node.get_label(slices))
|
|
156
|
+
# ------------ edges
|
|
157
|
+
for parent,child,dashed in edges:
|
|
158
|
+
graphviz_graph.edge(parent, child+':table', style='dashed' if dashed else 'solid')
|
|
159
|
+
|
|
160
|
+
def build_graph_depth_first(graphviz_graph, nodes, node_id, id_to_slices, nodes_at_depth, subgraphed_nodes, depth):
|
|
161
|
+
if node_id in id_to_slices:
|
|
162
|
+
if node_id in nodes_at_depth:
|
|
163
|
+
return
|
|
164
|
+
nodes_at_depth[node_id] = depth
|
|
165
|
+
node = nodes[node_id]
|
|
166
|
+
children = node.get_children()
|
|
167
|
+
slices = None
|
|
168
|
+
if node_id in id_to_slices:
|
|
169
|
+
slices = id_to_slices[node_id]
|
|
170
|
+
if not slices is None:
|
|
171
|
+
for index in slices:
|
|
172
|
+
child_id = id(children[index])
|
|
173
|
+
build_graph_depth_first(graphviz_graph, nodes, child_id, id_to_slices, nodes_at_depth, subgraphed_nodes, depth+1)
|
|
174
|
+
if not node.is_hidden_node():
|
|
175
|
+
add_to_graphviz_graph(graphviz_graph, nodes, node, slices, id_to_slices, subgraphed_nodes, depth)
|
|
176
|
+
|
|
177
|
+
def build_graph(graphviz_graph, nodes, root_id, id_to_slices):
|
|
178
|
+
nodes_at_depth = {}
|
|
179
|
+
build_graph_depth_first(graphviz_graph, nodes, root_id, id_to_slices, nodes_at_depth, set(), 0)
|
|
180
|
+
depth_of_nodes = create_depth_of_nodes(nodes, nodes_at_depth)
|
|
181
|
+
#print('nodes_at_depth:',nodes_at_depth,'depth_of_nodes:', depth_of_nodes)
|
|
182
|
+
for depth, depth_nodes in depth_of_nodes.items():
|
|
183
|
+
add_subgraph(graphviz_graph, depth_nodes)
|
|
184
|
+
|
|
185
|
+
def memory_to_nodes(data):
|
|
186
|
+
nodes, root_id = read_nodes(data)
|
|
187
|
+
#print('nodes:',nodes,'root_id:',root_id)
|
|
188
|
+
id_to_slices = slice_nodes(nodes, root_id, config.max_graph_depth)
|
|
189
|
+
#print('id_to_slices:',id_to_slices)
|
|
190
|
+
id_to_slices = add_missing_edges(nodes, id_to_slices, config.max_missing_edges)
|
|
191
|
+
#print('id_to_slices:',id_to_slices)
|
|
192
|
+
graphviz_graph_attr = {}
|
|
193
|
+
graphviz_node_attr = {'shape':'plaintext'}
|
|
194
|
+
graphviz_edge_attr = {}
|
|
195
|
+
graphviz_graph=graphviz.Digraph('memory_graph',
|
|
196
|
+
graph_attr=graphviz_graph_attr,
|
|
197
|
+
node_attr=graphviz_node_attr,
|
|
198
|
+
edge_attr=graphviz_edge_attr)
|
|
199
|
+
build_graph(graphviz_graph, nodes, root_id, id_to_slices)
|
|
200
|
+
return graphviz_graph
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# This file is part of memory_graph.
|
|
2
|
+
# Copyright (c) 2023, Bas Terwijn.
|
|
3
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
|
|
5
|
+
import memory_graph.utils as utils
|
|
6
|
+
import memory_graph.config as config
|
|
7
|
+
import memory_graph.config_helpers as config_helpers
|
|
8
|
+
from memory_graph.sequence import Sequence1D
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
|
|
12
|
+
class Node_Base(ABC):
|
|
13
|
+
"""
|
|
14
|
+
Node_Base represents a node in the memory graph. This base class has different subclasses for different types of nodes.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, data, children=None):
|
|
18
|
+
"""
|
|
19
|
+
Create a Node_Base object.
|
|
20
|
+
"""
|
|
21
|
+
self.data = data
|
|
22
|
+
self.children = Sequence1D([]) if children is None else children
|
|
23
|
+
self.parent_indices = {}
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
"""
|
|
27
|
+
Return a string representation of the node showing the original data represented by the node.
|
|
28
|
+
"""
|
|
29
|
+
return f'{self.get_type_name()}'
|
|
30
|
+
|
|
31
|
+
def add_parent_index(self, parent, parent_index):
|
|
32
|
+
"""
|
|
33
|
+
Add a parent to the node.
|
|
34
|
+
"""
|
|
35
|
+
if not parent in self.parent_indices:
|
|
36
|
+
self.parent_indices[parent] = []
|
|
37
|
+
self.parent_indices[parent].append(parent_index)
|
|
38
|
+
|
|
39
|
+
def is_root(self):
|
|
40
|
+
"""
|
|
41
|
+
Return if the node is the root node.
|
|
42
|
+
"""
|
|
43
|
+
return len(self.parent_indices) == 0
|
|
44
|
+
|
|
45
|
+
def get_parent_indices(self):
|
|
46
|
+
return self.parent_indices
|
|
47
|
+
|
|
48
|
+
def get_id(self):
|
|
49
|
+
"""
|
|
50
|
+
Return the id of the node.
|
|
51
|
+
"""
|
|
52
|
+
return id(self.data)
|
|
53
|
+
|
|
54
|
+
def __eq__(self, other):
|
|
55
|
+
"""
|
|
56
|
+
Return if the node is equal to another node.
|
|
57
|
+
"""
|
|
58
|
+
return self.get_id() == other.get_id()
|
|
59
|
+
|
|
60
|
+
def __hash__(self):
|
|
61
|
+
"""
|
|
62
|
+
Return the hash of the node.
|
|
63
|
+
"""
|
|
64
|
+
return self.get_id()
|
|
65
|
+
|
|
66
|
+
def get_data(self):
|
|
67
|
+
"""
|
|
68
|
+
Return the original data represented by the node.
|
|
69
|
+
"""
|
|
70
|
+
return self.data
|
|
71
|
+
|
|
72
|
+
def get_type(self):
|
|
73
|
+
"""
|
|
74
|
+
Return the type of the data represented by the node.
|
|
75
|
+
"""
|
|
76
|
+
return type(self.data)
|
|
77
|
+
|
|
78
|
+
def get_type_name(self):
|
|
79
|
+
"""
|
|
80
|
+
Return the name of the type of the data represented by the node.
|
|
81
|
+
"""
|
|
82
|
+
return utils.get_type_name(self.data)
|
|
83
|
+
|
|
84
|
+
def get_children(self):
|
|
85
|
+
"""
|
|
86
|
+
Return the children of the node. Initially the children are raw data, but
|
|
87
|
+
later they too are converted to Node_Base by the Memory_visitor using the Node_Base 'transform' method.
|
|
88
|
+
"""
|
|
89
|
+
return self.children
|
|
90
|
+
|
|
91
|
+
def get_name(self):
|
|
92
|
+
"""
|
|
93
|
+
Return a unique name for the node.
|
|
94
|
+
"""
|
|
95
|
+
return f'node{self.get_id()}'
|
|
96
|
+
|
|
97
|
+
def get_html_table(self, nodes, slices, id_to_slices):
|
|
98
|
+
"""
|
|
99
|
+
Return the HTML_Table object that determines how the node is visualized in the graph.
|
|
100
|
+
"""
|
|
101
|
+
from memory_graph.html_table import HTML_Table
|
|
102
|
+
import memory_graph.node_base
|
|
103
|
+
html_table = HTML_Table()
|
|
104
|
+
self.fill_html_table(nodes, html_table, slices, id_to_slices)
|
|
105
|
+
return html_table
|
|
106
|
+
|
|
107
|
+
def get_slicer(self):
|
|
108
|
+
return config_helpers.get_slicer(self, self.get_data())
|
|
109
|
+
|
|
110
|
+
def is_hidden_node(self):
|
|
111
|
+
"""
|
|
112
|
+
Return if the node is a hidden node in the graph.
|
|
113
|
+
"""
|
|
114
|
+
from memory_graph.node_key_value import Node_Key_Value
|
|
115
|
+
if self.get_type() is tuple:
|
|
116
|
+
parent_indices = self.get_parent_indices()
|
|
117
|
+
if len(parent_indices) == 1:
|
|
118
|
+
first_parent = next(iter(parent_indices))
|
|
119
|
+
if type(first_parent) is Node_Key_Value:
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
# -------------------- Node_Base interface, overriden by subclasses --------------------
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def fill_html_table(self, html_table, slices, id_to_slices):
|
|
127
|
+
"""
|
|
128
|
+
Fill the HTML_Table object with each child of the node.
|
|
129
|
+
"""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def get_label(self, slices):
|
|
133
|
+
"""
|
|
134
|
+
Return a label for the node to be shown in the graph next to the HTML table.
|
|
135
|
+
"""
|
|
136
|
+
return self.get_type_name()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# This file is part of memory_graph.
|
|
2
|
+
# Copyright (c) 2023, Bas Terwijn.
|
|
3
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
|
|
5
|
+
from memory_graph.node_base import Node_Base
|
|
6
|
+
from memory_graph.sequence import Sequence1D
|
|
7
|
+
|
|
8
|
+
import memory_graph.config_helpers as config_helpers
|
|
9
|
+
|
|
10
|
+
class Node_Key_Value(Node_Base):
|
|
11
|
+
"""
|
|
12
|
+
Node_Key_Value (subclass of Node_Base) is a node that represents a node with key-value
|
|
13
|
+
pairs (tuples) as children. This node type mainly used for dictionaries and classes.
|
|
14
|
+
Each child is made a Hidden_Node_Base so that each tuple is not shown as a separate node
|
|
15
|
+
but instead as a key,value pair in the current node.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, data, children):
|
|
19
|
+
"""
|
|
20
|
+
Create a Node_Key_Value object. Use a Slicer to slice the children so the
|
|
21
|
+
Node_Base will not get too big or have too many childeren in the graph.
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(data, Sequence1D(children))
|
|
24
|
+
|
|
25
|
+
def has_references(self, nodes, slices, id_to_slices):
|
|
26
|
+
"""
|
|
27
|
+
Return if the node has references to other nodes.
|
|
28
|
+
"""
|
|
29
|
+
for index in slices:
|
|
30
|
+
child_id = id(self.children[index])
|
|
31
|
+
child = nodes[child_id]
|
|
32
|
+
key_id = id(child.get_children()[0])
|
|
33
|
+
if key_id in nodes:
|
|
34
|
+
key = nodes[key_id]
|
|
35
|
+
if not key.is_hidden_node():
|
|
36
|
+
return True
|
|
37
|
+
value_id = id(child.get_children()[1])
|
|
38
|
+
if value_id in nodes:
|
|
39
|
+
value = nodes[value_id]
|
|
40
|
+
if not value.is_hidden_node():
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
def is_vertical(self, nodes, slices, id_to_slices):
|
|
45
|
+
"""
|
|
46
|
+
Return if the node is vertical or horizontal based on the orientation of the children.
|
|
47
|
+
"""
|
|
48
|
+
vertical = config_helpers.get_vertical_orientation(self, None)
|
|
49
|
+
if vertical is None:
|
|
50
|
+
vertical = not self.has_references(nodes, slices, id_to_slices)
|
|
51
|
+
return vertical
|
|
52
|
+
|
|
53
|
+
def fill_html_table(self, nodes, html_table, slices, id_to_slices):
|
|
54
|
+
"""
|
|
55
|
+
Fill the html_table with the children of the Node_Base.
|
|
56
|
+
"""
|
|
57
|
+
if slices is None:
|
|
58
|
+
return
|
|
59
|
+
vertical = self.is_vertical(nodes, slices, id_to_slices)
|
|
60
|
+
if vertical:
|
|
61
|
+
self.fill_html_table_vertical(html_table, nodes, slices, id_to_slices)
|
|
62
|
+
else:
|
|
63
|
+
self.fill_html_table_horizontal(html_table, nodes, slices, id_to_slices)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def get_value_dashed(nodes, child, index, id_to_slices):
|
|
67
|
+
grandchild = child[index]
|
|
68
|
+
child_id = id(child)
|
|
69
|
+
is_dashed = False
|
|
70
|
+
if child_id in id_to_slices:
|
|
71
|
+
slices = id_to_slices[child_id]
|
|
72
|
+
if not slices is None:
|
|
73
|
+
is_dashed = slices.is_dashed(index)
|
|
74
|
+
return grandchild, is_dashed
|
|
75
|
+
|
|
76
|
+
def fill_html_table_vertical(self, html_table, nodes, slices, id_to_slices):
|
|
77
|
+
"""
|
|
78
|
+
Helper function to fill the html_table with the children of the Node_Base in vertical orientation.
|
|
79
|
+
"""
|
|
80
|
+
for index in slices.table_iter(self.children.size()):
|
|
81
|
+
if index>=0:
|
|
82
|
+
child = self.children[index]
|
|
83
|
+
key, is_dashed = self.get_value_dashed(nodes, child,0,id_to_slices)
|
|
84
|
+
html_table.add_entry(self, nodes, key, id_to_slices, rounded=True, dashed=is_dashed)
|
|
85
|
+
value, is_dashed = self.get_value_dashed(nodes, child,1,id_to_slices)
|
|
86
|
+
html_table.add_entry(self, nodes, value, id_to_slices, dashed=is_dashed)
|
|
87
|
+
else:
|
|
88
|
+
html_table.add_dots(rounded=True)
|
|
89
|
+
html_table.add_dots()
|
|
90
|
+
html_table.add_new_line()
|
|
91
|
+
|
|
92
|
+
def fill_html_table_horizontal(self, html_table, nodes, slices, id_to_slices):
|
|
93
|
+
"""
|
|
94
|
+
Helper function to fill the html_table with the children of the Node_Base in horizontal orientation.
|
|
95
|
+
"""
|
|
96
|
+
for index in slices.table_iter(self.children.size()):
|
|
97
|
+
if index>=0:
|
|
98
|
+
child = self.children[index]
|
|
99
|
+
key, is_dashed = self.get_value_dashed(nodes, child,0,id_to_slices)
|
|
100
|
+
html_table.add_entry(self, nodes, key, id_to_slices, rounded=True, dashed=is_dashed)
|
|
101
|
+
else:
|
|
102
|
+
html_table.add_dots(rounded=True)
|
|
103
|
+
html_table.add_new_line()
|
|
104
|
+
for index in slices.table_iter(self.children.size()):
|
|
105
|
+
if index>=0:
|
|
106
|
+
child = self.children[index]
|
|
107
|
+
value, is_dashed = self.get_value_dashed(nodes, child,1,id_to_slices)
|
|
108
|
+
html_table.add_entry(self, nodes, value, id_to_slices, dashed=is_dashed)
|
|
109
|
+
else:
|
|
110
|
+
html_table.add_dots()
|
|
111
|
+
|
|
112
|
+
def get_label(self, slices):
|
|
113
|
+
"""
|
|
114
|
+
Return a label for the node to be shown in the graph next to the HTML table.
|
|
115
|
+
"""
|
|
116
|
+
type_name = self.get_type_name()
|
|
117
|
+
if slices is None:
|
|
118
|
+
return f'{type_name}'
|
|
119
|
+
size = self.get_children().size()
|
|
120
|
+
s = slices.get_slices()
|
|
121
|
+
if len(s) == 1:
|
|
122
|
+
if s[0][1] - s[0][0] == size:
|
|
123
|
+
return f'{type_name}'
|
|
124
|
+
return f'{type_name} {size}'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This file is part of memory_graph.
|
|
2
|
+
# Copyright (c) 2023, Bas Terwijn.
|
|
3
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
|
|
5
|
+
from memory_graph.node_base import Node_Base
|
|
6
|
+
|
|
7
|
+
class Node_Leaf(Node_Base):
|
|
8
|
+
"""
|
|
9
|
+
Node_Leaf (subclass of Node_Base) is a leaf node with no children but a value.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, data, value):
|
|
13
|
+
"""
|
|
14
|
+
Create a Node_Leaf object.
|
|
15
|
+
"""
|
|
16
|
+
super().__init__(data)
|
|
17
|
+
self.value = value
|
|
18
|
+
|
|
19
|
+
def fill_html_table(self, nodes, html_table, slices, id_to_slices):
|
|
20
|
+
"""
|
|
21
|
+
Fill the html_table with the children of the Node_Base.
|
|
22
|
+
"""
|
|
23
|
+
html_table.add_value(self.value)
|