diff --git a/README.md b/README.md index bb2292a..6123344 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Diff is a dict and may contain following keys: * `A` stands for 'added', it's value - added item. * `D` means 'different' and contains subdiff. +* `E` diffed entity (optional), value - empty instance of entity's class. * `I` index for sequence item, used only when prior item was omitted. * `N` is a new value for changed item. * `O` is a changed item's old value. @@ -23,9 +24,11 @@ Diff is a dict and may contain following keys: * `U` represent unchanged item. Diff metadata alternates with actual data; simple types specified as is, dicts, -lists, sets and tuples contain subdiffs for their items with native for such -types addressing: indexes for lists and tuples and keys for dictionaries. Each -status type, except `D` and `I`, may be omitted during diff computation. +lists and tuples contain subdiffs for their items with native for such types +addressing: indexes for lists and tuples and keys for dictionaries. Each status +type, except `D`. `E` and `I`, may be omitted during diff computation. `E` tag +is used with `D` when entity unable to contain diff by itself (set, frozenset); +`D` contain a list of subdiffs in this case. Annotated example: diff --git a/nested_diff/__init__.py b/nested_diff/__init__.py index 2693279..4c2badf 100644 --- a/nested_diff/__init__.py +++ b/nested_diff/__init__.py @@ -42,6 +42,7 @@ class Differ(object): Resulting diff is a dict and may contain following keys: `A` stands for 'added', it's value - added item. `D` means 'different' and contains subdiff. + `E` diffed entity (optional), value - empty instance of entity's class. `I` index for sequence item, used only when prior item was omitted. `N` is a new value for changed item. `O` is a changed item's old value. @@ -49,10 +50,12 @@ class Differ(object): `U` represent unchanged item. Diff metadata alternates with actual data; simple types specified as is, - dicts, lists, sets and tuples contain subdiffs for their items with native - for such types addressing: indexes for lists and tuples and keys for - dictionaries. Each status type, except `D` and `I`, may be omitted during - diff computation. + dicts, lists and tuples contain subdiffs for their items with native for + such types addressing: indexes for lists and tuples and keys for + dictionaries. Each status type, except `D`. `E` and `I`, may be omitted + during diff computation. `E` tag is used with `D` when entity unable to + contain diff by itself (set, frozenset); `D` contain a list of subdiffs + in this case. Example: @@ -114,7 +117,7 @@ def __init__(self, A=True, N=True, O=True, R=True, U=True, trimR=False, self.__differs = { dict: self.diff_dict, - frozenset: self.diff_frozenset, + frozenset: self.diff_set, list: self.diff_list, set: self.diff_set, tuple: self.diff_tuple, @@ -271,7 +274,7 @@ def diff_list(self, a, b): def diff_set(self, a, b): """ - Compute diff for two sets. + Compute diff for two [frozen]sets. :param a: First set to diff. :param b: Second set to diff. @@ -280,52 +283,30 @@ def diff_set(self, a, b): >>> b = {2, 3} >>> >>> Differ(U=False).diff_sets(a, b) - {'D': {{'R': 1}, {'A': 3}}} + {'D': [{'R': 1}, {'A': 3}], 'E': set()} >>> """ - dif = set() + dif = [] for i in a.union(b): if i in a and i in b: if self.op_u: - dif.add(_hdict('U', i)) + dif.append({'U': i}) elif i in a: # removed if self.op_r: - dif.add(_hdict('R', None if self.op_trim_r else i)) + dif.append({'R': None if self.op_trim_r else i}) else: # added if self.op_a: - dif.add(_hdict('A', i)) + dif.append({'A': i}) if dif: - return {'D': dif} + return {'D': dif, 'E': a.__class__()} return {} - def diff_frozenset(self, a, b): - """ - Compute diff for two frozen sets. - - :param a: First frozenset to diff. - :param b: Second frozenset to diff. - - >>> a = frozenset((1, 2)) - >>> b = frozenset((2, 3)) - >>> - >>> Differ(U=False).diff_frozensets(a, b) - {'D': frozenset({{'R': 1}, {'A': 3}})} - >>> - - """ - ret = self.diff_set(a, b) - - if 'D' in ret: - ret['D'] = frozenset(ret['D']) - - return ret - def diff_tuple(self, a, b): """ Compute diff for two tuples. @@ -371,19 +352,6 @@ def set_differ(self, cls, method): self.__differs[cls] = method -class _hdict(dict): - """ - Hashable dict, for internal use only. - """ - def __init__(self, op, val): - dict.__init__(self) - self[op] = val - self.__hash = hash((op, val)) - - def __hash__(self): - return self.__hash - - class Patcher(object): """ Patch objects using nested diff. @@ -438,7 +406,9 @@ def patch(self, target, ndiff): return getattr(target, self.__patch_method)(ndiff) if 'D' in ndiff: - return self.get_patcher(ndiff['D'].__class__)(target, ndiff) + return self.get_patcher( + ndiff.get('E', ndiff['D']).__class__ + )(target, ndiff) elif 'N' in ndiff: return ndiff['N'] else: @@ -561,13 +531,13 @@ def __init__(self, sort_keys=False): tuple: self.iter_sequence, } - def get_iter(self, value): + def get_iter(self, type_, value): """ Return apropriate iterator for passed value. """ try: - make_iter = self.__iters[value.__class__] + make_iter = self.__iters[type_] except KeyError: raise NotImplementedError @@ -651,7 +621,12 @@ def iterate(self, ndiff): yield depth, pointer, ndiff, is_pointed if 'D' in ndiff: - stack.append(self.get_iter(ndiff['D'])) + stack.append( + self.get_iter( + (ndiff['E'] if 'E' in ndiff else ndiff['D']).__class__, + ndiff['D'], + ) + ) depth += 1 diff --git a/nested_diff/fmt.py b/nested_diff/fmt.py index 4447d08..315f0a6 100644 --- a/nested_diff/fmt.py +++ b/nested_diff/fmt.py @@ -169,8 +169,8 @@ def iterate(self, diff): yield self.get_close_token(container_type) yield self.line_separator - stack.append(self.get_iter(diff['D'])) - container_type = diff['D'].__class__ + container_type = diff['E' if 'E' in diff else 'D'].__class__ + stack.append(self.get_iter(container_type, diff['D'])) path_types.append(container_type) emit_container_preamble = True depth += 1 diff --git a/tests/test_diff.py b/tests/test_diff.py index cac817c..477c398 100644 --- a/tests/test_diff.py +++ b/tests/test_diff.py @@ -1,4 +1,4 @@ -from nested_diff import diff, _hdict +from nested_diff import diff # Test what doesn't covered by external (JSON based) tests @@ -8,11 +8,12 @@ def test_frozensets_diff(): b = frozenset((2, 3)) expected = { - 'D': frozenset(( - _hdict('R', 1), - _hdict('U', 2), - _hdict('A', 3), - )) + 'D': [ + {'R': 1}, + {'U': 2}, + {'A': 3}, + ], + 'E': frozenset(), } assert expected == diff(a, b) @@ -23,11 +24,12 @@ def test_sets_diff(): b = {2, 3} expected = { - 'D': set(( - _hdict('R', 1), - _hdict('U', 2), - _hdict('A', 3), - )) + 'D': [ + {'R': 1}, + {'U': 2}, + {'A': 3}, + ], + 'E': set(), } assert expected == diff(a, b) @@ -38,9 +40,10 @@ def test_sets_diff_noAR(): b = {2, 3} expected = { - 'D': set(( - _hdict('U', 2), - )) + 'D': [ + {'U': 2}, + ], + 'E': set(), } assert expected == diff(a, b, A=False, R=False) @@ -51,10 +54,11 @@ def test_sets_diff_noU(): b = {2, 3} expected = { - 'D': set(( - _hdict('R', 1), - _hdict('A', 3), - )) + 'D': [ + {'R': 1}, + {'A': 3}, + ], + 'E': set(), } assert expected == diff(a, b, U=False) @@ -65,11 +69,12 @@ def test_sets_diff_trimR(): b = {2, 3} expected = { - 'D': set(( - _hdict('R', None), - _hdict('U', 2), - _hdict('A', 3), - )) + 'D': [ + {'R': None}, + {'U': 2}, + {'A': 3}, + ], + 'E': set(), } assert expected == diff(a, b, trimR=True) diff --git a/tests/test_iterator.py b/tests/test_iterator.py index 3292b38..f19cf81 100644 --- a/tests/test_iterator.py +++ b/tests/test_iterator.py @@ -1,6 +1,6 @@ import pytest -from nested_diff import Iterator, diff, _hdict +from nested_diff import Iterator, diff def test_scalar_diff(): @@ -94,7 +94,7 @@ def test_set_diff(): got = list(Iterator().iterate(diff(a, b))) assert len(got) == 4 - assert got[0] == (0, None, {'D': {_hdict('R', 1), _hdict('A', 2), _hdict('U', 0)}}, False) + assert got[0] == (0, None, {'D': [{'U': 0}, {'R': 1}, {'A': 2}], 'E': set()}, False) assert (1, None, {'R': 1}, False) in got assert (1, None, {'A': 2}, False) in got assert (1, None, {'U': 0}, False) in got diff --git a/tests/test_patch.py b/tests/test_patch.py index ea67273..240ad87 100644 --- a/tests/test_patch.py +++ b/tests/test_patch.py @@ -1,6 +1,6 @@ import pytest -from nested_diff import patch, _hdict +from nested_diff import patch def test_incorrect_diff_type(): @@ -26,14 +26,15 @@ def test_patch_set(): b = {0, 1, 2, 3} ndiff = { - 'D': set(( - _hdict('A', 0), - _hdict('U', 1), - _hdict('U', 2), - _hdict('A', 3), - _hdict('R', 4), - _hdict('R', 5), - )) + 'D': [ + {'A': 0}, + {'U': 1}, + {'U': 2}, + {'A': 3}, + {'R': 4}, + {'R': 5}, + ], + 'E': set(), } assert b == patch(a, ndiff) @@ -44,11 +45,12 @@ def test_patch_frozenset(): b = frozenset((2, 3)) ndiff = { - 'D': frozenset(( - _hdict('R', 1), - _hdict('U', 2), - _hdict('A', 3), - )) + 'D': [ + {'R': 1}, + {'U': 2}, + {'A': 3}, + ], + 'E': frozenset() } assert b == patch(a, ndiff)