2 ##########################################################################
4 # Copyright 2011 Jose Fonseca
7 # Permission is hereby granted, free of charge, to any person obtaining a copy
8 # of this software and associated documentation files (the "Software"), to deal
9 # in the Software without restriction, including without limitation the rights
10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 # copies of the Software, and to permit persons to whom the Software is
12 # furnished to do so, subject to the following conditions:
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 ##########################################################################/
35 def strip_object_hook(obj
):
36 if '__class__' in obj
:
38 for name
in obj
.keys():
39 if name
.startswith('__') and name
.endswith('__'):
46 def visit(self
, node
, *args
, **kwargs
):
47 if isinstance(node
, dict):
48 return self
.visitObject(node
, *args
, **kwargs
)
49 elif isinstance(node
, list):
50 return self
.visitArray(node
, *args
, **kwargs
)
52 return self
.visitValue(node
, *args
, **kwargs
)
54 def visitObject(self
, node
, *args
, **kwargs
):
57 def visitArray(self
, node
, *args
, **kwargs
):
60 def visitValue(self
, node
, *args
, **kwargs
):
64 class Dumper(Visitor
):
66 def __init__(self
, stream
= sys
.stdout
):
74 self
._write
(' '*self
.level
)
79 def visitObject(self
, node
):
84 for i
in range(len(members
)):
87 self
.enter_member(name
)
89 self
.leave_member(i
== len(members
) - 1)
92 def enter_object(self
):
97 def enter_member(self
, name
):
99 self
._write
('%s: ' % name
)
101 def leave_member(self
, last
):
106 def leave_object(self
):
113 def visitArray(self
, node
):
115 for i
in range(len(node
)):
119 if i
!= len(node
) - 1:
124 def enter_array(self
):
129 def leave_array(self
):
134 def visitValue(self
, node
):
135 self
._write
(json
.dumps(node
, allow_nan
=True))
139 class Comparer(Visitor
):
141 def __init__(self
, ignore_added
= False, tolerance
= 2.0 ** -24):
142 self
.ignore_added
= ignore_added
143 self
.tolerance
= tolerance
145 def visitObject(self
, a
, b
):
146 if not isinstance(b
, dict):
148 if len(a
) != len(b
) and not self
.ignore_added
:
154 if ak
!= bk
and not self
.ignore_added
:
162 if not self
.visit(ae
, be
):
166 def visitArray(self
, a
, b
):
167 if not isinstance(b
, list):
171 for ae
, be
in zip(a
, b
):
172 if not self
.visit(ae
, be
):
176 def visitValue(self
, a
, b
):
177 if isinstance(a
, float) or isinstance(b
, float):
179 return abs(b
) < self
.tolerance
181 return abs((b
- a
)/a
) < self
.tolerance
186 class Differ(Visitor
):
188 def __init__(self
, stream
= sys
.stdout
, ignore_added
= False):
189 self
.dumper
= Dumper(stream
)
190 self
.comparer
= Comparer(ignore_added
= ignore_added
)
192 def visit(self
, a
, b
):
193 if self
.comparer
.visit(a
, b
):
195 Visitor
.visit(self
, a
, b
)
197 def visitObject(self
, a
, b
):
198 if not isinstance(b
, dict):
201 self
.dumper
.enter_object()
202 names
= set(a
.keys())
203 if not self
.comparer
.ignore_added
:
204 names
.update(b
.keys())
208 for i
in range(len(names
)):
210 ae
= a
.get(name
, None)
211 be
= b
.get(name
, None)
212 if not self
.comparer
.visit(ae
, be
):
213 self
.dumper
.enter_member(name
)
215 self
.dumper
.leave_member(i
== len(names
) - 1)
217 self
.dumper
.leave_object()
219 def visitArray(self
, a
, b
):
220 if not isinstance(b
, list):
223 self
.dumper
.enter_array()
224 max_len
= max(len(a
), len(b
))
225 for i
in range(max_len
):
234 self
.dumper
._indent
()
235 if self
.comparer
.visit(ae
, be
):
236 self
.dumper
.visit(ae
)
240 self
.dumper
._write
(',')
241 self
.dumper
._newline
()
243 self
.dumper
.leave_array()
245 def visitValue(self
, a
, b
):
249 def replace(self
, a
, b
):
250 if isinstance(a
, basestring
) and isinstance(b
, basestring
):
251 if '\n' in a
or '\n' in b
:
254 differ
= difflib
.Differ()
255 result
= differ
.compare(a
, b
)
256 self
.dumper
.level
+= 1
258 self
.dumper
._newline
()
259 self
.dumper
._indent
()
270 line
= tag
+ prefix
+ text
+ suffix
271 self
.dumper
._write
(line
)
272 self
.dumper
.level
-= 1
275 self
.dumper
._write
(' -> ')
278 def isMultilineString(self
, value
):
279 return isinstance(value
, basestring
) and '\n' in value
281 def replaceMultilineString(self
, a
, b
):
283 self
.dumper
._write
(' -> ')
288 # Unfortunately JSON standard does not include comments, but this is a quite
289 # useful feature to have on regressions tests
293 r
'//[^\r\n]*', # comment
294 r
'"[^"\\]*(\\.[^"\\]*)*"', # string
297 _tokens_re
= re
.compile(r
'|'.join(['(' + token_re
+ ')' for token_re
in _token_res
]), re
.DOTALL
)
300 def _strip_comment(mo
):
307 def _strip_comments(data
):
308 '''Strip (non-standard) JSON comments.'''
309 return _tokens_re
.sub(_strip_comment
, data
)
312 assert _strip_comments('''// a comment
313 "// a comment in a string
315 "// a comment in a string
319 def load(stream
, strip_images
= True, strip_comments
= True):
321 object_hook
= strip_object_hook
326 data
= _strip_comments(data
)
327 return json
.loads(data
, strict
=False, object_hook
= object_hook
)
329 return json
.load(stream
, strict
=False, object_hook
= object_hook
)
333 optparser
= optparse
.OptionParser(
334 usage
="\n\t%prog [options] <ref_json> <src_json>")
335 optparser
.add_option(
337 action
="store_false", dest
="strip_images", default
=True,
338 help="compare images")
340 (options
, args
) = optparser
.parse_args(sys
.argv
[1:])
343 optparser
.error('incorrect number of arguments')
345 a
= load(open(sys
.argv
[1], 'rt'), options
.strip_images
)
346 b
= load(open(sys
.argv
[2], 'rt'), options
.strip_images
)
356 if __name__
== '__main__':