You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
142 lines
4.2 KiB
142 lines
4.2 KiB
# Copyright (C) 2020 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""A library containing functions for diffing XML elements."""
|
|
import textwrap
|
|
from typing import Any, Callable, Dict, Set
|
|
import xml.etree.ElementTree as ET
|
|
import dataclasses
|
|
|
|
Element = ET.Element
|
|
|
|
_INDENT = (' ' * 2)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class Change:
|
|
value_from: str
|
|
value_to: str
|
|
|
|
def __repr__(self):
|
|
return f'{self.value_from} -> {self.value_to}'
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class ChangeMap:
|
|
"""A collection of changes broken down by added, removed and modified.
|
|
|
|
Attributes:
|
|
added: A dictionary of string identifiers to the added string.
|
|
removed: A dictionary of string identifiers to the removed string.
|
|
modified: A dictionary of string identifiers to the changed object.
|
|
"""
|
|
added: Dict[str, str] = dataclasses.field(default_factory=dict)
|
|
removed: Dict[str, str] = dataclasses.field(default_factory=dict)
|
|
modified: Dict[str, Any] = dataclasses.field(default_factory=dict)
|
|
|
|
def __repr__(self):
|
|
ret_str = ''
|
|
if self.added:
|
|
ret_str += 'Added:\n'
|
|
for value in self.added.values():
|
|
ret_str += textwrap.indent(str(value) + '\n', _INDENT)
|
|
if self.removed:
|
|
ret_str += 'Removed:\n'
|
|
for value in self.removed.values():
|
|
ret_str += textwrap.indent(str(value) + '\n', _INDENT)
|
|
if self.modified:
|
|
ret_str += 'Modified:\n'
|
|
for name, value in self.modified.items():
|
|
ret_str += textwrap.indent(name + ':\n', _INDENT)
|
|
ret_str += textwrap.indent(str(value) + '\n', _INDENT * 2)
|
|
return ret_str
|
|
|
|
def __bool__(self):
|
|
return bool(self.added) or bool(self.removed) or bool(self.modified)
|
|
|
|
|
|
def element_string(e: Element) -> str:
|
|
return ET.tostring(e).decode(encoding='UTF-8').strip()
|
|
|
|
|
|
def attribute_changes(e1: Element, e2: Element,
|
|
ignored_attrs: Set[str]) -> ChangeMap:
|
|
"""Get the changes in attributes between two XML elements.
|
|
|
|
Arguments:
|
|
e1: the first xml element.
|
|
e2: the second xml element.
|
|
ignored_attrs: a set of attribute names to ignore changes.
|
|
|
|
Returns:
|
|
A ChangeMap of attribute changes. Keyed by attribute name.
|
|
"""
|
|
changes = ChangeMap()
|
|
attributes = set(e1.keys()) | set(e2.keys())
|
|
for attr in attributes:
|
|
if attr in ignored_attrs:
|
|
continue
|
|
a1 = e1.get(attr)
|
|
a2 = e2.get(attr)
|
|
if a1 == a2:
|
|
continue
|
|
elif not a1:
|
|
changes.added[attr] = a2 or ''
|
|
elif not a2:
|
|
changes.removed[attr] = a1
|
|
else:
|
|
changes.modified[attr] = Change(value_from=a1, value_to=a2)
|
|
return changes
|
|
|
|
|
|
def compare_subelements(
|
|
tag: str,
|
|
p1: Element,
|
|
p2: Element,
|
|
ignored_attrs: Set[str],
|
|
key_fn: Callable[[Element], str],
|
|
diff_fn: Callable[[Element, Element, Set[str]], Any]) -> ChangeMap:
|
|
"""Get the changes between subelements of two parent elements.
|
|
|
|
Arguments:
|
|
tag: tag name for children element.
|
|
p1: the base parent xml element.
|
|
p2: the parent xml element to compare
|
|
ignored_attrs: a set of attribute names to ignore changes.
|
|
key_fn: Function that takes a subelement and returns a key
|
|
diff_fn: Function that take two subelements and a set of ignored
|
|
attributes, returns the differences
|
|
|
|
Returns:
|
|
A ChangeMap object of the changes.
|
|
"""
|
|
changes = ChangeMap()
|
|
group1 = {}
|
|
for e1 in p1.findall(tag):
|
|
group1[key_fn(e1)] = e1
|
|
|
|
for e2 in p2.findall(tag):
|
|
key = key_fn(e2)
|
|
e1 = group1.pop(key, None)
|
|
if e1 is None:
|
|
changes.added[key] = element_string(e2)
|
|
else:
|
|
echange = diff_fn(e1, e2, ignored_attrs)
|
|
if echange:
|
|
changes.modified[key] = echange
|
|
|
|
for name, e1 in group1.items():
|
|
changes.removed[name] = element_string(e1)
|
|
|
|
return changes
|