# Copyright (C) 2018-'24 Frank Sachsenheim## This program is free software: you can redistribute it and/or modify# it under the terms of the GNU Affero General Public License as published# by the Free Software Foundation, either version 3 of the License, or# (at your option) any later version.## This program is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the# GNU Affero General Public License for more details.## You should have received a copy of the GNU Affero General Public License# along with this program. If not, see <https://www.gnu.org/licenses/>.from__future__importannotationsimportenumfromitertoolsimportzip_longestfrom_delb.exceptionsimportInvalidCodePathfrom_delb.nodesimportNodeBase,TagNodefrom_delb.utilsimport*# noqafrom_delb.utilsimport__all__# TODO increase test coverageclassTreeDifferenceKind(enum.Enum):None_=enum.auto()NodeContent=enum.auto()NodeType=enum.auto()TagAttributes=enum.auto()TagChildrenSize=enum.auto()TagLocalName=enum.auto()TagNamespace=enum.auto()
[docs]classTreesComparisonResult:""" Instances of this class describe one or no difference between two trees. Casting an instance to :class:`bool` will yield :obj:`True` when it describes no difference, thus the compared trees were equal. Casted to strings they're intended to support debugging. """def__init__(self,difference_kind:TreeDifferenceKind,lhn:NodeBase|None,rhn:NodeBase|None,):self.difference_kind=difference_kindself.lhn:NodeBase|None=lhnself.rhn:NodeBase|None=rhndef__bool__(self):returnself.difference_kindisTreeDifferenceKind.None_def__str__(self):ifself.difference_kindisTreeDifferenceKind.None_:return"Trees are equal."elifself.difference_kindin(TreeDifferenceKind.NodeContent,TreeDifferenceKind.NodeType,):returnself.__str_child()else:returnself.__str_tag()def__str_child(self)->str:assertself.lhnisnotNoneparent=self.lhn.parentifparentisNone:parent_msg_tail=":"else:parent_msg_tail=f", parent node has location_path {parent.location_path}:"ifself.difference_kindisTreeDifferenceKind.NodeContent:returnf"Nodes' content differ{parent_msg_tail}\n{self.lhn!r}\n{self.rhn!r}"else:# difference_kind is TreeDifferenceKind.NodeTypereturn(f"Nodes are of different type{parent_msg_tail} "f"{self.lhn.__class__} != {self.rhn.__class__}")def__str_tag(self)->str:assertisinstance(self.lhn,TagNode)assertisinstance(self.rhn,TagNode)ifself.difference_kindisTreeDifferenceKind.TagAttributes:return(f"Attributes of tag nodes at {self.lhn.location_path} differ:\n"f"{self.lhn.attributes}\n{self.rhn.attributes}")elifself.difference_kindisTreeDifferenceKind.TagChildrenSize:result=f"Child nodes of tag nodes at {self.lhn.location_path} differ:"fora,binzip_longest(self.lhn.iterate_children(),self.rhn.iterate_children(),fillvalue=None,):result+=f"\n\n{a!r}\n{b!r}"returnresultelifself.difference_kindisTreeDifferenceKind.TagLocalName:return(f"Local names of tag nodes at {self.lhn.location_path} differ: "f"{self.lhn.local_name} != {self.rhn.location_path}")elifself.difference_kindisTreeDifferenceKind.TagNamespace:return(f"Namespaces of tag nodes at {self.lhn.location_path} differ: "f"{self.lhn.namespace} != {self.rhn.namespace}")raiseInvalidCodePath()
[docs]defcompare_trees(lhr:NodeBase,rhr:NodeBase)->TreesComparisonResult:""" Compares two node trees for equality. Upon the first detection of a difference of nodes that are located at the same position within the compared (sub-)trees a mismatch is reported. :param lhr: The node that is considered as root of the left hand operand. :param rhr: The node that is considered as root of the right hand operand. :return: An object that contains information about the first or no difference. While node types that can't have descendants are comparable with a comparison expression, the :class:`TagNode` type deliberately doesn't implement the ``==`` operator, because it isn't clear whether a comparison should also consider the node's descendants as this function does. """ifnotisinstance(rhr,lhr.__class__):returnTreesComparisonResult(TreeDifferenceKind.NodeType,lhr,rhr)ifisinstance(lhr,TagNode):assertisinstance(rhr,TagNode)iflhr._namespace!=rhr._namespace:returnTreesComparisonResult(TreeDifferenceKind.TagNamespace,lhr,rhr)iflhr.local_name!=rhr.local_name:returnTreesComparisonResult(TreeDifferenceKind.TagLocalName,lhr,rhr)iflhr.attributes!=rhr.attributes:returnTreesComparisonResult(TreeDifferenceKind.TagAttributes,lhr,rhr)iflen(lhr)!=len(rhr):returnTreesComparisonResult(TreeDifferenceKind.TagChildrenSize,lhr,rhr)forlhn,rhninzip(lhr.iterate_children(),rhr.iterate_children()):result=compare_trees(lhn,rhn)ifnotresult:returnresulteliflhr!=rhr:returnTreesComparisonResult(TreeDifferenceKind.NodeContent,lhr,rhr)returnTreesComparisonResult(TreeDifferenceKind.None_,None,None)