Source code for nnsa.utils.dictionaries

"""
This module contains functions dealing with dictionaries.
"""

from collections.abc import Mapping
import csv


__all__ = [
    'add_nested_dict',
    'flatten_dict',
    'itemize_items',
    'nested_update',
    'unflatten_dict',
    'write_dict_to_csv_as_table',
]


[docs]def add_nested_dict(d, keys, value): """ Put a value in dictionary d with nested keys specified by the specified keys list. Set d['a']['b']['c'] = value, when keys = ['a', 'b', 'c'] Args: d (dict): dictionary to add the value with nested keys to. keys (list): list of nested keys. value: the value to put in the dictionary. """ # Set d['a']['b']['c'] = value, when keys = ['a', 'b', 'c'] for key in keys[:-1]: d = d.setdefault(key, {}) d[keys[-1]] = value
def add_postfix(d, postfix): """ Add postfix to each key in dict d. Args: d (dict): the dict. postfix (str): the postfix. Returns: d_out (dict): (shallow) copy of the dict with the postfix. """ d_out = {"{}{}".format(key, postfix): val for key, val in d.items()} return d_out
[docs]def flatten_dict(d, path='', d_out=None): """ Traverse a nested/multi-level dictionary and create a new one-level dictionary. Item d['a']['b'] is mapped to key 'a/b' in the output dictionary. The reverse operation is achieved by the unflatten_dict() function. Args: d (dict): dictionary to flatten. path (str, optional): prefix for the flattened keys. Only needed for recursive calls. The user needs not to specify this, i.e. specify ''. Defaults to ''. d_out (dict or None, optional): if a dict is specify, this dict if updated with the flattened keys and value pairs. Needed for recursive calls. If None, the output dictionary is a new empty dictionary. Defaults to None. Returns: d_out (dict): one-level dictionary with same values as input d, but with flattened keys. Examples: >>> d = {'A': {'a': 2, 'b': True}, 'B': {'a': 10, 'b': False}} >>> flatten_dict(d) {'A/a': 2, 'A/b': True, 'B/a': 10, 'B/b': False} """ # Create new dict. if d_out is None: d_out = {} # Separator of the key names in the flattened dict. sep = '/' # Loop over items in d for key, value in d.items(): # Add current key to path (path will be the flattened key in the output dict). if path: path_new = '{}{}{}'.format(path, sep, key) else: path_new = '{}'.format(key) # If not the deepest level is reached, recursively call the function on the value, else add key, value to output # dictionary. if isinstance(value, Mapping): if len(value) == 0: d_out.update({path_new: value}) else: d_out = flatten_dict(value, path=path_new, d_out=d_out) else: d_out.update({path_new: value}) return d_out
[docs]def itemize_items(items): """ Return a string that prints a list of items, where each item is a pair of objects. Handy for printing dictionaries, e.g. if d is some dict: print(itemize_items(d.items()) Args: items (iterable): iterable yielding two values. Returns: (str): string in which the items are printed underneath each other with indentation. """ # Specify indentation. indent = ' '*7 # Loop over items. description = [] for k, v in items: item_description = '{}{}: {}'.format(indent, k, v) # If the item description is multiline, add indentation to every line. description.append(item_description.replace('\n', '\n{}'.format(' '*len(indent)))) return '\n'.join(description)
[docs]def nested_update(d, other=None, accept_new_key=True, **kwargs): """ Update a nested dictionary d with update dictionary u, maintaining deeper levels of d that are not in u. Adapted from https://stackoverflow.com/questions/3232943/update-value-of-a-nested-dictionary-of-varying-depth Args: d (dict): (nested) dictionary to update. other (dict or iterable, optional): dictionary or iterable of key, value pairs with which to update. accept_new_key (bool, str, optional): If True, accepts new keys (keys that do not exist in d). If False, raises an error if attempting to update a key that does not exists in d (including deeper levels). If 'parameters_mode', do not accept new keys when updating a nnsa.Parameters object, but accept new keys in ordinary dict objects. Default to True (this is the default behaviour of Python dict.update()). **kwargs (optional): keyword arguments with which to update (in which the keyword is the key). Returns: d (dict): updated dictionary (in place, so return is in fact redundant). """ if isinstance(other, dict): # Convert dict to iterable with key, value pairs. other = other.items() # Check if accept_new_key == 'parameters_mode'. parameters_mode = False if isinstance(accept_new_key, str): if accept_new_key.lower() == 'parameters_mode': from nnsa.parameters.parameters import Parameters parameters_mode = True accept_new_key = type(d).__name__ != 'Parameters' else: raise ValueError('Invalid string argument for accept_new_key="{}".'.format(accept_new_key)) if not isinstance(accept_new_key, bool): raise TypeError('accept_new_key should be a bool. Got a {} instead.'.format(type(accept_new_key))) # For each key, value pair in other, update the key, value in d. if other is not None: for k, v in other: # Raise error if key does not exist in d. if not accept_new_key and k not in d: raise KeyError("'{}' not in collection with keys {}.".format(k, list(d.keys()))) if isinstance(v, Mapping): # Value is a dict/Parameters: recursively call this update function. # If parameters_mode, do not accept new keys if v is a Parameters. accept_new_key_i = 'parameters_mode' if parameters_mode else accept_new_key d_i = d.get(k, dict()) if d.get(k, {}) is not None else dict() d[k] = nested_update(d_i, other=v, accept_new_key=accept_new_key_i) else: # Replace d[k] with v. d[k] = v # Handle **kwargs: kwargs is a dictionary. if kwargs: nested_update(d, other=kwargs, accept_new_key=accept_new_key) return d
def removekey(d, key): # Make a shallow copy of dict d and remove key. r = dict(d) del r[key] return r
[docs]def unflatten_dict(d): """ Restore the original dictionary after flattening the dict with flatten_dict(). Item d['a/b'] is mapped to ['a']['b'] in the output dictionary. The reverse operation is achieved by the flatten_dict() function. Examples: >>> d = {'A': {'a': 2, 'b': True}, 'B': {'a': 10, 'b': False}} >>> d_flat = flatten_dict(d) >>> print(d == unflatten_dict(d_flat)) True Args: d (dict): one-level dictionary with keys representing nested dictionary keys, separated by '/'. Returns: unflat_dict (dict): unflattened, i.e. nested, dictionary with same values as input d. """ # Separator of the key names in the flattened dict. sep = '/' # Loop over key, value pairs and add nested version to the output dict. unflat_dict = {} for key_flat, value in d.items(): add_nested_dict(unflat_dict, keys=key_flat.split(sep), value=value) return unflat_dict
[docs]def write_dict_to_csv_as_table(filepath, table_dict): """ Write a dictionary to a csv, structuring it as a table with the keys of the dict as column headers. Args: filepath (str): filepath to save the csv to. table_dict (dict): dictionary that contains the table data. The values of the dictionary must be a list, and each element of the list will be put on a new row. The number of elements in a list may vary between the columns, i.e. under each column a varying number of elements may be put. """ with open(filepath, 'w', newline='') as csv_file: writer = csv.writer(csv_file) # Write header (standard annotations labels). writer.writerow(list(table_dict.keys())) # Maximum number of rows to write. max_length = max([len(labels_list) for labels_list in table_dict.values()]) for i in range(max_length): # Create row. all_elements = [] for key in table_dict.keys(): # Write the original annotation text, or an empty string if all orignal texts have already been written # to previous rows. element = table_dict[key][i] if len(table_dict[key]) > i else '' all_elements.append(element) # Write row. writer.writerow(all_elements)