2
2
import configparser
3
3
import filecmp
4
4
import json
5
+ import re
5
6
from abc import ABC
6
7
from abc import abstractmethod
7
8
from xml .etree import ElementTree
8
9
9
10
import dictdiffer
11
+ import jsonpath_ng
10
12
import yaml
13
+ from dicttoxml import dicttoxml
11
14
from diff_pdf_visually import pdf_similar
12
15
13
16
from dir_content_diff .util import diff_msg_formatter
@@ -282,6 +285,14 @@ class DictComparator(BaseComparator):
282
285
"add" : "Added the value(s) '{value}' in the '{key}' key." ,
283
286
"change" : "Changed the value of '{key}' from {value[0]} to {value[1]}." ,
284
287
"remove" : "Removed the value(s) '{value}' from '{key}' key." ,
288
+ "missing_ref_entry" : (
289
+ "The path '{key}' is missing in the reference dictionary, please fix the "
290
+ "'replace_pattern' argument."
291
+ ),
292
+ "missing_comp_entry" : (
293
+ "The path '{key}' is missing in the compared dictionary, please fix the "
294
+ "'replace_pattern' argument."
295
+ ),
285
296
}
286
297
287
298
def __init__ (self , * args , ** kwargs ):
@@ -318,6 +329,43 @@ def _format_change_value(value):
318
329
value [num ] = str (i )
319
330
return value
320
331
332
+ def format_data (self , data , ref = None , replace_pattern = None , ** kwargs ):
333
+ """Format the loaded data."""
334
+ # pylint: disable=too-many-nested-blocks
335
+ self .current_state ["format_errors" ] = errors = []
336
+
337
+ if replace_pattern is not None :
338
+ for pat , paths in replace_pattern .items ():
339
+ pattern = pat [0 ]
340
+ new_value = pat [1 ]
341
+ count = pat [2 ] if len (pat ) > 2 else 0
342
+ flags = pat [3 ] if len (pat ) > 3 else 0
343
+ for raw_path in paths :
344
+ path = jsonpath_ng .parse (raw_path )
345
+ if ref is not None and len (path .find (ref )) == 0 :
346
+ errors .append (
347
+ (
348
+ "missing_ref_entry" ,
349
+ raw_path ,
350
+ None ,
351
+ )
352
+ )
353
+ elif len (path .find (data )) == 0 :
354
+ errors .append (
355
+ (
356
+ "missing_comp_entry" ,
357
+ raw_path ,
358
+ None ,
359
+ )
360
+ )
361
+ else :
362
+ for i in path .find (data ):
363
+ if isinstance (i .value , str ):
364
+ i .full_path .update (
365
+ data , re .sub (pattern , new_value , i .value , count , flags )
366
+ )
367
+ return data
368
+
321
369
def diff (self , ref , comp , * args , ** kwargs ):
322
370
"""Compare 2 dictionaries.
323
371
@@ -332,13 +380,16 @@ def diff(self, ref, comp, *args, **kwargs):
332
380
path_limit (list[str]): List of path limit tuples or :class:`dictdiffer.utils.PathLimit`
333
381
object to limit the diff recursion depth.
334
382
"""
383
+ errors = self .current_state .get ("format_errors" , [])
384
+
335
385
if len (args ) > 5 :
336
386
dot_notation = args [5 ]
337
387
args = args [:5 ] + args [6 :]
338
388
else :
339
389
dot_notation = kwargs .pop ("dot_notation" , False )
340
390
kwargs ["dot_notation" ] = dot_notation
341
- return list (dictdiffer .diff (ref , comp , * args , ** kwargs ))
391
+ errors .extend (list (dictdiffer .diff (ref , comp , * args , ** kwargs )))
392
+ return errors
342
393
343
394
def format_diff (self , difference ):
344
395
"""Format one element difference."""
@@ -361,6 +412,11 @@ def load(self, path):
361
412
data = json .load (file )
362
413
return data
363
414
415
+ def save (self , data , path ):
416
+ """Save formatted data into a JSON file."""
417
+ with open (path , "w" , encoding = "utf-8" ) as file :
418
+ json .dump (data , file )
419
+
364
420
365
421
class YamlComparator (DictComparator ):
366
422
"""Comparator for YAML files.
@@ -374,6 +430,11 @@ def load(self, path):
374
430
data = yaml .full_load (file )
375
431
return data
376
432
433
+ def save (self , data , path ):
434
+ """Save formatted data into a YAML file."""
435
+ with open (path , "w" , encoding = "utf-8" ) as file :
436
+ yaml .dump (data , file )
437
+
377
438
378
439
class XmlComparator (DictComparator ):
379
440
"""Comparator for XML files.
@@ -407,9 +468,14 @@ def load(self, path): # pylint: disable=arguments-differ
407
468
data = self .xmltodict (file .read ())
408
469
return data
409
470
471
+ def save (self , data , path ):
472
+ """Save formatted data into a XML file."""
473
+ with open (path , "w" , encoding = "utf-8" ) as file :
474
+ file .write (dicttoxml (data ["root" ]).decode ())
475
+
410
476
@staticmethod
411
477
def _cast_from_attribute (text , attr ):
412
- """Converts XML text into a Python data format based on the tag attribute."""
478
+ """Convert XML text into a Python data format based on the tag attribute."""
413
479
if "type" not in attr :
414
480
return text
415
481
value_type = attr .get ("type" , "" ).lower ()
@@ -453,7 +519,7 @@ def add_to_output(obj, child):
453
519
454
520
@staticmethod
455
521
def xmltodict (obj ):
456
- """Converts an XML string into a Python object based on each tag's attribute."""
522
+ """Convert an XML string into a Python object based on each tag's attribute."""
457
523
root = ElementTree .fromstring (obj )
458
524
output = {}
459
525
@@ -473,11 +539,16 @@ class IniComparator(DictComparator):
473
539
"""
474
540
475
541
def load (self , path , ** kwargs ): # pylint: disable=arguments-differ
476
- """Open a XML file."""
542
+ """Open a INI file."""
477
543
data = configparser .ConfigParser (** kwargs )
478
544
data .read (path )
479
545
return self .configparser_to_dict (data )
480
546
547
+ def save (self , data , path ):
548
+ """Save formatted data into a INI file."""
549
+ with open (path , "w" , encoding = "utf-8" ) as file :
550
+ self .dict_to_configparser (data ).write (file )
551
+
481
552
@staticmethod
482
553
def configparser_to_dict (config ):
483
554
"""Transform a ConfigParser object into a dict."""
@@ -494,6 +565,16 @@ def configparser_to_dict(config):
494
565
dict_config [section ][option ] = val
495
566
return dict_config
496
567
568
+ @staticmethod
569
+ def dict_to_configparser (data , ** kwargs ):
570
+ """Transform a dict object into a ConfigParser."""
571
+ config = configparser .ConfigParser (** kwargs )
572
+ for k , v in data .items ():
573
+ config .add_section (k )
574
+ for opt , val in v .items ():
575
+ config [k ][opt ] = json .dumps (val )
576
+ return config
577
+
497
578
498
579
class PdfComparator (BaseComparator ):
499
580
"""Comparator for PDF files."""
0 commit comments