-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDiffJson.py
More file actions
373 lines (321 loc) · 13.5 KB
/
DiffJson.py
File metadata and controls
373 lines (321 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# coding=UTF-8
from __future__ import print_function
""" diff JSON objects """
__date__ = "2016-02-26"
__author__ = "Fabian Sandoval Saldias"
__email__ = "fabianvss@gmail.com"
import json
import copy
from collections import OrderedDict
import sys
import contextlib
import re
"""
Note: JSON files are loaded with OrderedDict when supported (Python >= 2.7).
Two examples resulting in the following output (but colored).
Output (when initialized with OrderedDict instead of regular dict):
~ a.x: 1 > 2
< a.y: true
> b: null
Initialization (with regular dict for simplicity in this example):
diffJson = DiffJson( {'a':{'x':1,'y':True}}, {'a':{'x':2},'b':None} )
diffJson.colored = True
Option 1: Simple use case:
diffJson.printDiff( 4 )
Option 2: With a user defined printer:
def diffPrinter( item ):
diffPrinter.result.append( item )
diffPrinter.result = []
diffJson( diffPrinter )
print( ' ' + '\n '.join( diffPrinter.result ) )
You can walk the JSON before comparison with select1() and select2(). The
keys have to be separated with the string defined by the property
pathDelimiter. With the property useSquareBrackets you decide whether to
use the array index syntax (that is, key[value] instead of key.value).
To ignore specific paths (relative to the final root paths after the usages
of select1() and select2()), add them to the list held by the property
ignorePaths. The key has to be separated with the string defined by the
property pathDelimiter.
You can deserialize specific string values to JSON while walking. To do that
preceed the key with the deserialize operator. To turn this feature on
set the property deserializeOperator to a string value; None turns this
off.
The API supports to change furthermore:
- how to print modified values via property modifiedValueFormatter
- the prefixes via setPrefixes()
- the colors used via setColors()
- option to not print irrelevant sub items via property minimal
"""
class DiffJson(object):
class Dye(object):
def __init__( self ):
self._colored = False
self._c_added = '\033[92m' # Green
self._c_removed = '\033[91m' # Red
self._c_modified = '\033[94m' # Blue
self.__c_end = '\033[0m' # Default shell color
@property
def colored( self ):
return self._colored
@colored.setter
def colored( self, value ):
self._colored = value
def setColors( self, added, removed, modified ):
self._c_added = added
self._c_removed = removed
self._c_modified = modified
def added( self, text ): return self._coloredText( text, self._c_added )
def removed( self, text ): return self._coloredText( text, self._c_removed )
def modified( self, text ): return self._coloredText( text, self._c_modified )
def _coloredText( self, text, color ):
return color + text + self.__c_end if self.colored and sys.stdout.isatty() else text
def __init__( self, json1, json2 ):
self._json1 = json1
self._json2 = json2
self._ignorePaths = []
self._dye = self.Dye()
self._modifiedValueFormatter = lambda v1, v2: v1 + ' > ' + v2
self._prefix_added = '> '
self._prefix_removed = '< '
self._prefix_modified = '~ '
self._pathDelimiter = '.'
self._deserializeOperator = None
self._useSquareBrackets = True
self._minimal = False
@classmethod
def fromPaths( cls, path1, path2 ):
with contextlib.nested( open( path1, 'r' ), open( path2, 'r' ) ) as (file1, file2):
return cls.fromFiles( file1, file2 )
@classmethod
def fromFiles( cls, file1, file2 ):
if sys.version_info >= (2, 7):
json1 = json.load( file1, object_pairs_hook=OrderedDict )
json2 = json.load( file2, object_pairs_hook=OrderedDict )
else:
json1 = json.load( file1 )
json2 = json.load( file2 )
return cls( json1, json2 )
def select1( self, path ):
sub = DiffJson.getPath( self._json1, path, self._pathDelimiter, self._deserializeOperator, self._useSquareBrackets )
if sub is not None:
self._json1 = sub
return True
else:
return False
def select2( self, path ):
sub = DiffJson.getPath( self._json2, path, self._pathDelimiter, self._deserializeOperator, self._useSquareBrackets )
if sub is not None:
self._json2 = sub
return True
else:
return False
def __call__( self, printer ):
self.__printer = printer
self.__diff()
def printDiff( self, indentation = 0 ):
self( lambda item: print( ' ' * indentation + item ) )
@property
def ignorePaths( self ):
"""
@rtype: [basestring]
"""
return self._ignorePaths
@ignorePaths.setter
def ignorePaths( self, value ):
"""
@type value: [basestring]
"""
self._ignorePaths = value
@property
def colored( self ):
return self._dye.colored
@colored.setter
def colored( self, value ):
self._dye.colored = value
@property
def modifiedValueFormatter( self ):
"""
@rtype: (basestring, basestring) -> basestring
"""
return self._modifiedValueFormatter
@modifiedValueFormatter.setter
def modifiedValueFormatter( self, value ):
"""
@type value: (basestring, basestring) -> basestring
"""
self._modifiedValueFormatter = value
def setPrefixes( self, added, removed, modified ):
self._prefix_added = added
self._prefix_removed = removed
self._prefix_modified = modified
def setColors( self, added, removed, modified ):
self._dye.setColors( added, removed, modified )
@property
def prefix_added( self ):
return self._prefix_added
@property
def prefix_removed( self ):
return self._prefix_removed
@property
def prefix_modified( self ):
return self._prefix_modified
@property
def pathDelimiter( self ):
return self._pathDelimiter
@pathDelimiter.setter
def pathDelimiter( self, value ):
self._pathDelimiter = value
@property
def deserializeOperator( self ):
"""
@rtype: basestring|None
"""
return self._deserializeOperator
@deserializeOperator.setter
def deserializeOperator( self, value ):
"""
@type value: basestring|None
"""
self._deserializeOperator = value
@property
def useSquareBrackets( self ):
return self._useSquareBrackets
@useSquareBrackets.setter
def useSquareBrackets( self, value ):
self._useSquareBrackets = value
@property
def minimal( self ):
return self._minimal
@colored.setter
def minimal( self, value ):
self._minimal = value
@staticmethod
def getPath( jsonDictOrList, path, delimiter, deserializeOperator, useSquareBrackets ):
"""
@type deserializeOperator: basestring|None
@param deserializeOperator: If not None, a path item with preceeding
deserializeOperator will cause the string value getting deserialized
into a json value. But only, if the path item is not an existing
key. Because then, we assume, that the user do not want to
deserialize but just navigate the key. If he intends to actually
deserialize, he must give another deserializeOperator, that does not
conflict with existing keys.
"""
elem = jsonDictOrList
try:
paths = path.strip( delimiter ).split( delimiter )
if useSquareBrackets:
# convert key[value] to key.value
newPaths = []
for x in paths:
if not deserializeOperator:
regex = r'^.*\[([0-9]+)\]$'
else:
regex = r'^.*\[(' + re.escape(deserializeOperator) + r'[0-9]+)\]$'
m = re.match( regex, x )
if m:
indexString = m.group( 1 )
newPaths.append( x[:len( x ) - (len( indexString ) + 2)] )
newPaths.append( indexString )
else:
newPaths.append( x )
paths = newPaths
for x in paths:
deserializeValue = False
if deserializeOperator is not None and not x in elem and len(x) >= len(deserializeOperator) and x[:len(deserializeOperator)] == deserializeOperator:
x = x[len(deserializeOperator):]
deserializeValue = True
if isinstance( elem, dict ):
elem = elem[x]
elif isinstance( elem, list ):
elem = elem[int(x)]
if deserializeValue:
if sys.version_info >= (2, 7):
elem = json.loads( elem, object_pairs_hook=OrderedDict )
else:
elem = json.loads( elem )
except (KeyError, IndexError):
return None
return elem
def _coloredKey( self, path, key, dyer ):
if self._useSquareBrackets and isinstance( key, int ):
return path + dyer( '[' + unicode(key) + ']' )
else:
if path == '':
return dyer( unicode(key) )
else:
return path + self._pathDelimiter + dyer( unicode( key ) )
def _combinePath( self, path, key ):
if self._useSquareBrackets and isinstance( key, int ):
return path + '[' + unicode(key) + ']'
else:
if path == '':
return unicode(key)
else:
return path + self._pathDelimiter + unicode( key )
@staticmethod
def _prettyValue( value ):
if isinstance( value, dict ) or isinstance( value, list ):
return json.dumps( value, sort_keys = sys.version_info < (2, 7) )
elif isinstance( value, bool ):
return 'true' if value else 'false'
elif value is None:
return 'null'
elif isinstance( value, int ) or isinstance( value, float ):
return unicode( value )
else:
return '"' + value + '"'
def __diff( self ):
self.__diffValue( '', '', self._json1, self._json2 )
def __diffDict( self, path, original, modified ):
remaining = copy.deepcopy( modified )
for key, value in original.iteritems():
if key in modified:
del remaining[key]
if self._combinePath( path, key ) in self._ignorePaths:
continue
if key in modified:
self.__diffValue( path, key, value, modified[key] )
else:
self.__printer( self.prefix_removed + self._coloredKey( path, key, self._dye.removed ) + ': ' +
(DiffJson._prettyValue( value ) if not self._minimal else '[...]')
)
for key, value in remaining.iteritems():
if self._combinePath( path, key ) in self._ignorePaths:
continue
self.__printer( self.prefix_added + self._coloredKey( path, key, self._dye.added ) + ': ' +
(DiffJson._prettyValue( value ) if not self._minimal else '[...]')
)
def __diffList( self, path, original, modified ):
for key, value in enumerate( original ):
if self._combinePath( path, key ) in self._ignorePaths:
continue
if key < len(modified):
self.__diffValue( path, key, value, modified[key] )
else:
self.__printer( self.prefix_removed + self._coloredKey( path, key, self._dye.removed ) + ': ' +
(DiffJson._prettyValue( value ) if not self._minimal else '[...]')
)
for key, value in enumerate( modified[len(original):] ):
if self._combinePath( path, key ) in self._ignorePaths:
continue
self.__printer( self.prefix_added + self._coloredKey( path, key + len(original), self._dye.added ) + ': ' +
(DiffJson._prettyValue( value ) if not self._minimal else '[...]')
)
def __diffValue( self, path, key, original, modified ):
if original != modified:
fullPath = self._combinePath( path, key )
if fullPath in self._ignorePaths:
return
if isinstance( original, dict ) and isinstance( modified, dict ):
self.__diffDict( fullPath, original, modified )
elif isinstance( original, list ) and isinstance( modified, list ):
self.__diffList( fullPath, original, modified )
else:
value = self.modifiedValueFormatter(
self._dye.removed( DiffJson._prettyValue( original ) ),
self._dye.added( DiffJson._prettyValue( modified ) )
)
self.__printer( self.prefix_modified + self._coloredKey( path, key, self._dye.modified ) + ': ' +
unicode( value )
)