Skip to content

Commit

Permalink
introduce E (entity) tag
Browse files Browse the repository at this point in the history
* to be able to build diffs for objects that can't hold subdiffs by themselves
  • Loading branch information
mr-mixas committed Aug 5, 2019
1 parent cba71e6 commit dec9074
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 95 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ 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.
* `R` key used for removed item.
* `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:

Expand Down
77 changes: 26 additions & 51 deletions nested_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,20 @@ 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.
`R` key used for removed item.
`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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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


Expand Down
4 changes: 2 additions & 2 deletions nested_diff/fmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 28 additions & 23 deletions tests/test_diff.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_iterator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from nested_diff import Iterator, diff, _hdict
from nested_diff import Iterator, diff


def test_scalar_diff():
Expand Down Expand Up @@ -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
Expand Down
30 changes: 16 additions & 14 deletions tests/test_patch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from nested_diff import patch, _hdict
from nested_diff import patch


def test_incorrect_diff_type():
Expand All @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit dec9074

Please sign in to comment.