utils.doc_diff() now also supports list values

This commit is contained in:
Sybren A. Stüvel 2018-03-27 11:19:46 +02:00
parent de8bff51b5
commit dee0b18429
2 changed files with 70 additions and 17 deletions

View File

@ -162,7 +162,7 @@ class DoesNotExist(object, metaclass=MetaFalsey):
"""Returned as value by doc_diff if a value does not exist."""
def doc_diff(doc1, doc2, falsey_is_equal=True):
def doc_diff(doc1, doc2, *, falsey_is_equal=True, superkey: str = None):
"""Generator, yields differences between documents.
Yields changes as (key, value in doc1, value in doc2) tuples, where
@ -176,25 +176,58 @@ def doc_diff(doc1, doc2, falsey_is_equal=True):
function won't report differences between DoesNotExist, False, '', and 0.
"""
for key in set(doc1.keys()).union(set(doc2.keys())):
if isinstance(key, str) and key[0] == '_':
continue
def combine_key(some_key):
"""Combine this key with the superkey.
val1 = doc1.get(key, DoesNotExist)
val2 = doc2.get(key, DoesNotExist)
Keep the key type the same, unless we have to combine with a superkey.
"""
if not superkey:
return some_key
if isinstance(some_key, str) and some_key[0] == '[':
return f'{superkey}{some_key}'
return f'{superkey}.{some_key}'
# Only recurse if both values are dicts
if isinstance(val1, dict) and isinstance(val2, dict):
for subkey, subval1, subval2 in doc_diff(val1, val2):
yield '%s.%s' % (key, subkey), subval1, subval2
continue
if doc1 is doc2:
return
if val1 == val2:
continue
if falsey_is_equal and bool(val1) == bool(val2) == False:
continue
if falsey_is_equal and not bool(doc1) and not bool(doc2):
return
yield key, val1, val2
if isinstance(doc1, dict) and isinstance(doc2, dict):
for key in set(doc1.keys()).union(set(doc2.keys())):
if isinstance(key, str) and key[0] == '_':
continue
val1 = doc1.get(key, DoesNotExist)
val2 = doc2.get(key, DoesNotExist)
yield from doc_diff(val1, val2,
falsey_is_equal=falsey_is_equal,
superkey=combine_key(key))
return
if isinstance(doc1, list) and isinstance(doc2, list):
for idx in range(max(len(doc1), len(doc2))):
try:
item1 = doc1[idx]
except IndexError:
item1 = DoesNotExist
try:
item2 = doc2[idx]
except IndexError:
item2 = DoesNotExist
subkey = f'[{idx}]'
if item1 is DoesNotExist or item2 is DoesNotExist:
yield combine_key(subkey), item1, item2
else:
yield from doc_diff(item1, item2,
falsey_is_equal=falsey_is_equal,
superkey=combine_key(subkey))
return
if doc1 != doc2:
yield superkey, doc1, doc2
def random_etag() -> str:

View File

@ -107,6 +107,27 @@ class DocDiffTest(unittest.TestCase):
('props.status2', DoesNotExist, 'todo')},
set(diff))
def test_diff_list_values(self):
from pillar.api.utils import doc_diff
diff = doc_diff({'a': 'b', 'props': ['status', 'todo', 'notes', 'jemoeder']},
{'a': 'b', 'props': ['todo', 'others', 'notes', 'jemoeder']})
self.assertEqual({
('props[0]', 'status', 'todo'),
('props[1]', 'todo', 'others'),
}, set(diff))
def test_diff_list_unequal_lengths(self):
from pillar.api.utils import doc_diff, DoesNotExist
diff = doc_diff({'a': 'b', 'props': ['status', 'todo', 'notes']},
{'a': 'b', 'props': ['todo', 'others', 'notes', 'jemoeder']})
self.assertEqual({
('props[0]', 'status', 'todo'),
('props[1]', 'todo', 'others'),
('props[3]', DoesNotExist, 'jemoeder'),
}, set(diff))
class NodeSetattrTest(unittest.TestCase):
def test_simple(self):
@ -163,4 +184,3 @@ class NodeSetattrTest(unittest.TestCase):
node_setattr(node, 'b.complex', {None: 5})
self.assertEqual({'b': {'complex': {None: 5}}}, node)