diff --git a/.gitignore b/.gitignore index dd302c0..a03d37c 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,4 @@ target/ # PyCharm .idea -.hypothesis/ \ No newline at end of file +.hypothesis/ diff --git a/README.rst b/README.rst index 4b32f5b..f9c29b9 100644 --- a/README.rst +++ b/README.rst @@ -40,6 +40,9 @@ Quickstart >>> diff({'a': 1, 'b': 2}, {'b': 3, 'c': 4}, syntax='symmetric') {insert: {'c': 4}, 'b': [2, 3], delete: {'a': 1}} + >>> diff({'list': [1, 2, 3], "poplist": [1, 2, 3]}, {'list': [1, 3]}, syntax="rightonly") + {"list": [1, 3], delete: ["poplist"]} + # Special handling of sets >>> diff({'a', 'b', 'c'}, {'a', 'c', 'd'}) {discard: set(['b']), add: set(['d'])} diff --git a/jsondiff/__init__.py b/jsondiff/__init__.py index a3fdcc2..7ebf748 100644 --- a/jsondiff/__init__.py +++ b/jsondiff/__init__.py @@ -342,10 +342,36 @@ def unpatch(self, b, d): raise Exception("Invalid symmetric diff") +class RightOnlyJsonDiffSyntax(CompactJsonDiffSyntax): + """ + Compare to the CompactJsonDiffSyntax, I will not compare the difference in list, + because in some senario we only care about the right value (in most cases means latest value). + Instead, I will pop the later list value. + """ + + def emit_dict_diff(self, a, b, s, added, changed, removed): + if s == 1.0: + return {} + else: + changed.update(added) + if removed: + changed[delete] = list(removed.keys()) + return changed + + def emit_list_diff(self, a, b, s, inserted, changed, deleted): + if s == 0.0: + return b + elif s == 1.0: + return {} + else: + return b + + builtin_syntaxes = { 'compact': CompactJsonDiffSyntax(), 'symmetric': SymmetricJsonDiffSyntax(), - 'explicit': ExplicitJsonDiffSyntax() + 'explicit': ExplicitJsonDiffSyntax(), + 'rightonly': RightOnlyJsonDiffSyntax(), } diff --git a/jsondiff/symbols.py b/jsondiff/symbols.py index cdbebb0..6e81e3e 100644 --- a/jsondiff/symbols.py +++ b/jsondiff/symbols.py @@ -19,6 +19,7 @@ def __eq__(self, other): def __hash__(self) -> int: return hash(self.label) + missing = Symbol('missing') identical = Symbol('identical') delete = Symbol('delete') @@ -55,4 +56,4 @@ def __hash__(self) -> int: 'left', 'right', '_all_symbols_' -] \ No newline at end of file +] diff --git a/tests/test_jsondiff.py b/tests/test_jsondiff.py index 2cbbc66..187e451 100644 --- a/tests/test_jsondiff.py +++ b/tests/test_jsondiff.py @@ -1,3 +1,4 @@ +import logging import sys import unittest import pytest @@ -9,6 +10,15 @@ from hypothesis import given, settings, strategies +logging.basicConfig( + level=logging.INFO, + format=( + '%(asctime)s %(pathname)s[line:%(lineno)d] %(levelname)s %(message)s' + ), +) +logging.getLogger("jsondiff").setLevel(logging.DEBUG) + + def generate_scenario(rng): a = generate_random_json(rng, sets=True) b = perturbate_json(a, rng, sets=True) @@ -162,3 +172,39 @@ class TestSpecificIssue: def test_issue(self, a, b, syntax, expected): actual = diff(a, b, syntax=syntax) assert actual == expected + + +class TestRightOnly(unittest.TestCase): + def test_right_only_syntax(self): + a = {"poplist": [1, 2, 3]} + b = {} + self.assertEqual( + diff(a, b, syntax="rightonly", marshal=True), + { + "$delete": ["poplist"], + } + ) + a = {1: 2, 2: 3, 'list': [1, 2, 3], 'samelist': [1, 2, 3], "poplist": [1, 2, 3]} + b = {1: 2, 2: 4, 'list': [1, 3], 'samelist': [1, 2, 3]} + self.assertEqual( + diff(a, b, syntax="rightonly", marshal=True), + { + 2: 4, + "list": [1, 3], + "$delete": ["poplist"], + } + ) + self.assertEqual( + diff(a, b, syntax="rightonly"), + { + 2: 4, + "list": [1, 3], + delete: ["poplist"], + } + ) + c = [1, 2, 3] + d = [4, 5, 6] + self.assertEqual( + diff(c, d, syntax="rightonly", marshal=True), + [4, 5, 6], + )