22import configparser
33import filecmp
44import json
5+ import re
56from abc import ABC
67from abc import abstractmethod
78from xml .etree import ElementTree
89
910import dictdiffer
11+ import jsonpath_ng
1012import yaml
13+ from dicttoxml import dicttoxml
1114from diff_pdf_visually import pdf_similar
1215
1316from dir_content_diff .util import diff_msg_formatter
@@ -282,6 +285,14 @@ class DictComparator(BaseComparator):
282285 "add" : "Added the value(s) '{value}' in the '{key}' key." ,
283286 "change" : "Changed the value of '{key}' from {value[0]} to {value[1]}." ,
284287 "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+ ),
285296 }
286297
287298 def __init__ (self , * args , ** kwargs ):
@@ -318,6 +329,43 @@ def _format_change_value(value):
318329 value [num ] = str (i )
319330 return value
320331
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+
321369 def diff (self , ref , comp , * args , ** kwargs ):
322370 """Compare 2 dictionaries.
323371
@@ -332,13 +380,16 @@ def diff(self, ref, comp, *args, **kwargs):
332380 path_limit (list[str]): List of path limit tuples or :class:`dictdiffer.utils.PathLimit`
333381 object to limit the diff recursion depth.
334382 """
383+ errors = self .current_state .get ("format_errors" , [])
384+
335385 if len (args ) > 5 :
336386 dot_notation = args [5 ]
337387 args = args [:5 ] + args [6 :]
338388 else :
339389 dot_notation = kwargs .pop ("dot_notation" , False )
340390 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
342393
343394 def format_diff (self , difference ):
344395 """Format one element difference."""
@@ -361,6 +412,11 @@ def load(self, path):
361412 data = json .load (file )
362413 return data
363414
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+
364420
365421class YamlComparator (DictComparator ):
366422 """Comparator for YAML files.
@@ -374,6 +430,11 @@ def load(self, path):
374430 data = yaml .full_load (file )
375431 return data
376432
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+
377438
378439class XmlComparator (DictComparator ):
379440 """Comparator for XML files.
@@ -407,9 +468,14 @@ def load(self, path): # pylint: disable=arguments-differ
407468 data = self .xmltodict (file .read ())
408469 return data
409470
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+
410476 @staticmethod
411477 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."""
413479 if "type" not in attr :
414480 return text
415481 value_type = attr .get ("type" , "" ).lower ()
@@ -453,7 +519,7 @@ def add_to_output(obj, child):
453519
454520 @staticmethod
455521 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."""
457523 root = ElementTree .fromstring (obj )
458524 output = {}
459525
@@ -473,11 +539,16 @@ class IniComparator(DictComparator):
473539 """
474540
475541 def load (self , path , ** kwargs ): # pylint: disable=arguments-differ
476- """Open a XML file."""
542+ """Open a INI file."""
477543 data = configparser .ConfigParser (** kwargs )
478544 data .read (path )
479545 return self .configparser_to_dict (data )
480546
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+
481552 @staticmethod
482553 def configparser_to_dict (config ):
483554 """Transform a ConfigParser object into a dict."""
@@ -494,6 +565,16 @@ def configparser_to_dict(config):
494565 dict_config [section ][option ] = val
495566 return dict_config
496567
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+
497578
498579class PdfComparator (BaseComparator ):
499580 """Comparator for PDF files."""
0 commit comments