Skip to content

Commit

Permalink
Merge b1bfdd6 into f8dc205
Browse files Browse the repository at this point in the history
  • Loading branch information
yoyonel committed Feb 10, 2019
2 parents f8dc205 + b1bfdd6 commit b40689c
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 134 deletions.
247 changes: 130 additions & 117 deletions dictdiffer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@

__all__ = ('diff', 'patch', 'swap', 'revert', 'dot_lookup', '__version__')

DICT_TYPES = (MutableMapping, )
LIST_TYPES = (MutableSequence, )
SET_TYPES = (MutableSet, )
DICT_TYPES = (MutableMapping,)
LIST_TYPES = (MutableSequence,)
SET_TYPES = (MutableSet,)

try:
pkg_resources.get_distribution('numpy')
Expand All @@ -38,7 +38,7 @@

import numpy

LIST_TYPES += (numpy.ndarray, )
LIST_TYPES += (numpy.ndarray,)


def diff(first, second, node=None, ignore=None, path_limit=None, expand=False,
Expand Down Expand Up @@ -124,7 +124,13 @@ def diff(first, second, node=None, ignore=None, path_limit=None, expand=False,
for value in ignore
}

node = node or []
if ignore:
ignore = type(ignore)(
[
(value,) if isinstance(value, int) else value
for value in ignore
]
)

def dotted(node, default_type=list):
"""Return dotted notation."""
Expand All @@ -134,126 +140,133 @@ def dotted(node, default_type=list):
else:
return default_type(node)

dotted_node = dotted(node)

differ = False

if isinstance(first, DICT_TYPES) and isinstance(second, DICT_TYPES):
# dictionaries are not hashable, we can't use sets
def check(key):
"""Test if key in current node should be ignored."""
return ignore is None or (
dotted(node + [key], default_type=tuple) not in ignore and
tuple(node + [key]) not in ignore
)

intersection = [k for k in first if k in second and check(k)]
addition = [k for k in second if k not in first and check(k)]
deletion = [k for k in first if k not in second and check(k)]

differ = True

elif isinstance(first, LIST_TYPES) and isinstance(second, LIST_TYPES):
len_first = len(first)
len_second = len(second)

intersection = list(range(0, min(len_first, len_second)))
addition = list(range(min(len_first, len_second), len_second))
deletion = list(reversed(range(min(len_first, len_second), len_first)))

differ = True

elif isinstance(first, SET_TYPES) and isinstance(second, SET_TYPES):
# Deep copy is not necessary for hashable items.
addition = second - first
if len(addition):
yield ADD, dotted_node, [(0, addition)]
deletion = first - second
if len(deletion):
yield REMOVE, dotted_node, [(0, deletion)]

if differ:
# Compare if object is a dictionary or list.
#
# NOTE variables: intersection, addition, deletion contain only
# hashable types, hence they do not need to be deepcopied.
#
# Call again the parent function as recursive if dictionary have child
# objects. Yields `add` and `remove` flags.
for key in intersection:
# if type is not changed, callees again diff function to compare.
# otherwise, the change will be handled as `change` flag.
if path_limit and path_limit.path_is_limit(node+[key]):
yield CHANGE, node+[key], (
deepcopy(first[key]), deepcopy(second[key])
)
else:
recurred = diff(first[key],
second[key],
node=node + [key],
ignore=ignore,
path_limit=path_limit,
expand=expand,
tolerance=tolerance)

for diffed in recurred:
yield diffed

if addition:
if path_limit:
collect = []
collect_recurred = []
for key in addition:
if not isinstance(second[key],
SET_TYPES + LIST_TYPES + DICT_TYPES):
collect.append((key, deepcopy(second[key])))
elif path_limit.path_is_limit(node+[key]):
collect.append((key, deepcopy(second[key])))
else:
collect.append((key, second[key].__class__()))
recurred = diff(second[key].__class__(),
second[key],
node=node+[key],
ignore=ignore,
path_limit=path_limit,
expand=expand,
tolerance=tolerance)
def _diff_recurrence(_first, _second, _node=None):
_node = _node or []

collect_recurred.append(recurred)
dotted_node = dotted(_node)

if expand:
for key, val in collect:
yield ADD, dotted_node, [(key, val)]
differ = False

if isinstance(_first, DICT_TYPES) and isinstance(_second, DICT_TYPES):
# dictionaries are not hashable, we can't use sets
def check(key):
"""Test if key in current node should be ignored."""
return ignore is None or (
dotted(_node + [key],
default_type=tuple) not in ignore and
tuple(_node + [key]) not in ignore
)

intersection = [k for k in _first if k in _second and check(k)]
addition = [k for k in _second if k not in _first and check(k)]
deletion = [k for k in _first if k not in _second and check(k)]

differ = True

elif isinstance(_first, LIST_TYPES) and isinstance(_second,
LIST_TYPES):
len_first = len(_first)
len_second = len(_second)

intersection = list(range(0, min(len_first, len_second)))
addition = list(range(min(len_first, len_second), len_second))
deletion = list(
reversed(range(min(len_first, len_second), len_first)))

differ = True

elif isinstance(_first, SET_TYPES) and isinstance(_second, SET_TYPES):
# Deep copy is not necessary for hashable items.
addition = _second - _first
if len(addition):
yield ADD, dotted_node, [(0, addition)]
deletion = _first - _second
if len(deletion):
yield REMOVE, dotted_node, [(0, deletion)]

if differ:
# Compare if object is a dictionary or list.
#
# NOTE variables: intersection, addition, deletion contain only
# hashable types, hence they do not need to be deepcopied.
#
# Call again the parent function as recursive if dictionary have
# child objects. Yields `add` and `remove` flags.
for key in intersection:
# if type is not changed,
# callees again diff function to compare.
# otherwise, the change will be handled as `change` flag.
if path_limit and path_limit.path_is_limit(_node + [key]):
yield CHANGE, _node + [key], (
deepcopy(_first[key]), deepcopy(_second[key])
)
else:
yield ADD, dotted_node, collect
recurred = _diff_recurrence(_first[key],
_second[key],
_node=_node + [key],
)

for recurred in collect_recurred:
for diffed in recurred:
yield diffed
else:
if expand:

if addition:
if path_limit:
collect = []
collect_recurred = []
for key in addition:
yield ADD, dotted_node, [(key, deepcopy(second[key]))]
if not isinstance(_second[key],
SET_TYPES + LIST_TYPES + DICT_TYPES):
collect.append((key, deepcopy(_second[key])))
elif path_limit.path_is_limit(_node + [key]):
collect.append((key, deepcopy(_second[key])))
else:
collect.append((key, _second[key].__class__()))
recurred = _diff_recurrence(
_second[key].__class__(),
_second[key],
_node=_node + [key],
)

collect_recurred.append(recurred)

if expand:
for key, val in collect:
yield ADD, dotted_node, [(key, val)]
else:
yield ADD, dotted_node, collect

for recurred in collect_recurred:
for diffed in recurred:
yield diffed
else:
yield ADD, dotted_node, [
# for additions, return a list that consist with
# two-pair tuples.
(key, deepcopy(second[key])) for key in addition]

if deletion:
if expand:
for key in deletion:
yield REMOVE, dotted_node, [(key, deepcopy(first[key]))]
else:
yield REMOVE, dotted_node, [
# for deletions, return the list of removed keys
# and values.
(key, deepcopy(first[key])) for key in deletion]

else:
# Compare string and numerical types and yield `change` flag.
if are_different(first, second, tolerance):
yield CHANGE, dotted_node, (deepcopy(first), deepcopy(second))
if expand:
for key in addition:
yield ADD, dotted_node, [
(key, deepcopy(_second[key]))]
else:
yield ADD, dotted_node, [
# for additions, return a list that consist with
# two-pair tuples.
(key, deepcopy(_second[key])) for key in addition]

if deletion:
if expand:
for key in deletion:
yield REMOVE, dotted_node, [
(key, deepcopy(_first[key]))]
else:
yield REMOVE, dotted_node, [
# for deletions, return the list of removed keys
# and values.
(key, deepcopy(_first[key])) for key in deletion]

else:
# Compare string and numerical types and yield `change` flag.
if are_different(_first, _second, tolerance):
yield CHANGE, dotted_node, (deepcopy(_first),
deepcopy(_second))

return _diff_recurrence(first, second, node)


def patch(diff_result, destination, in_place=False):
Expand Down
23 changes: 8 additions & 15 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,15 @@
'pytest>=2.8.0',
]

extras_require = {
':python_version=="2.6"': [
'unittest2>=1.1.0',
],
'docs': [
'Sphinx>=1.4.4',
'sphinx-rtd-theme>=0.1.9',
],
'numpy': [
'numpy>=1.11.0',
],
'tests': tests_require,
}
extras_require = {':python_version=="2.6"': [
'unittest2>=1.1.0',
], 'docs': [
'Sphinx>=1.4.4',
'sphinx-rtd-theme>=0.1.9',
], 'numpy': [
'numpy>=1.11.0',
], 'tests': tests_require, 'all': []}

extras_require['all'] = []
for key, reqs in extras_require.items():
if ':' == key[0]:
continue
Expand All @@ -58,7 +52,6 @@

packages = find_packages()


# Get the version string. Cannot be done with import!
with open(os.path.join('dictdiffer', 'version.py'), 'rt') as f:
version = re.search(
Expand Down
24 changes: 22 additions & 2 deletions tests/test_dictdiffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ def test_remove_list(self):
assert ('remove', 'a', [(1, 'c'), (0, 'b'), ]) == diffed

def test_add_set(self):
first = {'a': set([1, 2, 3])}
second = {'a': set([0, 1, 2, 3])}
first = {'a': {1, 2, 3}}
second = {'a': {0, 1, 2, 3}}
diffed = next(diff(first, second))
assert ('add', 'a', [(0, set([0]))]) == diffed

Expand Down Expand Up @@ -301,6 +301,26 @@ def test_ignore_missing_complex_keys(self):
diffed = next(diff(second, first, ignore=[['a', 1, 'b']]))
assert ('change', ['a', 1, 'a'], (1, 'a')) == diffed

def test_ignore_stringofintegers_keys(self):
a = {'1': '1', '2': '2', '3': '3'}
b = {'1': '1', '2': '2', '3': '99', '4': '100'}

assert list(diff(a, b, ignore={'3', '4'})) == []

def test_ignore_integers_keys(self):
a = {1: 1, 2: 2, 3: 3}
b = {1: 1, 2: 2, 3: 99, 4: 100}

assert len(list(diff(a, b, ignore={3, 4}))) == 0

def test_ignore_with_ignorecase(self):
class IgnoreCase(set):
def __contains__(self, key):
return set.__contains__(self, str(key).lower())

assert list(diff({'a': 1, 'b': 2}, {'A': 3, 'b': 4},
ignore=IgnoreCase('a'))) == [('change', 'b', (2, 4))]

def test_complex_diff(self):
"""Check regression on issue #4."""
from decimal import Decimal
Expand Down

0 comments on commit b40689c

Please sign in to comment.