Skip to content

Commit 8eb2a0c

Browse files
committed
ENH: add json compare scritp
This one is from https://github.com/ChannelIQ/jsoncompare with minor modification submitted in this PR ChannelIQ/jsoncompare#8. Without the modification, it is required that the compared JSONs have identical number of keys, which is not logical, since we are specifically interested in ignoring specified keys. Related to QIICR#97
1 parent 0f44ca3 commit 8eb2a0c

1 file changed

Lines changed: 168 additions & 0 deletions

File tree

util/jsoncompare.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import json
2+
from pprint import pprint
3+
4+
class Stack:
5+
def __init__(self):
6+
self.stack_items = []
7+
8+
def append(self, stack_item):
9+
self.stack_items.append(stack_item)
10+
return self
11+
12+
def __repr__(self):
13+
stack_dump = ''
14+
for item in self.stack_items:
15+
stack_dump += str(item)
16+
return stack_dump
17+
18+
def __str__(self):
19+
stack_dump = ''
20+
for item in self.stack_items:
21+
stack_dump += str(item)
22+
return stack_dump
23+
24+
25+
class StackItem:
26+
def __init__(self, reason, expected, actual):
27+
self.reason = reason
28+
self.expected = expected
29+
self.actual = actual
30+
31+
def __repr__(self):
32+
return 'Reason: {0}\nExpected:\n{1}\nActual:\n{2}' \
33+
.format(self.reason, _format_value(self.expected), _format_value(self.actual))
34+
35+
def __str__(self):
36+
return '\n\nReason: {0}\nExpected:\n{1}\nActual:\n{2}' \
37+
.format(self.reason, _format_value(self.expected), _format_value(self.actual))
38+
39+
40+
def _indent(s):
41+
return '\n'.join(' ' + line for line in s.splitlines())
42+
43+
44+
def _format_value(value):
45+
return _indent(_generate_pprint_json(value))
46+
47+
48+
def _generate_pprint_json(value):
49+
return json.dumps(value, sort_keys=True, indent=4)
50+
51+
52+
def _is_dict_same(expected, actual, ignore_value_of_keys):
53+
# DAN - I had to flip flop this
54+
for key in expected:
55+
if not key in actual:
56+
return False, \
57+
Stack().append(
58+
StackItem('Expected key "{0}" Missing from Actual'
59+
.format(key),
60+
expected,
61+
actual))
62+
63+
if not key in ignore_value_of_keys:
64+
# have to change order
65+
#are_same_flag, stack = _are_same(actual[key], expected[key], ignore_value_of_keys)
66+
are_same_flag, stack = _are_same(expected[key], actual[key],ignore_value_of_keys)
67+
if not are_same_flag:
68+
return False, \
69+
stack.append(StackItem('Different values', expected[key], actual[key]))
70+
return True, Stack()
71+
72+
def _is_list_same(expected, actual, ignore_value_of_keys):
73+
for i in xrange(len(expected)):
74+
are_same_flag, stack = _are_same(expected[i], actual[i], ignore_value_of_keys)
75+
if not are_same_flag:
76+
return False, \
77+
stack.append(
78+
StackItem('Different values (Check order)', expected[i], actual[i]))
79+
return True, Stack()
80+
81+
def _bottom_up_sort(unsorted_json):
82+
if isinstance(unsorted_json, list):
83+
new_list = []
84+
for i in xrange(len(unsorted_json)):
85+
new_list.append(_bottom_up_sort(unsorted_json[i]))
86+
return sorted(new_list)
87+
88+
elif isinstance(unsorted_json, dict):
89+
new_dict = {}
90+
for key in sorted(unsorted_json):
91+
new_dict[key] = _bottom_up_sort(unsorted_json[key])
92+
return new_dict
93+
94+
else:
95+
return unsorted_json
96+
97+
def _are_same(expected, actual, ignore_value_of_keys, ignore_missing_keys=False):
98+
# Check for None
99+
if expected is None:
100+
return expected == actual, Stack()
101+
102+
# Ensure they are of same type
103+
if type(expected) != type(actual):
104+
return False, \
105+
Stack().append(
106+
StackItem('Type Mismatch: Expected Type: {0}, Actual Type: {1}'
107+
.format(type(expected), type(actual)),
108+
expected,
109+
actual))
110+
111+
# Compare primitive types immediately
112+
if type(expected) in (int, str, bool, long, float, unicode):
113+
return expected == actual, Stack()
114+
115+
# Ensure collections have the same length (if applicable)
116+
if ignore_missing_keys:
117+
# Ensure collections has minimum length (if applicable)
118+
# This is a short-circuit condition because (b contains a)
119+
if len(expected) > len(actual):
120+
return False, \
121+
Stack().append(
122+
StackItem('Length Mismatch: Minimum Expected Length: {0}, Actual Length: {1}'
123+
.format(len(expected), len(actual)),
124+
expected,
125+
actual))
126+
127+
else:
128+
# Ensure collections has same length
129+
if len(expected) != len(actual):
130+
return False, \
131+
Stack().append(
132+
StackItem('Length Mismatch: Expected Length: {0}, Actual Length: {1}'
133+
.format(len(expected), len(actual)),
134+
expected,
135+
actual))
136+
137+
138+
139+
if isinstance(expected, dict):
140+
return _is_dict_same(expected, actual, ignore_value_of_keys)
141+
142+
if isinstance(expected, list):
143+
return _is_list_same(expected, actual, ignore_value_of_keys)
144+
145+
return False, Stack().append(StackItem('Unhandled Type: {0}'.format(type(expected)), expected, actual))
146+
147+
def are_same(original_a, original_b, ignore_list_order_recursively=False, ignore_missing_keys=False, ignore_value_of_keys=[]):
148+
if ignore_list_order_recursively:
149+
a = _bottom_up_sort(original_a)
150+
b = _bottom_up_sort(original_b)
151+
else:
152+
a = original_a
153+
b = original_b
154+
return _are_same(a, b, ignore_value_of_keys, ignore_missing_keys)
155+
156+
157+
def contains(expected_original, actual_original, ignore_list_order_recursively=False, ignore_value_of_keys=[]):
158+
if ignore_list_order_recursively:
159+
actual = _bottom_up_sort(actual_original)
160+
expected = _bottom_up_sort(expected_original)
161+
else:
162+
actual = actual_original
163+
expected = expected_original
164+
return _are_same(expected, actual, ignore_value_of_keys, True)
165+
166+
def json_are_same(a, b, ignore_list_order_recursively=False, ignore_value_of_keys=[]):
167+
return are_same(json.loads(a), json.loads(b), ignore_list_order_recursively, ignore_value_of_keys)
168+

0 commit comments

Comments
 (0)