Source code for twin4build.utils.dict_utils

# Utility functions for dictionary operations

# Standard library imports
from typing import Any, Dict

# Local application imports
from twin4build.utils.rhasattr import rhasattr


[docs] def compare_dict_structure( dict1: Dict[str, Any], dict2: Dict[str, Any], path: str = "" ) -> Dict[str, Any]: """ Compare the structure of two nested dictionaries. This function checks that both dictionaries have the same keys at all nested levels, without requiring the actual values to match. It returns detailed information about any differences found. Args: dict1: First dictionary to compare dict2: Second dictionary to compare path: Current path in the dictionary for error reporting Returns: Dict containing comparison results: - 'structures_match': bool - True if structures match, False otherwise - 'missing_in_2': set - keys in dict1 but not in dict2 - 'missing_in_1': set - keys in dict2 but not in dict1 Example: >>> dict1 = {"a": {"b": 1, "c": 2}, "d": 3} >>> dict2 = {"a": {"b": 10, "c": 20}, "d": 30} >>> result = compare_dict_structure(dict1, dict2) >>> result['structures_match'] True >>> dict1 = {"a": {"b": 1, "c": 2}, "d": 3} >>> dict2 = {"a": {"b": 10}, "e": 4} # Missing "c" and "d", extra "e" >>> result = compare_dict_structure(dict1, dict2) >>> result['structures_match'] False >>> result['missing_in_2'] {'d', 'a.c'} >>> result['missing_in_1'] {'e'} """ result = {"structures_match": True, "missing_in_2": set(), "missing_in_1": set()} if not isinstance(dict1, dict) or not isinstance(dict2, dict): return result keys1 = set(dict1.keys()) keys2 = set(dict2.keys()) missing_in_2 = keys1 - keys2 missing_in_1 = keys2 - keys1 if missing_in_2 or missing_in_1: result["structures_match"] = False if missing_in_2: missing_keys = {f"{path}.{key}" if path else key for key in missing_in_2} result["missing_in_2"].update(missing_keys) if missing_in_1: extra_keys = {f"{path}.{key}" if path else key for key in missing_in_1} result["missing_in_1"].update(extra_keys) # Check nested dictionaries for key in keys1.intersection(keys2): current_path = f"{path}.{key}" if path else key if isinstance(dict1[key], dict) and isinstance(dict2[key], dict): nested_result = compare_dict_structure(dict1[key], dict2[key], current_path) if not nested_result["structures_match"]: result["structures_match"] = False result["missing_in_2"].update(nested_result["missing_in_2"]) result["missing_in_1"].update(nested_result["missing_in_1"]) return result
[docs] def get_dict_differences( dict1: Dict[str, Any], dict2: Dict[str, Any], path: str = "" ) -> Dict[str, Any]: """ Get detailed differences between two nested dictionaries. Args: dict1: First dictionary to compare dict2: Second dictionary to compare path: Current path in the dictionary for error reporting Returns: Dict containing information about differences: - 'missing_in_2': keys in dict1 but not in dict2 - 'missing_in_1': keys in dict2 but not in dict1 - 'structure_mismatch': True if structures don't match - 'differences': nested dictionary with specific differences Example: >>> dict1 = {"a": {"b": 1, "c": 2}, "d": 3} >>> dict2 = {"a": {"b": 10}, "e": 4} >>> get_dict_differences(dict1, dict2) { 'missing_in_2': {'d', 'a.c'}, 'missing_in_1': {'e'}, 'structure_mismatch': True, 'differences': {'a': {'missing_in_2': {'c'}}} } """ result = { "missing_in_2": set(), "missing_in_1": set(), "structure_mismatch": False, "differences": {}, } if not isinstance(dict1, dict) or not isinstance(dict2, dict): return result keys1 = set(dict1.keys()) keys2 = set(dict2.keys()) missing_in_2 = keys1 - keys2 missing_in_1 = keys2 - keys1 if missing_in_2 or missing_in_1: result["structure_mismatch"] = True if missing_in_2: result["missing_in_2"].update( f"{path}.{key}" if path else key for key in missing_in_2 ) if missing_in_1: result["missing_in_1"].update( f"{path}.{key}" if path else key for key in missing_in_1 ) # Check nested dictionaries for key in keys1.intersection(keys2): current_path = f"{path}.{key}" if path else key if isinstance(dict1[key], dict) and isinstance(dict2[key], dict): nested_diff = get_dict_differences(dict1[key], dict2[key], current_path) if nested_diff["structure_mismatch"]: result["structure_mismatch"] = True result["differences"][key] = nested_diff["differences"] result["missing_in_2"].update(nested_diff["missing_in_2"]) result["missing_in_1"].update(nested_diff["missing_in_1"]) return result
[docs] def merge_dicts( dict1: Dict[str, Any], dict2: Dict[str, Any], prioritize: str = None ) -> Dict[str, Any]: """ Merge two nested dictionaries. Args: dict1: First dictionary dict2: Second dictionary to merge into dict1 prioritize: If 'dict1', prioritize dict1 values (only overwrite if dict1 value is None) If 'dict2', prioritize dict2 values (only overwrite if dict2 value is None) If None, use standard merge behavior (dict2 overwrites dict1) Returns: Merged dictionary Example: >>> dict1 = {"a": {"b": 1, "c": 2}, "d": 3} >>> dict2 = {"a": {"b": 10, "e": 4}, "f": 5} >>> merge_dicts(dict1, dict2) # Standard merge {"a": {"b": 10, "c": 2, "e": 4}, "d": 3, "f": 5} >>> dict1 = {"a": {"b": 1, "c": None}, "d": None} >>> dict2 = {"a": {"b": 10, "c": 20}, "d": 30, "e": 40} >>> merge_dicts(dict1, dict2, prioritize='dict1') {"a": {"b": 1, "c": 20}, "d": 30} """ if prioritize == "dict1": # Prioritize dict1: only overwrite dict1 values if they are None result = dict1.copy() for key, value in dict2.items(): if key in result: # Key exists in dict1 if isinstance(result[key], dict) and isinstance(value, dict): # Both are dictionaries, merge recursively result[key] = merge_dicts(result[key], value, prioritize="dict1") elif result[key] is None: # Value in dict1 is None, use value from dict2 result[key] = value # If result[key] is not None, keep the original value (dict1 priority) else: # Key doesn't exist in dict1, don't add it (dict1 priority) pass return result elif prioritize == "dict2": # Prioritize dict2: only overwrite dict2 values if they are None result = dict2.copy() for key, value in dict1.items(): if key in result: # Key exists in dict2 if isinstance(result[key], dict) and isinstance(value, dict): # Both are dictionaries, merge recursively result[key] = merge_dicts(value, result[key], prioritize="dict2") elif result[key] is None: # Value in dict2 is None, use value from dict1 result[key] = value # If result[key] is not None, keep the original value (dict2 priority) else: # Key doesn't exist in dict2, don't add it (dict2 priority) pass return result else: # Standard merge behavior: dict2 overwrites dict1 result = dict1.copy() for key, value in dict2.items(): if ( key in result and isinstance(result[key], dict) and isinstance(value, dict) ): result[key] = merge_dicts(result[key], value) else: result[key] = value return result
[docs] def flatten_dict(nested_dict: Dict[str, Any], obj: Any) -> list[tuple[str, Any]]: """ Flatten a nested dictionary into a list of tuples with (key, value) pairs. This function recursively traverses nested dictionaries and creates flattened key-value pairs using only the final key names. Only final values (non-dictionary values) are included in the result. Args: nested_dict: The nested dictionary to flatten obj: The object to which the flattened dictionary belongs Returns: List of tuples containing (final_key, value) pairs Example: >>> nested = {"a": {"b": 1, "c": {"d": 2, "e": 3}}, "f": 4} >>> flatten_dict(nested) [('b', 1), ('d', 2), ('e', 3), ('f', 4)] >>> nested = {"user": {"profile": {"name": "John", "age": 30}}} >>> flatten_dict(nested) [('name', 'John'), ('age', 30)] >>> flatten_dict({}) # Empty dict [] >>> flatten_dict({"a": None, "b": {"c": 0}}) # None and zero values [('a', None), ('c', 0)] """ flattened = [] if not isinstance(nested_dict, dict): return flattened for key, value in nested_dict.items(): # Check if all keys in the value dict are valid attributes of the object cond = isinstance(value, dict) and all([rhasattr(obj, k) for k in value.keys()]) if cond: # Recursively flatten nested dictionaries flattened.extend(flatten_dict(value, obj)) else: # Add the final key-value pair using only the final key flattened.append((key, value)) return flattened