Skip to content

Commit 1a5a254

Browse files
committed
docstring etc.
1 parent fb6d060 commit 1a5a254

File tree

3 files changed

+154
-38
lines changed

3 files changed

+154
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
* Added `compas_rhino8` as starting point for Rhino8 support.
1313
* Added tutorial for `compas.datastructures.Tree`.
14+
* Added `compas.datastructures.HashTree` and `compas.datastructures.HashNode`.
1415

1516
### Changed
1617

src/compas/datastructures/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
from .cell_network.cell_network import CellNetwork
166166

167167
from .tree.tree import Tree, TreeNode
168+
from .tree.hashtree import HashTree, HashNode
168169

169170
__all__ = [
170171
"Datastructure",
@@ -280,6 +281,8 @@
280281
# Trees
281282
"Tree",
282283
"TreeNode",
284+
"HashTree",
285+
"HashNode",
283286
]
284287

285288
if not compas.IPY:

src/compas/datastructures/tree/hashtree.py

Lines changed: 150 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,35 @@
44
import hashlib
55

66

7-
def sha256(content):
8-
return hashlib.sha256(json_dumps(content).encode()).hexdigest()
9-
10-
117
class 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

61100
class 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("\nCOMPARE 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("\nCOMPARE 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

Comments
 (0)