Skip to content

Commit

Permalink
[Issue #3897] Get the diff of two nested dict (#3968)
Browse files Browse the repository at this point in the history
## Summary
Fixes #{[3897](#3897)}

### Time to review: __5 mins__

## Changes proposed
Util Function: Given two complex nested dictionaries can provide a set
of changes between the two
Test Added
  • Loading branch information
babebe authored Feb 28, 2025
1 parent 8d02355 commit bf551d2
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 1 deletion.
36 changes: 36 additions & 0 deletions api/src/util/dict_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,39 @@ def flatten_dict(in_dict: Any, separator: str = ".", prefix: str = "") -> dict:

# value isn't a dictionary, so no more recursion
return {prefix: in_dict}


def diff_nested_dicts(dict1: dict, dict2: dict) -> list:
"""
Compare two dictionaries (possibly nested), return a list of differences
with 'field', 'before', and 'after' for each key.
:param dict1 : The first dictionary.
:param dict2 : The second dictionary.
:return : Returns a list of dictionaries representing the differences.
a"""

flatt_dict1 = flatten_dict(dict1)
flatt_dict2 = flatten_dict(dict2)

diffs: list = []

all_keys = set(flatt_dict1.keys()).union(flatt_dict2.keys()) # Does not keep order

for k in all_keys:
values = [flatt_dict1.get(k, None), flatt_dict2.get(k, None)]
# convert values to set for comparison
v_a = _convert_iterables_to_set(values[0])
v_b = _convert_iterables_to_set(values[1])

if v_a != v_b:
diffs.append({"field": k, "before": values[0], "after": values[1]})

return diffs


def _convert_iterables_to_set(data: Any) -> Any:
if isinstance(data, (list, tuple)):
return set(data)
return data
90 changes: 89 additions & 1 deletion api/tests/src/util/test_dict_util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from src.util.dict_util import flatten_dict
from src.util.dict_util import diff_nested_dicts, flatten_dict


@pytest.mark.parametrize(
Expand Down Expand Up @@ -51,3 +51,91 @@
)
def test_flatten_dict(data, expected_output):
assert flatten_dict(data) == expected_output


@pytest.mark.parametrize(
"dict1,dict2,expected_output",
[
(
# dict1
{"a": "apple", "b": {"x": 1, "y": 2}, "c": 100}, # additional field a
# dict2
{
"b": {"x": 1, "y": 3}, # changed y
"c": 200, # changed c
"d": "dog", # new field added
},
# expected output
[
{"field": "a", "before": "apple", "after": None},
{"field": "b.y", "before": 2, "after": 3},
{"field": "c", "before": 100, "after": 200},
{"field": "d", "before": None, "after": "dog"},
],
),
(
# dict1
{"a": "ball", "b": {"p": 5, "q": 10}, "e": "elephant"},
# dict2
{
"a": "bat", # changed a
"b": {"p": 5, "q": 1.1}, # no change # changed q
"e": "elephant", # no change
},
# expected output
[
{"field": "a", "before": "ball", "after": "bat"},
{"field": "b.q", "before": 10, "after": 1.1},
],
),
(
# dict1
{"x": 10, "y": "yellow", "z": {"m": "mouse", "n": True}},
# dict2
{
"x": 10, # no change
"y": "yellow", # no change
"z": {"m": "mouse", "n": False}, # no change # changed n
},
# expected output
[{"field": "z.n", "before": True, "after": False}],
),
(
# dict1
{"x": {"x": {"x": [1, 2, True]}}},
# dict2
{"x": {"x": {"x": [1, 2, True]}}}, # no change
# expected output
[],
),
(
# dict1
{"x": {"x": {"x": [1, 2, True]}}},
# dict2
{"x": {"x": {"x": [1, True, 2]}}}, # re-ordered list
# expected output
[],
),
(
# dict1
{"x": {"x": [1, 2], "z": None}}, # missing y
# dict2
{"x": {"y": [1, 2], "z": 4}}, # missing x
# expected output
[
{"field": "x.x", "before": [1, 2], "after": None},
{"field": "x.y", "before": None, "after": [1, 2]},
{"field": "x.z", "before": None, "after": 4},
],
),
],
)
def test_diff_nested_dicts(dict1, dict2, expected_output):
result = diff_nested_dicts(dict1, dict2)

assert len(result) == len(expected_output)

expected_sorted = sorted(expected_output, key=lambda x: x["field"])
sorted_result = sorted(result, key=lambda x: x["field"])

assert expected_sorted == sorted_result

0 comments on commit bf551d2

Please sign in to comment.