Skip to content

Commit 90d5283

Browse files
committed
prep new jdiff compliance testing
1 parent fbed747 commit 90d5283

17 files changed

+208
-119
lines changed

jdiff/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Pre/Post Check library."""
2+
23
from .check_types import CheckType
34
from .extract_data import extract_data_from_json
45

jdiff/check_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""CheckType Implementation."""
2+
23
from typing import List, Tuple, Dict, Any, Union
34
from abc import ABC, abstractmethod
45
from .evaluators import diff_generator, parameter_evaluator, regex_evaluator, operator_evaluator

jdiff/evaluators.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Evaluators."""
2+
23
import re
34
from typing import Any, Mapping, Dict, Tuple, List
45
from deepdiff import DeepDiff

jdiff/extract_data.py

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
)
1717

1818

19-
def extract_data_from_json(
20-
data: Union[Mapping, List], path: str = "*", exclude: Optional[List] = None
21-
) -> Any:
19+
def extract_data_from_json(data: Union[Mapping, List], path: str = "*", exclude: Optional[List] = None) -> Any:
2220
"""Return wanted data from outpdevice data based on the check path. See unit test for complete example.
2321
2422
Get the wanted values to be evaluated if JMESPath expression is defined,
@@ -37,16 +35,12 @@ def extract_data_from_json(
3735
"""
3836
if exclude and isinstance(data, Dict):
3937
if not isinstance(exclude, list):
40-
raise ValueError(
41-
f"Exclude list must be defined as a list. You have {type(exclude)}"
42-
)
38+
raise ValueError(f"Exclude list must be defined as a list. You have {type(exclude)}")
4339
# exclude unwanted elements
4440
exclude_filter(data, exclude)
4541

4642
if not path:
47-
warnings.warn(
48-
"JMSPath cannot be empty string or type 'None'. Path argument reverted to default value '*'"
49-
)
43+
warnings.warn("JMSPath cannot be empty string or type 'None'. Path argument reverted to default value '*'")
5044
path = "*"
5145

5246
if path == "*":
@@ -85,27 +79,19 @@ def extract_data_from_json(
8579
# Based on the expression or data we might have different data types
8680
# therefore we need to normalize.
8781
if re.search(r"\$.*\$", path):
88-
paired_key_value = associate_key_of_my_value(
89-
jmespath_value_parser(path), values
90-
)
82+
paired_key_value = associate_key_of_my_value(jmespath_value_parser(path), values)
9183
wanted_reference_keys = jmespath.search(jmespath_refkey_parser(path), data)
9284

93-
if isinstance(
94-
wanted_reference_keys, dict
95-
): # when wanted_reference_keys is dict() type
85+
if isinstance(wanted_reference_keys, dict): # when wanted_reference_keys is dict() type
9686
list_of_reference_keys = list(wanted_reference_keys.keys())
9787
elif any(
9888
isinstance(element, list) for element in wanted_reference_keys
9989
): # when wanted_reference_keys is a nested list
10090
list_of_reference_keys = flatten_list(wanted_reference_keys)[0]
101-
elif isinstance(
102-
wanted_reference_keys, list
103-
): # when wanted_reference_keys is a list
91+
elif isinstance(wanted_reference_keys, list): # when wanted_reference_keys is a list
10492
list_of_reference_keys = wanted_reference_keys
10593
else:
106-
raise ValueError(
107-
"Reference Key normalization failure. Please verify data type returned."
108-
)
94+
raise ValueError("Reference Key normalization failure. Please verify data type returned.")
10995

11096
normalized = keys_values_zipper(list_of_reference_keys, paired_key_value)
11197
# Data between pre and post may come in different order, so it needs to be sorted.

jdiff/operator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Operator diff."""
2+
23
import operator
34
from typing import Any, List, Tuple
45

jdiff/utils/data_normalization.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Data Normalization utilities."""
2+
23
from typing import List, Generator, Union, Dict
34

45

jdiff/utils/diff_helpers.py

Lines changed: 69 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import re
44
from collections import defaultdict
5-
from functools import partial
5+
from functools import partial, reduce
6+
from operator import getitem
67
from typing import DefaultDict, Dict, List, Mapping
78

89
REGEX_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:
8481
def 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

112141
def 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

jdiff/utils/jmespath_parsers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
From one expression defined in jdiff, we will derive two expressions: one expression that traverse the json output and get the
66
evaluated bit of it, the second will target the reference key relative to the value to evaluate. More on README.md
77
"""
8+
89
import re
910
from typing import Mapping, List, Union
1011

tasks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Tasks for use with Invoke."""
2+
23
import os
34
import sys
45
from distutils.util import strtobool

tests/test_diff_generator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Diff generator tests."""
2+
23
import pytest
34
from jdiff.evaluators import diff_generator
45
from jdiff import extract_data_from_json

0 commit comments

Comments
 (0)