Source code for delb.utils

# Copyright (C) 2018-'25  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__ import annotations

import enum
from itertools import zip_longest
from typing import TYPE_CHECKING, Final

from _delb.exceptions import InvalidCodePath
from _delb.typing import TagNodeType
from _delb.utils import *  # noqa
from _delb.utils import __all__

if TYPE_CHECKING:
    from _delb.typing import XMLNodeType


class TreeDifferenceKind(enum.Enum):
    None_ = enum.auto()
    NodeContent = enum.auto()
    NodeType = enum.auto()
    TagAttributes = enum.auto()
    TagChildrenSize = enum.auto()
    TagLocalName = enum.auto()
    TagNamespace = enum.auto()


[docs] class TreesComparisonResult: """ 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: XMLNodeType | None, rhn: XMLNodeType | None, ): self.difference_kind: Final = difference_kind self.lhn: Final[XMLNodeType | None] = lhn self.rhn: Final[XMLNodeType | None] = rhn def __bool__(self): return self.difference_kind is TreeDifferenceKind.None_ def __str__(self): if self.difference_kind is TreeDifferenceKind.None_: return "Trees are equal." elif self.difference_kind in ( TreeDifferenceKind.NodeContent, TreeDifferenceKind.NodeType, ): return self.__str_child() else: return self.__str_tag() def __str_child(self) -> str: assert self.lhn is not None if not isinstance(self.lhn._parent, TagNodeType): parent_msg_tail = ":" else: parent_msg_tail = ( f", parent node has location_path {self.lhn._parent.location_path}:" ) if self.difference_kind is TreeDifferenceKind.NodeContent: return f"Nodes' content differ{parent_msg_tail}\n{self.lhn!r}\n{self.rhn!r}" else: # difference_kind is TreeDifferenceKind.NodeType return ( f"Nodes are of different type{parent_msg_tail} " f"{self.lhn.__class__} != {self.rhn.__class__}" ) def __str_tag(self) -> str: assert isinstance(self.lhn, TagNodeType) assert isinstance(self.rhn, TagNodeType) if self.difference_kind is TreeDifferenceKind.TagAttributes: return ( f"Attributes of tag nodes at {self.lhn.location_path} differ:\n" f"{self.lhn.attributes}\n{self.rhn.attributes}" ) elif self.difference_kind is TreeDifferenceKind.TagChildrenSize: result = f"Child nodes of tag nodes at {self.lhn.location_path} differ:" for a, b in zip_longest( self.lhn.iterate_children(), self.rhn.iterate_children(), fillvalue=None, ): result += f"\n\n{a!r}\n{b!r}" return result elif self.difference_kind is TreeDifferenceKind.TagLocalName: return ( f"Local names of tag nodes at {self.lhn.location_path} differ: " f"{self.lhn.local_name} != {self.rhn.location_path}" ) elif self.difference_kind is TreeDifferenceKind.TagNamespace: return ( f"Namespaces of tag nodes at {self.lhn.location_path} differ: " f"{self.lhn.namespace} != {self.rhn.namespace}" ) raise InvalidCodePath()
[docs] def compare_trees(lhr: XMLNodeType, rhr: XMLNodeType) -> 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:`delb.nodes.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. """ if not isinstance(rhr, lhr.__class__): return TreesComparisonResult(TreeDifferenceKind.NodeType, lhr, rhr) if isinstance(lhr, TagNodeType): assert isinstance(rhr, TagNodeType) if lhr.namespace != rhr.namespace: return TreesComparisonResult(TreeDifferenceKind.TagNamespace, lhr, rhr) if lhr.local_name != rhr.local_name: return TreesComparisonResult(TreeDifferenceKind.TagLocalName, lhr, rhr) if lhr.attributes != rhr.attributes: return TreesComparisonResult(TreeDifferenceKind.TagAttributes, lhr, rhr) if len(lhr) != len(rhr): return TreesComparisonResult(TreeDifferenceKind.TagChildrenSize, lhr, rhr) for lhn, rhn in zip(lhr.iterate_children(), rhr.iterate_children()): result = compare_trees(lhn, rhn) if not result: return result elif lhr != rhr: return TreesComparisonResult(TreeDifferenceKind.NodeContent, lhr, rhr) return TreesComparisonResult(TreeDifferenceKind.None_, None, None)
__all__ = __all__ + (compare_trees.__name__,)