From 7c310e12efd97d7ae48eb1c196092bbe0c57842d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 12 Oct 2016 16:01:30 +0200 Subject: [PATCH] Added util function to compute the difference between two dicts. --- pillar/api/utils/__init__.py | 34 ++++++++++++++++++++++ tests/test_api/test_utils.py | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/pillar/api/utils/__init__.py b/pillar/api/utils/__init__.py index db9307ce..00f64696 100644 --- a/pillar/api/utils/__init__.py +++ b/pillar/api/utils/__init__.py @@ -113,3 +113,37 @@ def gravatar(email, size=64): return "https://www.gravatar.com/avatar/" + \ hashlib.md5(str(email)).hexdigest() + \ "?" + urllib.urlencode(parameters) + + +class DoesNotExist(object): + """Returned as value by doc_diff if a value does not exist.""" + + +def doc_diff(doc1, doc2): + """Generator, yields differences between documents. + + Yields changes as (key, value in doc1, value in doc2) tuples, where + the value can also be the DoesNotExist class. Does not report changed + private keys (i.e. starting with underscores). + + Sub-documents (i.e. dicts) are recursed, and dot notation is used + for the keys if changes are found. + """ + + for key in set(doc1.keys()).union(set(doc2.keys())): + if isinstance(key, basestring) and key[0] == u'_': + continue + + val1 = doc1.get(key, DoesNotExist) + val2 = doc2.get(key, DoesNotExist) + + # 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 val1 == val2: + continue + + yield key, val1, val2 diff --git a/tests/test_api/test_utils.py b/tests/test_api/test_utils.py index f7540a7c..f4dc4f65 100644 --- a/tests/test_api/test_utils.py +++ b/tests/test_api/test_utils.py @@ -1,4 +1,6 @@ # -*- encoding: utf-8 -*- +from __future__ import absolute_import +import unittest from bson import ObjectId from pillar.tests import AbstractPillarTest @@ -28,3 +30,57 @@ class Str2idTest(AbstractPillarTest): unhappy('') unhappy(u'') unhappy(None) + + +class DocDiffTest(unittest.TestCase): + def test_no_diff_simple(self): + from pillar.api.utils import doc_diff + diff = doc_diff({'a': 'b', 3: 42}, + {'a': 'b', 3: 42}) + + self.assertEqual([], list(diff)) + + def test_no_diff_privates(self): + from pillar.api.utils import doc_diff + diff = doc_diff({'a': 'b', 3: 42, '_updated': 5133}, + {'a': 'b', 3: 42, '_updated': 42}) + + self.assertEqual([], list(diff)) + + def test_diff_values_simple(self): + from pillar.api.utils import doc_diff + diff = doc_diff({'a': 'b', 3: 42}, + {'a': 'b', 3: 513}) + + self.assertEqual([(3, 42, 513)], list(diff)) + + def test_diff_keys_simple(self): + from pillar.api.utils import doc_diff, DoesNotExist + diff = doc_diff({'a': 'b', 3: 42}, + {'a': 'b', 2: 42}) + + self.assertEqual({(3, 42, DoesNotExist), (2, DoesNotExist, 42)}, set(diff)) + + def test_no_diff_nested(self): + from pillar.api.utils import doc_diff + diff = doc_diff({'a': 'b', 'props': {'status': u'todo', 'notes': u'jemoeder'}}, + {'a': 'b', 'props': {'status': u'todo', 'notes': u'jemoeder'}}) + + self.assertEqual([], list(diff)) + + def test_diff_values_nested(self): + from pillar.api.utils import doc_diff + diff = doc_diff({'a': 'b', 'props': {'status': u'todo', 'notes': u'jemoeder'}}, + {'a': 'c', 'props': {'status': u'done', 'notes': u'jemoeder'}}) + + self.assertEqual({('a', 'b', 'c'), ('props.status', u'todo', u'done')}, + set(diff)) + + def test_diff_keys_nested(self): + from pillar.api.utils import doc_diff, DoesNotExist + diff = doc_diff({'a': 'b', 'props': {'status1': u'todo', 'notes': u'jemoeder'}}, + {'a': 'b', 'props': {'status2': u'todo', 'notes': u'jemoeder'}}) + + self.assertEqual({('props.status1', u'todo', DoesNotExist), + ('props.status2', DoesNotExist, u'todo')}, + set(diff))