44import hashlib
55
66
7- def sha256 (content ):
8- return hashlib .sha256 (json_dumps (content ).encode ()).hexdigest ()
9-
10-
117class HashNode (TreeNode ):
8+ """A node in a HashTree. This class is used internally by the HashTree class.
9+
10+ Parameters
11+ ----------
12+ path : str
13+ The relative path of the node.
14+ value : str, int, float, list, bool, None
15+ The value of the node. Only leaf nodes can have a value.
16+
17+ Attributes
18+ ----------
19+ path : str
20+ The relative path of the node.
21+ value : str, int, float, list, bool, None
22+ The value of the node. Only leaf nodes can have a value.
23+ absolute_path : str
24+ The absolute path of the node.
25+ is_value : bool
26+ True if the node is a leaf node and has a value.
27+ signature : str
28+ The SHA256 signature of the node.
29+ children_dict : dict
30+ A dictionary of the children of the node. The keys are the relative paths
31+ children_paths : list[str]
32+ A list of the relative paths of the children of the node.
33+
34+ """
35+
1236 def __init__ (self , path , value = None ):
1337 super ().__init__ ()
1438 self .path = path
@@ -46,6 +70,21 @@ def children_paths(self):
4670
4771 @classmethod
4872 def from_dict (cls , data_dict , path = "" ):
73+ """Construct a HashNode from a dictionary.
74+
75+ Parameters
76+ ----------
77+ data_dict : dict
78+ A dictionary to construct the HashNode from.
79+ path : str
80+ The relative path of the node.
81+
82+ Returns
83+ -------
84+ :class:`compas.datastructures.HashNode`
85+ A HashNode constructed from the dictionary.
86+
87+ """
4988 node = cls (path )
5089 for key in data_dict :
5190 path = ".{}" .format (key )
@@ -59,40 +98,128 @@ def from_dict(cls, data_dict, path=""):
5998
6099
61100class HashTree (Tree ):
101+ """A Hash tree (or Merkle tree) is a tree in which every leaf node is labelled with the cryptographic hash of a data block
102+ and every non-leaf node is labelled with the hash of the labels of its child nodes.
103+ Hash trees allow efficient and secure verification of the contents of large data structures.
104+ They can also be used to compare different versions(states) of the same data structure for changes.
105+
106+ Attributes
107+ ----------
108+ signatures : dict[str, str]
109+ The SHA256 signatures of the nodes in the tree. The keys are the absolute paths of the nodes, the values are the signatures.
110+
111+
112+ Examples
113+ --------
114+ >>> tree1 = HashTree.from_dict({"a": {"b": 1, "c": 3}, "d": [1, 2, 3], "e": 2})
115+ >>> tree2 = HashTree.from_dict({"a": {"b": 1, "c": 2}, "d": [1, 2, 3], "f": 2})
116+ >>> tree1.print_hierarchy()
117+ └── ROOT @ 4cd56
118+ ├── .a @ c16fd
119+ │ ├── .b:1 @ c9b55
120+ │ └── .c:3 @ 518d4
121+ ├── .d:[1, 2, 3] @ 9be3a
122+ └── .e:2 @ 68355
123+ >>> tree2.print_hierarchy()
124+ └── ROOT @ fbe39
125+ ├── .a @ c2022
126+ │ ├── .b:1 @ c9b55
127+ │ └── .c:2 @ e3365
128+ ├── .d:[1, 2, 3] @ 9be3a
129+ └── .f:2 @ 93861
130+ >>> tree2.print_diff(tree1)
131+ Added:
132+ {'path': '.f', 'value': 2}
133+ Removed:
134+ {'path': '.e', 'value': 2}
135+ Modified:
136+ {'path': '.a.c', 'old': 3, 'new': 2}
137+
138+
139+ """
140+
62141 def __init__ (self ):
63142 super ().__init__ ()
64143 self .signatures = {}
65144
66145 @classmethod
67146 def from_dict (cls , data_dict ):
147+ """Construct a HashTree from a dictionary.
148+
149+ Parameters
150+ ----------
151+ data_dict : dict
152+ A dictionary to construct the HashTree from.
153+
154+ Returns
155+ -------
156+ :class:`compas.datastructures.HashTree`
157+ A HashTree constructed from the dictionary.
158+
159+ """
68160 tree = cls ()
69161 root = HashNode .from_dict (data_dict )
70162 tree .add (root )
71- tree .calculate_signatures ( )
163+ tree .node_signature ( tree . root )
72164 return tree
73165
74- def calculate_signatures (self ):
75- self .node_signature (self .root )
166+ @classmethod
167+ def from_object (cls , obj ):
168+ """Construct a HashTree from a COMPAS object."""
169+ raise NotImplementedError
170+
171+ @classmethod
172+ def from_scene (cls , scene ):
173+ """Construct a HashTree from a COMPAS scene."""
174+ raise NotImplementedError
76175
77176 def node_signature (self , node , parent_path = "" ):
177+ """Compute the SHA256 signature of a node. The computed nodes are cached in `self.signatures` dictionary.
178+
179+ Parameters
180+ ----------
181+ node : :class:`compas.datastructures.HashNode`
182+ The node to compute the signature of.
183+ parent_path : str
184+ The absolute path of the parent node.
185+
186+ Returns
187+ -------
188+ str
189+ The SHA256 signature of the node.
190+
191+ """
78192 absolute_path = parent_path + node .path
79193 if absolute_path in self .signatures :
80194 return self .signatures [absolute_path ]
81195
82- signature = sha256 (
83- {
84- "path " : node .path ,
85- "value" : node .value ,
86- "children" : [ self . node_signature ( child , absolute_path ) for child in node . children ],
87- }
88- )
196+ content = {
197+ "path" : node . path ,
198+ "value " : node .value ,
199+ "children" : [ self . node_signature ( child , absolute_path ) for child in node .children ] ,
200+ }
201+
202+ signature = hashlib . sha256 ( json_dumps ( content ). encode ()). hexdigest ( )
89203
90204 self .signatures [absolute_path ] = signature
91205 node ._signature = signature
92206
93207 return signature
94208
95209 def diff (self , other ):
210+ """Compute the difference between two HashTrees.
211+
212+ Parameters
213+ ----------
214+ other : :class:`compas.datastructures.HashTree`
215+ The HashTree to compare with.
216+
217+ Returns
218+ -------
219+ dict
220+ A dictionary containing the differences between the two HashTrees. The keys are `added`, `removed` and `modified`.
221+ The values are lists of dictionaries containing the paths and values of the nodes that were added, removed or modified.
222+ """
96223 added = []
97224 removed = []
98225 modified = []
@@ -123,6 +250,15 @@ def _diff(node1, node2):
123250 return {"added" : added , "removed" : removed , "modified" : modified }
124251
125252 def print_diff (self , other ):
253+ """Print the difference between two HashTrees.
254+
255+ Parameters
256+ ----------
257+ other : :class:`compas.datastructures.HashTree`
258+ The HashTree to compare with.
259+
260+ """
261+
126262 diff = self .diff (other )
127263 print ("Added:" )
128264 for item in diff ["added" ]:
@@ -133,27 +269,3 @@ def print_diff(self, other):
133269 print ("Modified:" )
134270 for item in diff ["modified" ]:
135271 print (item )
136-
137-
138- if __name__ == "__main__" :
139-
140- print ("\n COMPARE DICTS:" )
141- tree1 = HashTree .from_dict ({"a" : {"b" : 1 , "c" : 3 }, "d" : [1 , 2 , 3 ], "e" : 2 })
142- tree2 = HashTree .from_dict ({"a" : {"b" : 1 , "c" : 2 }, "d" : [1 , 2 , 3 ], "f" : 2 })
143-
144- tree1 .print_hierarchy ()
145- tree2 .print_hierarchy ()
146-
147- tree2 .print_diff (tree1 )
148-
149- print ("\n COMPARE MESH CHANGE:" )
150-
151- from compas .datastructures import Mesh
152-
153- mesh = Mesh .from_polyhedron (4 )
154- tree1 = HashTree .from_dict (mesh .data )
155- mesh .vertex_attribute (0 , "x" , 1.0 )
156- tree2 = HashTree .from_dict (mesh .data )
157- tree1 .print_hierarchy ()
158- tree2 .print_hierarchy ()
159- tree2 .print_diff (tree1 )
0 commit comments