diff --git a/changelog/59017.fixed b/changelog/59017.fixed new file mode 100644 index 000000000000..a34acd3429de --- /dev/null +++ b/changelog/59017.fixed @@ -0,0 +1 @@ +Fixed RecursiveDictDiffer with added nested dicts diff --git a/salt/utils/dictdiffer.py b/salt/utils/dictdiffer.py index e78b1518aba1..adac0d4b4aba 100644 --- a/salt/utils/dictdiffer.py +++ b/salt/utils/dictdiffer.py @@ -173,7 +173,7 @@ def _get_diffs(cls, dict1, dict2, ignore_missing_keys): Simple compares are done on lists """ ret_dict = {} - for p in dict1.keys(): + for p in dict1: if p not in dict2: ret_dict.update({p: {"new": dict1[p], "old": cls.NONE_VALUE}}) elif dict1[p] != dict2[p]: @@ -186,8 +186,8 @@ def _get_diffs(cls, dict1, dict2, ignore_missing_keys): else: ret_dict.update({p: {"new": dict1[p], "old": dict2[p]}}) if not ignore_missing_keys: - for p in dict2.keys(): - if p not in dict1.keys(): + for p in dict2: + if p not in dict1: ret_dict.update({p: {"new": cls.NONE_VALUE, "old": dict2[p]}}) return ret_dict @@ -200,8 +200,8 @@ def _get_values(cls, diff_dict, type="new"): Which values to return, 'new' or 'old' """ ret_dict = {} - for p in diff_dict.keys(): - if type in diff_dict[p].keys(): + for p in diff_dict: + if type in diff_dict[p]: ret_dict.update({p: diff_dict[p][type]}) else: ret_dict.update({p: cls._get_values(diff_dict[p], type=type)}) @@ -215,101 +215,172 @@ def _get_changes(cls, diff_dict): Each inner difference is tabulated two space deeper """ changes_strings = [] - for p in sorted(diff_dict.keys()): - if sorted(diff_dict[p].keys()) == ["new", "old"]: - # Some string formatting - old_value = diff_dict[p]["old"] - if diff_dict[p]["old"] == cls.NONE_VALUE: - old_value = "nothing" - elif isinstance(diff_dict[p]["old"], str): - old_value = "'{}'".format(diff_dict[p]["old"]) - elif isinstance(diff_dict[p]["old"], list): - old_value = "'{}'".format(", ".join(diff_dict[p]["old"])) - new_value = diff_dict[p]["new"] - if diff_dict[p]["new"] == cls.NONE_VALUE: - new_value = "nothing" - elif isinstance(diff_dict[p]["new"], str): - new_value = "'{}'".format(diff_dict[p]["new"]) - elif isinstance(diff_dict[p]["new"], list): - new_value = "'{}'".format(", ".join(diff_dict[p]["new"])) + for p in sorted(diff_dict): + if set(diff_dict[p]) == {"new", "old"}: + changes = { + "old_value": diff_dict[p]["old"], + "new_value": diff_dict[p]["new"], + } + for ref in ("old_value", "new_value"): + val = changes[ref] + # Some string formatting + if val == cls.NONE_VALUE: + changes[ref] = "nothing" + elif isinstance(val, str): + changes[ref] = f"'{val}'" + elif isinstance(val, list): + changes[ref] = f"'{', '.join(val)}'" changes_strings.append( - "{} from {} to {}".format(p, old_value, new_value) + f"{p} from {changes['old_value']} to {changes['new_value']}" ) else: sub_changes = cls._get_changes(diff_dict[p]) if sub_changes: - changes_strings.append("{}:".format(p)) - changes_strings.extend([" {}".format(c) for c in sub_changes]) + changes_strings.append(f"{p}:") + changes_strings.extend([f" {c}" for c in sub_changes]) return changes_strings - def added(self): + def _it_addrm( + self, + key_a, + key_b, + include_nested=False, + diffs=None, + prefix="", + is_nested=False, + separator=".", + ): + keys = [] + if diffs is None: + diffs = self.diffs + + for key in diffs: + if is_nested: + keys.append(f"{prefix}{key}") + + if not isinstance(diffs[key], dict): + continue + + if is_nested and include_nested: + keys.extend( + self._it_addrm( + key_a, + key_b, + diffs=diffs[key], + prefix=f"{prefix}{key}{separator}", + is_nested=is_nested, + include_nested=include_nested, + separator=separator, + ) + ) + elif "old" not in diffs[key]: + keys.extend( + self._it_addrm( + key_a, + key_b, + diffs=diffs[key], + prefix=f"{prefix}{key}{separator}", + is_nested=is_nested, + include_nested=include_nested, + separator=separator, + ) + ) + elif diffs[key][key_a] == self.NONE_VALUE: + keys.append(f"{prefix}{key}") + + if isinstance(diffs[key][key_b], dict) and include_nested: + keys.extend( + self._it_addrm( + key_a, + key_b, + diffs=diffs[key][key_b], + is_nested=True, + prefix=f"{prefix}{key}{separator}", + include_nested=include_nested, + separator=separator, + ) + ) + # type change from/to dict + elif ( + not is_nested + and not isinstance(diffs[key][key_a], dict) + and isinstance(diffs[key][key_b], dict) + ): + keys.extend( + self._it_addrm( + key_a, + key_b, + diffs=diffs[key][key_b], + is_nested=True, + prefix=f"{prefix}{key}{separator}", + include_nested=include_nested, + separator=separator, + ) + ) + + return keys + + def added( + self, include_nested=False, separator="." + ): # pylint: disable=arguments-differ """ Returns all keys that have been added. - If the keys are in child dictionaries they will be represented with - . notation - """ + include_nested + If an added key contains a dictionary, include its + keys in dot notation as well. Defaults to false. - def _added(diffs, prefix): - keys = [] - for key in diffs.keys(): - if isinstance(diffs[key], dict) and "old" not in diffs[key]: - keys.extend(_added(diffs[key], prefix="{}{}.".format(prefix, key))) - elif diffs[key]["old"] == self.NONE_VALUE: - if isinstance(diffs[key]["new"], dict): - keys.extend( - _added( - diffs[key]["new"], prefix="{}{}.".format(prefix, key) - ) - ) - else: - keys.append("{}{}".format(prefix, key)) - return keys + .. versionadded:: 3006 - return sorted(_added(self._diffs, prefix="")) + separator + Separator used to indicate nested keys. Defaults to ``.``. - def removed(self): + .. versionadded:: 3006 """ - Returns all keys that have been removed. + return sorted(self._it_addrm("old", "new", include_nested, separator=separator)) - If the keys are in child dictionaries they will be represented with - . notation + def removed( + self, include_nested=False, separator="." + ): # pylint: disable=arguments-differ """ + Returns all keys that have been removed. - def _removed(diffs, prefix): - keys = [] - for key in diffs.keys(): - if isinstance(diffs[key], dict) and "old" not in diffs[key]: - keys.extend( - _removed(diffs[key], prefix="{}{}.".format(prefix, key)) - ) - elif diffs[key]["new"] == self.NONE_VALUE: - keys.append("{}{}".format(prefix, key)) - elif isinstance(diffs[key]["new"], dict): - keys.extend( - _removed(diffs[key]["new"], prefix="{}{}.".format(prefix, key)) - ) - return keys + include_nested + If an added key contains a dictionary, include its + keys in dot notation as well. Defaults to false. - return sorted(_removed(self._diffs, prefix="")) + .. versionadded:: 3006 - def changed(self): + separator + Separator used to indicate nested keys. Defaults to ``.``. + + .. versionadded:: 3006 + """ + return sorted(self._it_addrm("new", "old", include_nested, separator=separator)) + + def changed(self, separator="."): # pylint: disable=arguments-differ """ Returns all keys that have been changed. - If the keys are in child dictionaries they will be represented with - . notation + separator + Separator used to indicate nested keys. Defaults to ``.``. + + .. versionadded:: 3006 """ - def _changed(diffs, prefix): + def _changed(diffs, prefix, separator): keys = [] - for key in diffs.keys(): + for key in diffs: if not isinstance(diffs[key], dict): continue if isinstance(diffs[key], dict) and "old" not in diffs[key]: keys.extend( - _changed(diffs[key], prefix="{}{}.".format(prefix, key)) + _changed( + diffs[key], + prefix=f"{prefix}{key}{separator}", + separator=separator, + ) ) continue if self.ignore_unset_values: @@ -323,14 +394,19 @@ def _changed(diffs, prefix): keys.extend( _changed( diffs[key]["new"], - prefix="{}{}.".format(prefix, key), + prefix=f"{prefix}{key}{separator}", + separator=separator, ) ) else: - keys.append("{}{}".format(prefix, key)) + keys.append(f"{prefix}{key}") elif isinstance(diffs[key], dict): keys.extend( - _changed(diffs[key], prefix="{}{}.".format(prefix, key)) + _changed( + diffs[key], + prefix=f"{prefix}{key}{separator}", + separator=separator, + ) ) else: if "old" in diffs[key] and "new" in diffs[key]: @@ -338,49 +414,58 @@ def _changed(diffs, prefix): keys.extend( _changed( diffs[key]["new"], - prefix="{}{}.".format(prefix, key), + prefix=f"{prefix}{key}{separator}", + separator=separator, ) ) else: - keys.append("{}{}".format(prefix, key)) + keys.append(f"{prefix}{key}") elif isinstance(diffs[key], dict): keys.extend( - _changed(diffs[key], prefix="{}{}.".format(prefix, key)) + _changed( + diffs[key], + prefix=f"{prefix}{key}{separator}", + separator=separator, + ) ) return keys - return sorted(_changed(self._diffs, prefix="")) + return sorted(_changed(self._diffs, prefix="", separator=separator)) - def unchanged(self): + def unchanged(self, separator="."): # pylint: disable=arguments-differ """ Returns all keys that have been unchanged. - If the keys are in child dictionaries they will be represented with - . notation + separator + Separator used to indicate nested keys. Defaults to ``.``. + + .. versionadded:: 3006 """ - def _unchanged(current_dict, diffs, prefix): + def _unchanged(current_dict, diffs, prefix, separator): keys = [] - for key in current_dict.keys(): + for key in current_dict: if key not in diffs: - keys.append("{}{}".format(prefix, key)) + keys.append(f"{prefix}{key}") elif isinstance(current_dict[key], dict): if "new" in diffs[key]: # There is a diff continue - else: - keys.extend( - _unchanged( - current_dict[key], - diffs[key], - prefix="{}{}.".format(prefix, key), - ) + keys.extend( + _unchanged( + current_dict[key], + diffs[key], + prefix=f"{prefix}{key}{separator}", + separator=separator, ) + ) return keys - return sorted(_unchanged(self.current_dict, self._diffs, prefix="")) + return sorted( + _unchanged(self.current_dict, self._diffs, prefix="", separator=separator) + ) @property def diffs(self): diff --git a/tests/pytests/unit/utils/test_dictdiffer.py b/tests/pytests/unit/utils/test_dictdiffer.py new file mode 100644 index 000000000000..bef363b81f21 --- /dev/null +++ b/tests/pytests/unit/utils/test_dictdiffer.py @@ -0,0 +1,319 @@ +import pytest + +import salt.utils.dictdiffer as dictdiffer + +NONE = dictdiffer.RecursiveDictDiffer.NONE_VALUE +IGNORE_MISSING = True + + +@pytest.fixture +def differ(request): + old, new, *ignore_missing = request.param + try: + ignore_missing = bool(ignore_missing.pop(0)) + except IndexError: + ignore_missing = False + return dictdiffer.RecursiveDictDiffer(old, new, ignore_missing) + + +@pytest.mark.parametrize("separator", [None, ":"]) +@pytest.mark.parametrize( + "differ,include_nested,expected", + [ + (({"a": "a"}, {"a": "a", "b": "b"}), False, ["b"]), + (({"a": "a"}, {"a": "a", "b": "b"}), True, ["b"]), + (({"a": "a"}, {"a": "a", "b": None}), False, ["b"]), + (({"a": {}}, {"a": {"b": "b"}}), False, ["a.b"]), + (({"a": {}}, {"a": {"b": {}}}), False, ["a.b"]), + (({"a": {}}, {"a": {"b": None}}), False, ["a.b"]), + (({"a": "a"}, {"a": "a", "b": {}}), False, ["b"]), + (({"a": "a"}, {"a": "a", "b": {"c": "c"}}), False, ["b"]), + (({"a": "a"}, {"a": {"c": "c"}}), False, ["a.c"]), + (({"a": {}}, {"a": {"b": {"c": "c"}}}), False, ["a.b"]), + (({"a": {}}, {"a": {"b": {"c": "c"}}}), True, ["a.b", "a.b.c"]), + ( + ({"a": {}}, {"a": {"b": {"c": {"d": "d"}}}}), + True, + ["a.b", "a.b.c", "a.b.c.d"], + ), + ], + indirect=["differ"], +) +def test_added(differ, include_nested, expected, separator): + if separator: + expected = [x.replace(".", separator) for x in expected] + assert ( + differ.added(separator=separator, include_nested=include_nested) == expected + ) + else: + assert differ.added(include_nested=include_nested) == expected + + +@pytest.mark.parametrize("separator", [None, ":"]) +@pytest.mark.parametrize( + "differ,expected", + [ + (({"a": "a", "unchanged": True}, {"a": "b", "unchanged": True}), ["a"]), + (({"a": "a"}, {"a": None}), ["a"]), + ( + ( + {"a": {"b": "b", "unchanged": True}}, + {"a": {"b": "c", "unchanged": True}}, + ), + ["a.b"], + ), + (({"a": {"b": "b"}}, {"a": {"b": None}}), ["a.b"]), + (({"a": "a"}, {"a": "a", "b": "b"}), []), + (({"a": {}}, {"a": {"b": "b"}}), []), + (({"a": "a", "b": "b"}, {"a": "a"}), []), + (({"a": {"b": "b"}}, {"a": {}}), []), + ], + indirect=["differ"], +) +def test_changed(differ, expected, separator): + if separator: + expected = [x.replace(".", separator) for x in expected] + assert differ.changed(separator=separator) == expected + else: + assert differ.changed() == expected + + +@pytest.mark.parametrize( + "differ,expected", + [ + (({"a": "a", "unchanged": True}, {"a": "b", "unchanged": True}), ["a"]), + (({"a": "a"}, {"a": None}), ["a"]), + ( + ( + {"a": {"b": "b", "unchanged": True}}, + {"a": {"b": "c", "unchanged": True}}, + ), + ["a.b"], + ), + (({"a": {"b": "b"}}, {"a": {"b": None}}), ["a.b"]), + (({"a": "a"}, {"a": "a", "b": "b"}), ["b"]), + (({"a": {}}, {"a": {"b": "b"}}), ["a.b"]), + (({"a": "a", "b": "b"}, {"a": "a"}), ["b"]), + (({"a": {"b": "b"}}, {"a": {}}), ["a.b"]), + ], + indirect=["differ"], +) +def test_changed_without_ignore_unset_values(differ, expected): + differ.ignore_unset_values = False + assert differ.changed() == expected + + +@pytest.mark.parametrize("separator", [None, ":"]) +@pytest.mark.parametrize( + "differ,include_nested,expected", + [ + (({"a": "a", "b": "b"}, {"a": "a"}), False, ["b"]), + (({"a": "a", "b": "b"}, {"a": "a"}), True, ["b"]), + (({"a": "a", "b": None}, {"a": "a"}), False, ["b"]), + (({"a": {"b": "b"}}, {"a": {}}), False, ["a.b"]), + (({"a": {"b": {}}}, {"a": {}}), False, ["a.b"]), + (({"a": {"b": None}}, {"a": {}}), False, ["a.b"]), + (({"a": "a", "b": {}}, {"a": "a"}), False, ["b"]), + (({"a": "a", "b": {"c": "c"}}, {"a": "a"}), False, ["b"]), + (({"a": "z", "b": {"c": "c"}}, {"a": "a"}), False, ["b"]), + (({"a": {}, "b": {"c": "c"}}, {"a": {"z": {"y": "y"}}}), False, ["b"]), + (({"a": {"b": "b"}}, {"a": None}), False, ["a.b"]), + ( + ({"a": {"b": {"c": {"d": "d"}}}}, {"a": {}}), + True, + ["a.b", "a.b.c", "a.b.c.d"], + ), + ], + indirect=["differ"], +) +def test_removed(differ, include_nested, expected, separator): + if separator: + expected = [x.replace(".", separator) for x in expected] + assert ( + differ.removed(separator=separator, include_nested=include_nested) + == expected + ) + else: + assert differ.removed(include_nested=include_nested) == expected + + +@pytest.mark.parametrize("separator", [None, ":"]) +@pytest.mark.parametrize( + "differ,expected", + [ + ( + ({"a": "a", "unchanged": True}, {"a": "b", "unchanged": True}), + ["unchanged"], + ), + (({"a": "a"}, {"a": None}), []), + ( + ( + {"a": {"b": "b", "unchanged": True}}, + {"a": {"b": "c", "unchanged": True}}, + ), + ["a.unchanged"], + ), + (({"a": {"b": "b"}}, {"a": {"b": None}}), []), + (({"a": {}}, {"a": {"b": "b"}}), []), + (({"a": "a", "b": "b"}, {"a": "a"}), ["a"]), + (({"a": {"b": "b"}}, {"a": {}}), []), + ], + indirect=["differ"], +) +def test_unchanged(differ, expected, separator): + if separator: + expected = [x.replace(".", separator) for x in expected] + assert differ.unchanged(separator=separator) == expected + else: + assert differ.unchanged() == expected + + +@pytest.mark.parametrize( + "differ,expected", + [ + (({"a": "a"}, {"a": "a", "b": "b"}), {"b": {"old": NONE, "new": "b"}}), + ( + ({"a": "a"}, {"a": "a", "b": "b"}, IGNORE_MISSING), + {"b": {"old": NONE, "new": "b"}}, + ), + ( + ({"a": "a"}, {"a": "a", "b": {"c": "c"}}), + {"b": {"old": NONE, "new": {"c": "c"}}}, + ), + ( + ({"a": "a"}, {"a": "a", "b": {"c": "c"}}, IGNORE_MISSING), + {"b": {"old": NONE, "new": {"c": "c"}}}, + ), + ( + ({"a": {}}, {"a": {"b": "b"}}), + {"a": {"b": {"old": NONE, "new": "b"}}}, + ), + ( + ({"a": "a", "unchanged": True}, {"a": "b", "unchanged": True}), + {"a": {"old": "a", "new": "b"}}, + ), + ( + ( + {"a": "a", "unchanged": True}, + {"a": "b", "unchanged": True}, + IGNORE_MISSING, + ), + {"a": {"old": "a", "new": "b"}}, + ), + ( + ( + {"a": {"b": "b", "unchanged": True}}, + {"a": {"b": "c", "unchanged": True}}, + ), + {"a": {"b": {"old": "b", "new": "c"}}}, + ), + (({"a": "a", "b": "b"}, {"a": "a"}), {"b": {"old": "b", "new": NONE}}), + (({"a": "a", "b": "b"}, {"a": "a"}, IGNORE_MISSING), {}), + ( + ({"a": {"b": "b"}}, {"a": {}}), + {"a": {"b": {"old": "b", "new": NONE}}}, + ), + (({"a": {"b": "b"}}, {"a": {}}, IGNORE_MISSING), {}), + ( + ({"a": "a", "b": {"c": "c"}}, {"a": "a"}), + {"b": {"old": {"c": "c"}, "new": NONE}}, + ), + (({"a": "a", "b": {"c": "c"}}, {"a": "a"}, IGNORE_MISSING), {}), + ], + indirect=["differ"], +) +def test_diffs(differ, expected): + assert differ.diffs == expected + + +@pytest.mark.parametrize( + "differ,expected", + [ + (({"a": "a"}, {"a": "a", "b": "b"}), {"b": "b"}), + (({"a": "a"}, {"a": "a", "b": {"c": "c"}}), {"b": {"c": "c"}}), + (({"a": {}}, {"a": {"b": "b"}}), {"a": {"b": "b"}}), + ( + ({"a": "a", "unchanged": True}, {"a": "b", "unchanged": True}), + {"a": "b"}, + ), + ( + ( + {"a": {"b": "b", "unchanged": True}}, + {"a": {"b": "c", "unchanged": True}}, + ), + {"a": {"b": "c"}}, + ), + (({"a": "a", "b": "b"}, {"a": "a"}), {"b": NONE}), + (({"a": {"b": "b"}}, {"a": {}}), {"a": {"b": NONE}}), + (({"a": "a", "b": {"c": "c"}}, {"a": "a"}), {"b": NONE}), + ], + indirect=["differ"], +) +def test_new_values(differ, expected): + assert differ.new_values == expected + + +@pytest.mark.parametrize( + "differ,expected", + [ + (({"a": "a"}, {"a": "a", "b": "b"}), {"b": NONE}), + (({"a": "a"}, {"a": "a", "b": {"c": "c"}}), {"b": NONE}), + (({"a": {}}, {"a": {"b": "b"}}), {"a": {"b": NONE}}), + ( + ({"a": "a", "unchanged": True}, {"a": "b", "unchanged": True}), + {"a": "a"}, + ), + ( + ( + {"a": {"b": "b", "unchanged": True}}, + {"a": {"b": "c", "unchanged": True}}, + ), + {"a": {"b": "b"}}, + ), + (({"a": "a", "b": "b"}, {"a": "a"}), {"b": "b"}), + (({"a": {"b": "b"}}, {"a": {}}), {"a": {"b": "b"}}), + (({"a": "a", "b": {"c": "c"}}, {"a": "a"}), {"b": {"c": "c"}}), + ], + indirect=["differ"], +) +def test_old_values(differ, expected): + assert differ.old_values == expected + + +@pytest.mark.parametrize( + "differ,expected", + [ + (({"a": "a"}, {"a": "a", "b": "b"}), "b from nothing to 'b'"), + ( + ({"a": "a"}, {"a": "a", "b": {"c": "c"}}), + "b from nothing to {'c': 'c'}", + ), + (({"a": {}}, {"a": {"b": "b"}}), "a:\n b from nothing to 'b'"), + ( + ({"a": "a", "unchanged": True}, {"a": "b", "unchanged": True}), + "a from 'a' to 'b'", + ), + ( + ( + {"a": {"b": "b", "unchanged": True}}, + {"a": {"b": "c", "unchanged": True}}, + ), + "a:\n b from 'b' to 'c'", + ), + (({"a": "a", "b": "b"}, {"a": "a"}), "b from 'b' to nothing"), + (({"a": {"b": "b"}}, {"a": {}}), "a:\n b from 'b' to nothing"), + ( + ({"a": "a", "b": {"c": "c"}}, {"a": "a"}), + "b from {'c': 'c'} to nothing", + ), + ( + ({"a": {"b": "b"}, "c": "c"}, {"a": {}, "c": "d"}), + "a:\n b from 'b' to nothing\nc from 'c' to 'd'", + ), + (({"a": []}, {"a": ["b", "c"]}), "a from '' to 'b, c'"), + (({"a": ["b", "c"]}, {"a": []}), "a from 'b, c' to ''"), + ], + indirect=["differ"], +) +def test_changes_str(differ, expected): + assert differ.changes_str == expected diff --git a/tests/unit/utils/test_dictdiffer.py b/tests/unit/utils/test_dictdiffer.py deleted file mode 100644 index 108dc86d8449..000000000000 --- a/tests/unit/utils/test_dictdiffer.py +++ /dev/null @@ -1,107 +0,0 @@ -import salt.utils.dictdiffer as dictdiffer -from tests.support.unit import TestCase - -NONE = dictdiffer.RecursiveDictDiffer.NONE_VALUE - - -class RecursiveDictDifferTestCase(TestCase): - def setUp(self): - old_dict = { - "a": {"b": 1, "c": 2, "e": "old_value", "f": "old_key"}, - "j": "value", - } - new_dict = { - "a": {"b": 1, "c": 4, "e": "new_value", "g": "new_key"}, - "h": "new_key", - "i": None, - "j": "value", - } - self.recursive_diff = dictdiffer.recursive_diff( - old_dict, new_dict, ignore_missing_keys=False - ) - self.recursive_diff_ign = dictdiffer.recursive_diff(old_dict, new_dict) - - def tearDown(self): - for attrname in ("recursive_diff", "recursive_diff_missing_keys"): - try: - delattr(self, attrname) - except AttributeError: - continue - - def test_added(self): - self.assertEqual(self.recursive_diff.added(), ["a.g", "h", "i"]) - - def test_removed(self): - self.assertEqual(self.recursive_diff.removed(), ["a.f"]) - - def test_changed_with_ignore_unset_values(self): - self.recursive_diff.ignore_unset_values = True - self.assertEqual(self.recursive_diff.changed(), ["a.c", "a.e"]) - - def test_changed_without_ignore_unset_values(self): - self.recursive_diff.ignore_unset_values = False - self.assertEqual( - self.recursive_diff.changed(), ["a.c", "a.e", "a.f", "a.g", "h", "i"] - ) - - def test_unchanged(self): - self.assertEqual(self.recursive_diff.unchanged(), ["a.b", "j"]) - - def test_diffs(self): - self.assertDictEqual( - self.recursive_diff.diffs, - { - "a": { - "c": {"old": 2, "new": 4}, - "e": {"old": "old_value", "new": "new_value"}, - "f": {"old": "old_key", "new": NONE}, - "g": {"old": NONE, "new": "new_key"}, - }, - "h": {"old": NONE, "new": "new_key"}, - "i": {"old": NONE, "new": None}, - }, - ) - self.assertDictEqual( - self.recursive_diff_ign.diffs, - { - "a": { - "c": {"old": 2, "new": 4}, - "e": {"old": "old_value", "new": "new_value"}, - "g": {"old": NONE, "new": "new_key"}, - }, - "h": {"old": NONE, "new": "new_key"}, - "i": {"old": NONE, "new": None}, - }, - ) - - def test_new_values(self): - self.assertDictEqual( - self.recursive_diff.new_values, - { - "a": {"c": 4, "e": "new_value", "f": NONE, "g": "new_key"}, - "h": "new_key", - "i": None, - }, - ) - - def test_old_values(self): - self.assertDictEqual( - self.recursive_diff.old_values, - { - "a": {"c": 2, "e": "old_value", "f": "old_key", "g": NONE}, - "h": NONE, - "i": NONE, - }, - ) - - def test_changes_str(self): - self.assertEqual( - self.recursive_diff.changes_str, - "a:\n" - " c from 2 to 4\n" - " e from 'old_value' to 'new_value'\n" - " f from 'old_key' to nothing\n" - " g from nothing to 'new_key'\n" - "h from nothing to 'new_key'\n" - "i from nothing to None", - )