from .common import is_str
[docs]
def last_element(d, key_list):
    """Return the last element and key for a document d given a docstring.
    A document d is passed with a list of keys key_list.  A generator is then
    returned for all elements that match all keys.  Not that there may be
    a 1-to-many relationship between keys and elements due to lists in the document.
    :param d: document d to return elements from
    :param key_list: list of keys that specify elements in the document d
    :return: generator for elements that match all keys
    """
    # preconditions
    if not d or not key_list:
        return
    k = key_list.pop(0)
    # termination
    if not key_list:
        yield k, d
    # recursion
    else:
        try:
            t = d[k]
        except KeyError:
            return  # key does not exist
        except TypeError:
            return  # not sub-scriptable
        if isinstance(t, dict):
            yield from last_element(t, key_list)
        elif isinstance(t, list):
            for l in t:
                yield from last_element(l, key_list.copy())
        elif isinstance(t, tuple):
            # unsupported type
            raise ValueError("unsupported type in key {}".format(k)) 
[docs]
def key_value(dictionary, key):
    """Return a generator for all values in a dictionary specific by a dotstirng (key)
       if key is not found from the dictionary, None is returned.
    :param dictionary: a dictionary to return values from
    :param key: key that specifies a value in the dictionary
    :return: generator for values that match the given key
    """
    def safe_ref(k, d):
        if d:
            try:
                return d[k]
            except KeyError:
                pass
    if not is_str(key):
        raise TypeError("key argument must of be of type 'str'")
    key_list = key.split(".")
    for k, le in last_element(dictionary, key_list):
        yield safe_ref(k, le) 
[docs]
def set_key_value(dictionary, key, value):
    """Set values all values in dictionary matching a dotstring key to a specified value.
       if key is not found in dictionary, it just skip quietly.
    :param dictionary: a dictionary to set values in
    :param key: key that specifies an element in the dictionary
    :return: dictionary after changes have been made
    """
    def safe_assign(k, d):
        if d:
            try:
                d[k] = value
            except KeyError:
                pass
    if not is_str(key):
        raise TypeError("key argument must of be of type 'str'")
    key_list = key.split(".")
    for k, le in last_element(dictionary, key_list):
        safe_assign(k, le)
    return dictionary 
[docs]
def remove_key(dictionary, key):
    """Remove field specified by the docstring key
    :param dictionary: a dictionary to remove the value from
    :param key: key that specifies an element in the dictionary
    :return: dictionary after changes have been made
    """
    if not is_str(key):
        raise TypeError("key argument must of be of type 'str'")
    key_list = key.split(".")
    for k, le in last_element(dictionary, key_list):
        try:
            del le[k]
        except KeyError:
            pass
    return dictionary 
[docs]
def list_length(d, field):
    """Return the length of a list specified by field.
    If field represents a list in the document, then return its length.
    Otherwise return 0.
    :param d: a dictionary
    :param field: the dotstring field specifying a list
    """
    default_value = 0
    try:
        lst = next(key_value(d, field))
    except StopIteration:
        return default_value
    if isinstance(lst, list):
        return len(lst)
    else:
        return default_value