22
33import re
44from collections import defaultdict
5- from functools import partial
5+ from functools import partial , reduce
6+ from operator import getitem
67from typing import DefaultDict , Dict , List , Mapping
78
89REGEX_PATTERN_RELEVANT_KEYS = r"'([A-Za-z0-9_\./\\-]*)'"
@@ -63,12 +64,8 @@ def fix_deepdiff_key_names(obj: Mapping) -> Dict:
6364 result = {} # type: Dict
6465 for key , value in obj .items ():
6566 key_parts = re .findall (REGEX_PATTERN_RELEVANT_KEYS , key )
66- if (
67- not key_parts
68- ): # If key parts can't be find, keep original key so data is not lost.
69- key_parts = [
70- key .replace ("root" , "index_element" )
71- ] # replace root from DeepDiff with more meaningful name.
67+ if not key_parts : # If key parts can't be find, keep original key so data is not lost.
68+ key_parts = [key .replace ("root" , "index_element" )] # replace root from DeepDiff with more meaningful name.
7269 partial_res = group_value (key_parts , value )
7370 dict_merger (result , partial_res )
7471 return result
@@ -84,16 +81,10 @@ def group_value(tree_list: List, value: Dict) -> Dict:
8481def dict_merger (original_dict : Dict , dict_to_merge : Dict ):
8582 """Function to merge a dictionary (dict_to_merge) recursively into the original_dict."""
8683 for key in dict_to_merge .keys ():
87- if (
88- key in original_dict
89- and isinstance (original_dict [key ], dict )
90- and isinstance (dict_to_merge [key ], dict )
91- ):
84+ if key in original_dict and isinstance (original_dict [key ], dict ) and isinstance (dict_to_merge [key ], dict ):
9285 dict_merger (original_dict [key ], dict_to_merge [key ])
9386 elif key in original_dict .keys ():
94- original_dict [key + "_dup!" ] = dict_to_merge [
95- key
96- ] # avoid overwriting existing keys.
87+ original_dict [key + "_dup!" ] = dict_to_merge [key ] # avoid overwriting existing keys.
9788 else :
9889 original_dict [key ] = dict_to_merge [key ]
9990
@@ -106,7 +97,45 @@ def _parse_index_element_string(index_element_string):
10697 if match :
10798 for inner_key in match [1 ::]:
10899 result [inner_key ] = ""
109- return result
100+ return match , result
101+
102+
103+ def set_nested_value (data , keys , value ):
104+ """
105+ Recursively sets a value in a nested dictionary, given a list of keys.
106+
107+ Args:
108+ data (dict): The nested dictionary to modify.
109+ keys (list): A list of keys to access the target value.
110+ value: The value to set.
111+
112+ Returns:
113+ None: The function modifies the dictionary in place. Returns None.
114+ """
115+ if not keys :
116+ return # Should not happen, but good to have.
117+ if len (keys ) == 1 :
118+ data [keys [0 ]] = value
119+ else :
120+ if keys [0 ] not in data :
121+ data [keys [0 ]] = {} # Create the nested dictionary if it doesn't exist
122+ set_nested_value (data [keys [0 ]], keys [1 :], value )
123+
124+
125+ def all_values_empty (input_dict ):
126+ """
127+ Checks if all values in a dictionary are empty objects (empty string, list, or dictionary).
128+
129+ Args:
130+ input_dict: The dictionary to check.
131+
132+ Returns:
133+ True if all values are empty, False otherwise.
134+ """
135+ for value in input_dict .values ():
136+ if value : # Empty objects evaluate to False in a boolean context
137+ return False
138+ return True
110139
111140
112141def parse_diff (jdiff_evaluate_response , actual , intended , match_config ):
@@ -116,67 +145,39 @@ def parse_diff(jdiff_evaluate_response, actual, intended, match_config):
116145
117146 def process_diff (_map , extra_map , missing_map ):
118147 for key , value in _map .items ():
119- if (
120- isinstance (value , dict )
121- and "new_value" in value
122- and "old_value" in value
123- ):
148+ if isinstance (value , dict ) and "new_value" in value and "old_value" in value :
124149 extra_map [key ] = value ["old_value" ]
125150 missing_map [key ] = value ["new_value" ]
126151 elif isinstance (value , str ):
127152 if "missing" in value :
128153 extra_map [key ] = actual .get (match_config , {}).get (key )
129154 if "new" in value :
130- new_key = _parse_index_element_string (key )
131- new_key [key ] = intended .get (match_config , {}).get (key )
132- missing_map .update (new_key )
155+ key_chain , _ = _parse_index_element_string (key )
156+ new_value = reduce (getitem , key_chain , intended )
157+ set_nested_value (missing_map , key_chain [1 ::], new_value )
158+ elif isinstance (value , defaultdict ):
159+ if dict (value ).get ("new" ):
160+ missing [key ] = dict (value ).get ("new" , {})
161+ if dict (value ).get ("missing" ):
162+ extra_map [key ] = dict (value ).get ("missing" , {})
133163 elif isinstance (value , dict ):
134164 extra_map [key ] = {}
135165 missing_map [key ] = {}
136166 process_diff (value , extra_map [key ], missing_map [key ])
137167 return extra_map , missing_map
138168
139- extras , missings = process_diff (jdiff_evaluate_response , extra , missing )
140- return extras , missings
141-
142-
143- # result = {'hostname': {'new_value': 'veos', 'old_value': 'veos-0'}, 'domain-name': 'missing'}
144- # result = {'domain-name': 'missing'}
145- # result = {'hostname': {'new_value': 'veos', 'old_value': 'veos-0'}, 'domain-name': 'missing', "index_element['openconfig-system:config']['ip name']": 'new'}
146- # result = {'domain-name': 'missing','hostname': 'missing', "index_element['openconfig-system:config']['ip name']": 'new'}
147- # result = {'servers': {'server': defaultdict(<class 'list'>, {'missing': [{'address': '1.us.pool.ntp.org', 'config': {'address': '1.us.pool.ntp.org'}, 'state': {'address': '1.us.pool.ntp.org'}}]})}}
148-
149- # '''
150- # ```
151- # from jdiff import CheckType
152-
153- # a = {
154- # "openconfig-system:ntp": {
155- # "servers": {
156- # "server": [
157- # {
158- # "address": "1.us.pool.ntp.org",
159- # "config": {
160- # "address": "1.us.pool.ntp.org"
161- # },
162- # "state": {
163- # "address": "1.us.pool.ntp.org"
164- # }
165- # }
166- # ]
167- # }
168- # }
169- # }
170-
171- # i = {
172- # "openconfig-system:ntp": {
173- # "servers": {
174- # "server": []
175- # }
176- # }
177- # }
178-
179- # jdiff_param_match = CheckType.create("exact_match")
180- # result, compliant = jdiff_param_match.evaluate(a, i)
181- # ```
182- # '''
169+ extras , missing = process_diff (jdiff_evaluate_response , extra , missing )
170+ # Don't like this, but with less the performant way of doing it right now it works to clear out
171+ # Any empty dicts that are left over from the diff.
172+ # This is a bit of a hack, but it works for now.
173+ final_extras = extras .copy ()
174+ final_missing = missing .copy ()
175+ for key , value in extras .items ():
176+ if isinstance (value , dict ):
177+ if not value :
178+ del final_extras [key ]
179+ for key , value in missing .items ():
180+ if isinstance (value , dict ):
181+ if not value :
182+ del final_missing [key ]
183+ return final_extras , final_missing
0 commit comments