"""Helper datastructures.These extend the ``treelib`` library ``Node`` and ``Tree`` classes by addingcomparisons and equality functionalities used to compare log trees."""fromtreelibimportNodeas_NodefromtreelibimportTreeas_Treefromversioned_collection.utils.data_structuresimporthashabledict
[docs]classTree(_Tree):def__init__(self,tree=None,deep=False,node_class=None,identifier=None):ifnode_classisNone:node_class=Nodesuper().__init__(tree,deep,node_class,identifier)def__hash__(self)->int:root:Node=self.get_node(self.root)_hash=""to_visit=[root]whilelen(to_visit):node=to_visit.pop(0)leading="0"ifnode.is_leaf(self._identifier)else"1"_hash+=leading+str(hash(node))to_visit.extend(sorted(self.children(node.identifier)))returnhash(_hash)def__eq__(self,other:object)->bool:ifnot(isinstance(other,Tree)orisinstance(other,_Tree)):returnFalseifotherisself:returnTrue# Different number of nodesiflen(self)!=len(other):returnFalseto_visit_here=[self.get_node(self.root)]to_visit_there=[other.get_node(other.root)]whilelen(to_visit_here)>0:iflen(to_visit_here)!=len(to_visit_there):returnFalsethis_node:Node=to_visit_here.pop(0)that_node:Node=to_visit_there.pop(0)# The nodes should be the sameif(this_node.identifier!=that_node.identifierorthis_node.tag!=that_node.tagorthis_node.data!=that_node.data):returnFalse# The nodes should have the same number of childrenthis_node_children=self.children(this_node.identifier)that_node_children=other.children(that_node.identifier)iflen(this_node_children)!=len(that_node_children):returnFalse# The children nodes should be the sameto_visit_here.extend(sorted(this_node_children))to_visit_there.extend(sorted(that_node_children))returnTruedef_is_subtree_of(self,other:object,strict:bool=True)->bool:ifnot(isinstance(other,Tree)orisinstance(other,_Tree)):op_str='<'ifstrictelse'<='raiseTypeError(f"{op_str} not supported between instances of "f"'Tree' and '{type(other)}'")ifotherisself:returnnotstrict# Different number of nodesiflen(self)>len(other):returnFalsethese_paths_to_leaves=self.paths_to_leaves()iflen(these_paths_to_leaves)>len(other.paths_to_leaves()):returnFalse# `self` has either the same or a lower number of branches as `other`these_leaves={p[-1]forpinthese_paths_to_leaves}# All leaves of `self` are nodes in `other`forthis_leafinthese_leaves:ifnotother.contains(this_leaf):returnFalsetrimmed=Falseother_copy=Tree(tree=other,deep=True)forleafinthese_leaves:other_node:Node=other_copy.get_node(leaf)# Other node should exist because otherwise we should have# returned ``False`` aboveassertother_nodeisnotNoneifother_node.is_leaf(other_copy.identifier):# Nothing to trimcontinueforother_childinother_copy.children(other_node.identifier):other_copy.remove_subtree(other_child.identifier)ifother_node.dataisnotNone:# This is ugly because it makes assumption about the data.# Only valid for log trees, but it's the only use case# anywayother_node.data.next.remove(other_child.tag)trimmed=True# `other_copy` is trimmed to the length of `self` for all branches of# `self` that are also branches of `other`. We still have to remove# those branches that are only in `other`.removed_branches=Falseforpathinother_copy.paths_to_leaves():ifpath[-1]inthese_leaves:# this has already been trimmedcontinueforother_node_idinpath:ifnotself.contains(other_node_id):other_copy.remove_subtree(other_node_id)removed_branches=Truebreak# `other_copy` is trimmed to the length of `self`. At this point,# `self` is a subtree of `other` if `self` equals `other_copy`ifself!=other_copy:returnFalseifstrict:returntrimmedorremoved_branchesreturnTruedef__lt__(self,other:object)->bool:returnself._is_subtree_of(other,strict=True)def__le__(self,other:object)->bool:returnself._is_subtree_of(other,strict=False)