"""
import json
+from typing import Sequence, Callable, Any, Tuple, cast as type_hints_pseudo_cast
from . import string
from .. import arnied_api
return max([c.instance for c in self]) if len(self) > 0 else -1
+class CnfDiff(list):
+ """A list of differences between :py:class:`BaseCnfList`s"""
+
+ def add_missing(self, cnf: BaseCnf, ancestry: Sequence[BaseCnf]):
+ self.append(("-", cnf, ancestry))
+
+ def add_excess(self, cnf: BaseCnf, ancestry: Sequence[BaseCnf]):
+ self.append(("+", cnf, ancestry))
+
+ def print(self, output_func: Callable[[str], Any] = print):
+ """
+ Create a string representation of this diff and "print" it, using given function
+
+ :param output_func: Function to use for printing
+ :return: Iterator over text lines
+ """
+ for diff_type, cnf, ancestry in self:
+ ancestral_string = ' > '.join(f"{anc.name}={anc.value}" for anc in ancestry)
+ output_func(f"cnf diff: {diff_type} {cnf.name} ({cnf.instance}) = {cnf.value!r} in "
+ + ancestral_string)
+
+
+class CnfCompareMixin(BaseCnfList):
+ """Mixin to add a `compare()` function."""
+
+ def compare(self, other: BaseCnfList, ignore_list: Sequence[str] = None,
+ ancestry: Tuple[BaseCnf, ...] = None) -> CnfDiff:
+ """
+ Compare this list of config variables to another list, return differences.
+
+ :param other: another list of configuration variables
+ :param ignore_list: names of variables to ignore
+ :param ancestry: when comparing recursively, we call this function on children (of children ...).
+ This is the "path" of parent cnf vars that lead to the list of children we
+ currently compare.
+ :return: difference between self and other
+ """
+ if ancestry is None:
+ ancestry = ()
+ if ignore_list is None:
+ ignore_list = ()
+ diff = CnfDiff()
+ # check whether all own values also appear in other config
+ for c_own in self:
+ c_own = type_hints_pseudo_cast(BaseCnf, c_own)
+ if c_own.name in ignore_list:
+ continue
+ c_other = other.where(lambda c: c_own == c)
+ if len(c_other) == 0:
+ diff.add_missing(c_own, ancestry)
+ elif len(c_other) == 1:
+ # found matching entry. Recurse into children
+ diff.extend(c_own.children.compare(c_other[0].children, ignore_list,
+ ancestry + (c_own, )))
+ else:
+ # several variables in other have the same name, value, parent, and instance as c_own?!
+ raise NotImplementedError("This should not be possible!")
+
+ # reverse check: all other values also appear in own config?
+ for c_other in other:
+ c_other = type_hints_pseudo_cast(BaseCnf, c_other)
+ if c_other.name in ignore_list:
+ continue
+ c_own = self.where(lambda c: c_other == c)
+ if len(c_own) == 0:
+ diff.add_excess(c_other, ancestry)
+ elif len(c_own) == 1:
+ pass # no need to descend into children again
+ else:
+ # several variables in self have the same name, value, parent, and instance as c_other?!
+ raise NotImplementedError("This should not be possible!")
+ return diff
+
###############################################################################
# PUBLIC CLASSES
###############################################################################
#
-class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin):
+class CnfList(CnfListSerializationMixin, CnfListArniedApiMixin, CnfListQueryingMixin, CnfCompareMixin):
"""Collection of Cnf variables."""
pass
pass
-__all__ = ["CnfList", "Cnf"]
+__all__ = ["CnfList", "Cnf", "CnfDiff"]
import json
from textwrap import dedent
from tempfile import NamedTemporaryFile
+from copy import deepcopy
-from src.cnfvar import CnfList
+from src.cnfvar import CnfList, CnfDiff
CNF_TEST_DATA = dedent("""\
1 USER,1: "jake"
.comment = "hey this is a comment"
+class TestCompare(unittest.TestCase):
+ """Test compare() function of BaseCnfList."""
+
+ DATA_STR = dedent("""\
+ 01 FRANCHISE,0: "Star Trek"
+ 02 (01) SERIES,0: "The Original Series"
+ 03 (02) SHIP,0: "Enterprise (original)"
+ 04 (03) CAPTAIN,0: "Pike"
+ 05 (03) CAPTAIN,1: "Kirk"
+ 06 (01) SERIES,1: "The Animated Series"
+ 07 (01) SERIES,1: "The Next Generation"
+ 08 (07) SHIP,0: "Enterprise D"
+ 09 (08) CAPTAIN,0: "Picard"
+ 11 (08) CAPTAIN,1: "Riker"
+ 12 (01) SERIES,2: "Deep Space Nine"
+ 13 (01) SERIES,3: "Voyager"
+ 14 (13) SHIP,0: "Voyager"
+ 15 (01) SERIES,4: "Enterprise"
+ 16 (15) SHIP,0: "Enterprise"
+ 17 (16) CAPTAIN,0: "Archer"
+ 18 FRANCHISE,1: "Star Wars"
+ 19 (18) MOVIE,0: "A New Hope"
+ 21 (19) EPISODE,0: "4"
+ 22 (18) MOVIE,1: "The Empire Strikes Back"
+ 23 (22) EPISODE,0: "5"
+ 24 (18) MOVIE,2: "Return of the Jedi"
+ 25 (24) EPISODE,0: "6"
+ 26 (18) MOVIE,3: "The Phantom Menace"
+ 27 (26) EPISODE,0: "1"
+ 28 (18) MOVIE,4: "Attack of the Clones"
+ 29 (28) EPISODE,0: "2"
+ 31 (18) MOVIE,5: "Revenge of the Sith"
+ 32 (31) EPISODE,0: "3"
+ 33 FRANCHISE,2: "Battlestar Galactica"
+ 34 FRANCHISE,3: "Firefly"
+ 35 FRANCHISE,4: "Babylon 5"
+ 36 FRANCHISE,5: "Star Gate"
+ """)
+
+ DATA: CnfList # initialized in setUpClass
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ """Called once before first test, converts above string to CnfList."""
+ cls.DATA = CnfList.from_cnf_string(cls.DATA_STR)
+
+ def test_equal(self):
+ """Compare data to itself."""
+ self.assertEqual(CnfDiff(), self.DATA.compare(self.DATA))
+
+ def test_remove(self):
+ """Remove entry from data, check it is found."""
+ other = deepcopy(self.DATA)
+ removed = other.single_with_value("Star Trek").children\
+ .single_with_value("The Original Series").children\
+ .single_with_value("Enterprise (original)").children \
+ .remove_where(lambda cnf: cnf.value == "Pike")
+ self.assertEqual(1, len(removed))
+ diff = self.DATA.compare(other)
+ self.assertEqual(1, len(diff))
+ diff_text = []
+ diff.print(output_func=lambda s: diff_text.append(s))
+ self.assertEqual(1, len(diff_text))
+ expect = "cnf diff: - captain (0) = 'Pike' " + \
+ "in franchise=Star Trek > series=The Original Series > ship=Enterprise (original)"
+ self.assertEqual(expect, diff_text[0])
+
+ def test_add(self):
+ """Add entry to data, check it is found."""
+ other = deepcopy(self.DATA)
+ other.single_with_value("Star Wars").children.single_with_value("The Phantom Menace")\
+ .add_child("CHARACTER", "Jar Jar Binks")
+ diff = self.DATA.compare(other)
+ self.assertEqual(1, len(diff))
+ diff_text = []
+ diff.print(output_func=lambda s: diff_text.append(s))
+ self.assertEqual(1, len(diff_text))
+ expect = "cnf diff: + CHARACTER (0) = 'Jar Jar Binks' " + \
+ "in franchise=Star Wars > movie=The Phantom Menace"
+ self.assertEqual(expect, diff_text[0])
+
+ def test_change(self):
+ """Change entry in data, check it is found."""
+ other = deepcopy(self.DATA)
+ other.single_with_value("Star Trek").children \
+ .single_with_value("The Next Generation").children \
+ .single_with_value("Enterprise D").children \
+ .single_with_value("Picard").value = "Jean-Luc"
+ diff = self.DATA.compare(other)
+ self.assertEqual(2, len(diff))
+ diff_text = []
+ diff.print(output_func=lambda s: diff_text.append(s))
+ self.assertEqual(2, len(diff_text))
+ expect = [
+ "cnf diff: - captain (0) = 'Picard' in franchise=Star Trek > "
+ "series=The Next Generation > ship=Enterprise D",
+ "cnf diff: + captain (0) = 'Jean-Luc' in franchise=Star Trek > "
+ "series=The Next Generation > ship=Enterprise D",
+ ]
+ self.assertEqual(expect, diff_text)
+
+ def test_ignore(self):
+ """Check that changes on ignore_list are ignored."""
+ other = deepcopy(self.DATA)
+ # same change as in test_change
+ other.single_with_value("Star Trek").children \
+ .single_with_value("The Next Generation").children \
+ .single_with_value("Enterprise D").children \
+ .single_with_value("Picard").value = "Jean-Luc"
+ diff = self.DATA.compare(other, ignore_list=["CAPTAIN",])
+ self.assertEqual(0, len(diff))
+
+
if __name__ == '__main__':
unittest.main()