1*105f6285SAndroid Build Coastguard Worker# Copyright (C) 2020 The Android Open Source Project 2*105f6285SAndroid Build Coastguard Worker# 3*105f6285SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 4*105f6285SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 5*105f6285SAndroid Build Coastguard Worker# You may obtain a copy of the License at 6*105f6285SAndroid Build Coastguard Worker# 7*105f6285SAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 8*105f6285SAndroid Build Coastguard Worker# 9*105f6285SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 10*105f6285SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 11*105f6285SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*105f6285SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 13*105f6285SAndroid Build Coastguard Worker# limitations under the License. 14*105f6285SAndroid Build Coastguard Worker"""A library containing functions for diffing XML elements.""" 15*105f6285SAndroid Build Coastguard Workerimport textwrap 16*105f6285SAndroid Build Coastguard Workerfrom typing import Any, Callable, Dict, Set 17*105f6285SAndroid Build Coastguard Workerimport xml.etree.ElementTree as ET 18*105f6285SAndroid Build Coastguard Workerimport dataclasses 19*105f6285SAndroid Build Coastguard Worker 20*105f6285SAndroid Build Coastguard WorkerElement = ET.Element 21*105f6285SAndroid Build Coastguard Worker 22*105f6285SAndroid Build Coastguard Worker_INDENT = (' ' * 2) 23*105f6285SAndroid Build Coastguard Worker 24*105f6285SAndroid Build Coastguard Worker 25*105f6285SAndroid Build Coastguard Worker@dataclasses.dataclass 26*105f6285SAndroid Build Coastguard Workerclass Change: 27*105f6285SAndroid Build Coastguard Worker value_from: str 28*105f6285SAndroid Build Coastguard Worker value_to: str 29*105f6285SAndroid Build Coastguard Worker 30*105f6285SAndroid Build Coastguard Worker def __repr__(self): 31*105f6285SAndroid Build Coastguard Worker return f'{self.value_from} -> {self.value_to}' 32*105f6285SAndroid Build Coastguard Worker 33*105f6285SAndroid Build Coastguard Worker 34*105f6285SAndroid Build Coastguard Worker@dataclasses.dataclass 35*105f6285SAndroid Build Coastguard Workerclass ChangeMap: 36*105f6285SAndroid Build Coastguard Worker """A collection of changes broken down by added, removed and modified. 37*105f6285SAndroid Build Coastguard Worker 38*105f6285SAndroid Build Coastguard Worker Attributes: 39*105f6285SAndroid Build Coastguard Worker added: A dictionary of string identifiers to the added string. 40*105f6285SAndroid Build Coastguard Worker removed: A dictionary of string identifiers to the removed string. 41*105f6285SAndroid Build Coastguard Worker modified: A dictionary of string identifiers to the changed object. 42*105f6285SAndroid Build Coastguard Worker """ 43*105f6285SAndroid Build Coastguard Worker added: Dict[str, str] = dataclasses.field(default_factory=dict) 44*105f6285SAndroid Build Coastguard Worker removed: Dict[str, str] = dataclasses.field(default_factory=dict) 45*105f6285SAndroid Build Coastguard Worker modified: Dict[str, Any] = dataclasses.field(default_factory=dict) 46*105f6285SAndroid Build Coastguard Worker 47*105f6285SAndroid Build Coastguard Worker def __repr__(self): 48*105f6285SAndroid Build Coastguard Worker ret_str = '' 49*105f6285SAndroid Build Coastguard Worker if self.added: 50*105f6285SAndroid Build Coastguard Worker ret_str += 'Added:\n' 51*105f6285SAndroid Build Coastguard Worker for value in self.added.values(): 52*105f6285SAndroid Build Coastguard Worker ret_str += textwrap.indent(str(value) + '\n', _INDENT) 53*105f6285SAndroid Build Coastguard Worker if self.removed: 54*105f6285SAndroid Build Coastguard Worker ret_str += 'Removed:\n' 55*105f6285SAndroid Build Coastguard Worker for value in self.removed.values(): 56*105f6285SAndroid Build Coastguard Worker ret_str += textwrap.indent(str(value) + '\n', _INDENT) 57*105f6285SAndroid Build Coastguard Worker if self.modified: 58*105f6285SAndroid Build Coastguard Worker ret_str += 'Modified:\n' 59*105f6285SAndroid Build Coastguard Worker for name, value in self.modified.items(): 60*105f6285SAndroid Build Coastguard Worker ret_str += textwrap.indent(name + ':\n', _INDENT) 61*105f6285SAndroid Build Coastguard Worker ret_str += textwrap.indent(str(value) + '\n', _INDENT * 2) 62*105f6285SAndroid Build Coastguard Worker return ret_str 63*105f6285SAndroid Build Coastguard Worker 64*105f6285SAndroid Build Coastguard Worker def __bool__(self): 65*105f6285SAndroid Build Coastguard Worker return bool(self.added) or bool(self.removed) or bool(self.modified) 66*105f6285SAndroid Build Coastguard Worker 67*105f6285SAndroid Build Coastguard Worker 68*105f6285SAndroid Build Coastguard Workerdef element_string(e: Element) -> str: 69*105f6285SAndroid Build Coastguard Worker return ET.tostring(e).decode(encoding='UTF-8').strip() 70*105f6285SAndroid Build Coastguard Worker 71*105f6285SAndroid Build Coastguard Worker 72*105f6285SAndroid Build Coastguard Workerdef attribute_changes(e1: Element, e2: Element, 73*105f6285SAndroid Build Coastguard Worker ignored_attrs: Set[str]) -> ChangeMap: 74*105f6285SAndroid Build Coastguard Worker """Get the changes in attributes between two XML elements. 75*105f6285SAndroid Build Coastguard Worker 76*105f6285SAndroid Build Coastguard Worker Arguments: 77*105f6285SAndroid Build Coastguard Worker e1: the first xml element. 78*105f6285SAndroid Build Coastguard Worker e2: the second xml element. 79*105f6285SAndroid Build Coastguard Worker ignored_attrs: a set of attribute names to ignore changes. 80*105f6285SAndroid Build Coastguard Worker 81*105f6285SAndroid Build Coastguard Worker Returns: 82*105f6285SAndroid Build Coastguard Worker A ChangeMap of attribute changes. Keyed by attribute name. 83*105f6285SAndroid Build Coastguard Worker """ 84*105f6285SAndroid Build Coastguard Worker changes = ChangeMap() 85*105f6285SAndroid Build Coastguard Worker attributes = set(e1.keys()) | set(e2.keys()) 86*105f6285SAndroid Build Coastguard Worker for attr in attributes: 87*105f6285SAndroid Build Coastguard Worker if attr in ignored_attrs: 88*105f6285SAndroid Build Coastguard Worker continue 89*105f6285SAndroid Build Coastguard Worker a1 = e1.get(attr) 90*105f6285SAndroid Build Coastguard Worker a2 = e2.get(attr) 91*105f6285SAndroid Build Coastguard Worker if a1 == a2: 92*105f6285SAndroid Build Coastguard Worker continue 93*105f6285SAndroid Build Coastguard Worker elif not a1: 94*105f6285SAndroid Build Coastguard Worker changes.added[attr] = a2 or '' 95*105f6285SAndroid Build Coastguard Worker elif not a2: 96*105f6285SAndroid Build Coastguard Worker changes.removed[attr] = a1 97*105f6285SAndroid Build Coastguard Worker else: 98*105f6285SAndroid Build Coastguard Worker changes.modified[attr] = Change(value_from=a1, value_to=a2) 99*105f6285SAndroid Build Coastguard Worker return changes 100*105f6285SAndroid Build Coastguard Worker 101*105f6285SAndroid Build Coastguard Worker 102*105f6285SAndroid Build Coastguard Workerdef compare_subelements( 103*105f6285SAndroid Build Coastguard Worker tag: str, 104*105f6285SAndroid Build Coastguard Worker p1: Element, 105*105f6285SAndroid Build Coastguard Worker p2: Element, 106*105f6285SAndroid Build Coastguard Worker ignored_attrs: Set[str], 107*105f6285SAndroid Build Coastguard Worker key_fn: Callable[[Element], str], 108*105f6285SAndroid Build Coastguard Worker diff_fn: Callable[[Element, Element, Set[str]], Any]) -> ChangeMap: 109*105f6285SAndroid Build Coastguard Worker """Get the changes between subelements of two parent elements. 110*105f6285SAndroid Build Coastguard Worker 111*105f6285SAndroid Build Coastguard Worker Arguments: 112*105f6285SAndroid Build Coastguard Worker tag: tag name for children element. 113*105f6285SAndroid Build Coastguard Worker p1: the base parent xml element. 114*105f6285SAndroid Build Coastguard Worker p2: the parent xml element to compare 115*105f6285SAndroid Build Coastguard Worker ignored_attrs: a set of attribute names to ignore changes. 116*105f6285SAndroid Build Coastguard Worker key_fn: Function that takes a subelement and returns a key 117*105f6285SAndroid Build Coastguard Worker diff_fn: Function that take two subelements and a set of ignored 118*105f6285SAndroid Build Coastguard Worker attributes, returns the differences 119*105f6285SAndroid Build Coastguard Worker 120*105f6285SAndroid Build Coastguard Worker Returns: 121*105f6285SAndroid Build Coastguard Worker A ChangeMap object of the changes. 122*105f6285SAndroid Build Coastguard Worker """ 123*105f6285SAndroid Build Coastguard Worker changes = ChangeMap() 124*105f6285SAndroid Build Coastguard Worker group1 = {} 125*105f6285SAndroid Build Coastguard Worker for e1 in p1.findall(tag): 126*105f6285SAndroid Build Coastguard Worker group1[key_fn(e1)] = e1 127*105f6285SAndroid Build Coastguard Worker 128*105f6285SAndroid Build Coastguard Worker for e2 in p2.findall(tag): 129*105f6285SAndroid Build Coastguard Worker key = key_fn(e2) 130*105f6285SAndroid Build Coastguard Worker e1 = group1.pop(key, None) 131*105f6285SAndroid Build Coastguard Worker if e1 is None: 132*105f6285SAndroid Build Coastguard Worker changes.added[key] = element_string(e2) 133*105f6285SAndroid Build Coastguard Worker else: 134*105f6285SAndroid Build Coastguard Worker echange = diff_fn(e1, e2, ignored_attrs) 135*105f6285SAndroid Build Coastguard Worker if echange: 136*105f6285SAndroid Build Coastguard Worker changes.modified[key] = echange 137*105f6285SAndroid Build Coastguard Worker 138*105f6285SAndroid Build Coastguard Worker for name, e1 in group1.items(): 139*105f6285SAndroid Build Coastguard Worker changes.removed[name] = element_string(e1) 140*105f6285SAndroid Build Coastguard Worker 141*105f6285SAndroid Build Coastguard Worker return changes 142