Source code for TidalPy.utilities.dictionary_utils

import copy
from typing import Any, List, Tuple, Union

PossibleKeyType = Union[str, int, float]


[docs] def nested_get( input_dict: dict, nested_keys: Union[PossibleKeyType, List[PossibleKeyType], Tuple[PossibleKeyType, ...]], default: Any = None, raiseon_nolocate: bool = False ) -> Any: """ Returns a value from a series of nested dictionaries given a list of keys Parameters ---------- input_dict : dict Dictionary containing nested dictionary(ies) nested_keys : Union[PossibleKeyType, List[PossibleKeyType, ...], Tuple[PossibleKeyType, ...]] List of keys to search the dictionary with. default : Any = None Default value if key can't be found raiseon_nolocate : bool = False If key can't be found and this is True then a KeyError exception will be raised. Returns ------- value : Any Value stored in the nested dictionaries """ # TODO: When numba .44 comes out test this with @njit to speed it up. That version of numba should support # dict types # --Update: The new version of numba is out, but you need to specifiy the types of the keys and items. # So I don't think njiting is going to work except for very specfic cases. if type(nested_keys) not in [str, int, float, list, tuple]: raise TypeError internal_dict_value = input_dict if type(nested_keys) not in [tuple, list]: nested_keys = [nested_keys] for key in nested_keys: if type(key) not in [str, int, float]: # Items in nested_key can not be lists or Tuples raise TypeError if type(internal_dict_value) is not dict: raise KeyError('There are more nested keys than there are nested dicts.') if key not in internal_dict_value: if raiseon_nolocate: raise KeyError(f'Key:"{key}" not located.') else: return default else: internal_dict_value = internal_dict_value[key] return internal_dict_value
[docs] def nested_place( replacement_value: Any, dict_to_overwrite: dict, nested_keys: Union[PossibleKeyType, List[PossibleKeyType], Tuple[PossibleKeyType, ...]], make_copy: bool = False, retain_old_value: bool = False ) -> dict: """ Replaces a nested-dictionary value based on a list of keys. Parameters ---------- replacement_value : Any New value that will replace any old values. dict_to_overwrite : dict Dictionary whose value will be replaced. nested_keys : Union[PossibleKeyType, List[PossibleKeyType, ...], Tuple[PossibleKeyType, ...]] List-like container of keys required to get to the location of the replacement value. The last key's value will be replaced by replacement_value. make_copy : bool = False If true then a new dictionary will be made (copy of dict_to_overwrite). In-place (mutation) replacement will not take place. retain_old_value : bool = False If true then any old value found will be saved with the key "<name of last key in list>_OLD" Returns ------- dict_to_overwrite : dict Dictionary after the replacement has been made. Note: if make_copy is True then this will not be the same dictionary reference that was originally passed in. """ if type(nested_keys) not in [str, int, float, list, tuple]: raise TypeError if make_copy: # Make a copy of the input dict so its original values remain unchanged. The copy will be returned to the user. dict_to_overwrite = copy.deepcopy(dict_to_overwrite) def _retain(_key: PossibleKeyType, _old_value: Any, _dict_ref: dict): """ Helper function to make a copy of previous value """ if retain_old_value: i = 0 while True: if i > 500: raise StopIteration('A large number of retained key values found during nested placement.') if i == 0: retain_key_attempt = f'{_key}_OLD' else: retain_key_attempt = f'{_key}_OLD{i}' if retain_key_attempt not in _dict_ref: _dict_ref[retain_key_attempt] = _old_value break i += 1 else: # Do Nothing pass if type(nested_keys) not in [tuple, list]: # No nesting: simply override the value stored at the key if nested_keys not in dict_to_overwrite: # If the key is not already present in the dictionary, create a new entry. dict_to_overwrite[nested_keys] = dict() _retain(nested_keys, dict_to_overwrite[nested_keys], dict_to_overwrite) dict_to_overwrite[nested_keys] = replacement_value else: # Work forwards to find the location of the last nested dictionary, then replace its value at the last key. last_i = len(nested_keys) - 1 dict_ref = dict_to_overwrite for k_i, key in enumerate(nested_keys): if key not in dict_ref: # Key not found, make a new dictionary to nest into dict_ref[key] = dict() if k_i == last_i: # Make the replacement _retain(key, dict_ref[key], dict_ref) dict_ref[key] = replacement_value else: # Update reference dict_ref = dict_ref[key] return dict_to_overwrite
[docs] def nested_merge(old_dict: dict, new_dict: dict, make_copies: bool = True) -> dict: """ Replaces values in an old dict with values in a new dict, but does not overwrite nested dicts. Instead it will perform the same type of replacement on each nested dict. Parameters ---------- old_dict : dict Old dictionary that will have some values replaced. new_dict : dict New dictionary whose values will override any values in old_dict. make_copies : bool = True If `True`, then copies will be made of all dictionaries so that later mutations don't propagate to the combined dict. Returns ------- combo_dict : dict Dictionary made from combining old and new dicts. """ # Make copy of the old dictionary if make_copies: combo_dict = copy.deepcopy(old_dict) else: combo_dict = old_dict # Go through each key of the new dict and make replacements for key, value in new_dict.items(): if key not in old_dict: # Simple assignment if it is not present combo_dict[key] = value else: # Replacement required. Check if the value is itself a dict which would require further parsing. if type(value) is dict: old_value_dict = old_dict[key] if type(old_value_dict) is not dict: raise Exception('How did that happen? Old value was not a dict.') combo_dict[key] = nested_merge(old_dict=old_value_dict, new_dict=value, make_copies=True) else: # Other kind of value, just replace old value combo_dict[key] = value return combo_dict