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.""" """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. """Generator, yields differences between documents.
Yields changes as (key, value in doc1, value in doc2) tuples, where Yields changes as (key, value in doc1, value in doc2) tuples, where
@ -176,6 +176,24 @@ def doc_diff(doc1, doc2, falsey_is_equal=True):
function won't report differences between DoesNotExist, False, '', and 0. function won't report differences between DoesNotExist, False, '', and 0.
""" """
def combine_key(some_key):
"""Combine this key with the superkey.
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}'
if doc1 is doc2:
return
if falsey_is_equal and not bool(doc1) and not bool(doc2):
return
if isinstance(doc1, dict) and isinstance(doc2, dict):
for key in set(doc1.keys()).union(set(doc2.keys())): for key in set(doc1.keys()).union(set(doc2.keys())):
if isinstance(key, str) and key[0] == '_': if isinstance(key, str) and key[0] == '_':
continue continue
@ -183,18 +201,33 @@ def doc_diff(doc1, doc2, falsey_is_equal=True):
val1 = doc1.get(key, DoesNotExist) val1 = doc1.get(key, DoesNotExist)
val2 = doc2.get(key, DoesNotExist) val2 = doc2.get(key, DoesNotExist)
# Only recurse if both values are dicts yield from doc_diff(val1, val2,
if isinstance(val1, dict) and isinstance(val2, dict): falsey_is_equal=falsey_is_equal,
for subkey, subval1, subval2 in doc_diff(val1, val2): superkey=combine_key(key))
yield '%s.%s' % (key, subkey), subval1, subval2 return
continue
if val1 == val2: if isinstance(doc1, list) and isinstance(doc2, list):
continue for idx in range(max(len(doc1), len(doc2))):
if falsey_is_equal and bool(val1) == bool(val2) == False: try:
continue item1 = doc1[idx]
except IndexError:
item1 = DoesNotExist
try:
item2 = doc2[idx]
except IndexError:
item2 = DoesNotExist
yield key, val1, val2 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: def random_etag() -> str:

View File

@ -107,6 +107,27 @@ class DocDiffTest(unittest.TestCase):
('props.status2', DoesNotExist, 'todo')}, ('props.status2', DoesNotExist, 'todo')},
set(diff)) 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): class NodeSetattrTest(unittest.TestCase):
def test_simple(self): def test_simple(self):
@ -163,4 +184,3 @@ class NodeSetattrTest(unittest.TestCase):
node_setattr(node, 'b.complex', {None: 5}) node_setattr(node, 'b.complex', {None: 5})
self.assertEqual({'b': {'complex': {None: 5}}}, node) self.assertEqual({'b': {'complex': {None: 5}}}, node)