Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

diff: support for collection.abc subclasses #70

Merged
merged 1 commit into from
Apr 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 17 additions & 11 deletions dictdiffer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is part of Dictdiffer.
#
# Copyright (C) 2013 Fatih Erikli.
# Copyright (C) 2013, 2014, 2015 CERN.
# Copyright (C) 2013, 2014, 2015, 2016 CERN.
#
# Dictdiffer is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more
Expand All @@ -14,14 +14,19 @@

from .utils import are_different, EPSILON, dot_lookup, PathLimit
from .version import __version__
from ._compat import string_types, text_type, PY2
from ._compat import MutableMapping, MutableSet, MutableSequence, \
string_types, text_type, PY2


(ADD, REMOVE, CHANGE) = (
'add', 'remove', 'change')

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

DICT_TYPE = MutableMapping
LIST_TYPE = MutableSequence
SET_TYPE = MutableSet


def diff(first, second, node=None, ignore=None, path_limit=None, expand=False,
tolerance=EPSILON):
Expand Down Expand Up @@ -90,7 +95,7 @@ def diff(first, second, node=None, ignore=None, path_limit=None, expand=False,

differ = False

if isinstance(first, dict) and isinstance(second, dict):
if isinstance(first, DICT_TYPE) and isinstance(second, DICT_TYPE):
# dictionaries are not hashable, we can't use sets
def check(key):
"""Test if key in current node should be ignored."""
Expand All @@ -99,7 +104,7 @@ def check(key):
else:
new_key = key
return ignore is None \
or (node + [key] if isinstance(dotted_node, list)
or (node + [key] if isinstance(dotted_node, LIST_TYPE)
else '.'.join(node + [str(new_key)])) not in ignore

intersection = [k for k in first if k in second and check(k)]
Expand All @@ -108,7 +113,7 @@ def check(key):

differ = True

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

Expand All @@ -118,7 +123,7 @@ def check(key):

differ = True

elif isinstance(first, set) and isinstance(second, set):
elif isinstance(first, SET_TYPE) and isinstance(second, SET_TYPE):

intersection = {}
addition = second - first
Expand Down Expand Up @@ -155,7 +160,8 @@ def check(key):
collect = []
collect_recurred = []
for key in addition:
if not isinstance(second[key], (set, list, dict)):
if not isinstance(second[key], (
SET_TYPE, LIST_TYPE, DICT_TYPE)):
collect.append((key, second[key]))
elif path_limit.path_is_limit(node+[key]):
collect.append((key, second[key]))
Expand Down Expand Up @@ -213,9 +219,9 @@ def patch(diff_result, destination):
def add(node, changes):
for key, value in changes:
dest = dot_lookup(destination, node)
if isinstance(dest, list):
if isinstance(dest, LIST_TYPE):
dest.insert(key, value)
elif isinstance(dest, set):
elif isinstance(dest, SET_TYPE):
dest |= value
else:
dest[key] = value
Expand All @@ -226,15 +232,15 @@ def change(node, changes):
last_node = node.split('.')[-1]
else:
last_node = node[-1]
if isinstance(dest, list):
if isinstance(dest, LIST_TYPE):
last_node = int(last_node)
_, value = changes
dest[last_node] = value

def remove(node, changes):
for key, value in changes:
dest = dot_lookup(destination, node)
if isinstance(dest, set):
if isinstance(dest, SET_TYPE):
dest -= value
else:
del dest[key]
Expand Down
4 changes: 3 additions & 1 deletion dictdiffer/_compat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is part of Dictdiffer.
#
# Copyright (C) 2015 CERN.
# Copyright (C) 2015, 2016 CERN.
#
# Dictdiffer is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more
Expand All @@ -17,6 +17,7 @@
num_types = int, float
PY2 = False

from collections.abc import MutableMapping, MutableSet, MutableSequence
from itertools import zip_longest as _zip_longest
izip_longest = _zip_longest
else: # pragma: no cover (Python 2/3 specific code)
Expand All @@ -25,5 +26,6 @@
num_types = int, long, float
PY2 = True

from collections import MutableMapping, MutableSet, MutableSequence
from itertools import izip_longest as _zip_longest
izip_longest = _zip_longest
75 changes: 74 additions & 1 deletion tests/test_dictdiffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# This file is part of Dictdiffer.
#
# Copyright (C) 2013 Fatih Erikli.
# Copyright (C) 2013, 2014, 2015 CERN.
# Copyright (C) 2013, 2014, 2015, 2016 CERN.
#
# Dictdiffer is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more
Expand All @@ -12,6 +12,7 @@
import unittest

from dictdiffer import diff, patch, revert, swap, dot_lookup
from dictdiffer._compat import MutableMapping, MutableSet, MutableSequence
from dictdiffer.utils import PathLimit


Expand Down Expand Up @@ -364,6 +365,78 @@ class Foo(dict):
diffed = next(diff(first, second))
assert ('change', [2014, 4, 'sum'], (-12140.0, -12141.0)) == diffed

def test_collection_subclasses(self):
class DictA(MutableMapping):

def __init__(self, *args, **kwargs):
self.__dict__.update(*args, **kwargs)

def __setitem__(self, key, value):
self.__dict__[key] = value

def __getitem__(self, key):
return self.__dict__[key]

def __delitem__(self, key):
del self.__dict__[key]

def __iter__(self):
return iter(self.__dict__)

def __len__(self):
return len(self.__dict__)

class DictB(MutableMapping):

def __init__(self, *args, **kwargs):
self.__dict__.update(*args, **kwargs)

def __setitem__(self, key, value):
self.__dict__[key] = value

def __getitem__(self, key):
return self.__dict__[key]

def __delitem__(self, key):
del self.__dict__[key]

def __iter__(self):
return iter(self.__dict__)

def __len__(self):
return len(self.__dict__)

class ListA(MutableSequence):

def __init__(self, *args, **kwargs):
self._list = list(*args, **kwargs)

def __getitem__(self, index):
return self._list[index]

def __setitem__(self, index, value):
self._list[index] = value

def __delitem__(self, index):
del self._list[index]

def __iter__(self):
for value in self._list:
yield value

def __len__(self):
return len(self._list)

def insert(self, index, value):
self._list.insert(index, value)

daa = DictA(a=ListA(['a', 'A']))
dba = DictB(a=ListA(['a', 'A']))
dbb = DictB(a=ListA(['b', 'A']))
assert list(diff(daa, dba)) == []
assert list(diff(daa, dbb)) == [('change', ['a', 0], ('a', 'b'))]
assert list(diff(dba, dbb)) == [('change', ['a', 0], ('a', 'b'))]


class DiffPatcherTests(unittest.TestCase):
def test_addition(self):
Expand Down