Source code for utils.dict_tools

"""

"""
import copy
import six
from utils import logging_tools
LOG = logging_tools.get_logger(__name__)


_TC = {  # Tree Characters
    '|': u'\u2502',  # Horizontal
    '-': u'\u2500',  # Vertical
    'L': u'\u2514',  # L-Shape
    'S': u'\u251C',  # Split
}


# Additional Dictionary Classes and Functions ##################################


[docs]class DotDict(dict): """ Make dict fields accessible by . """ def __init__(self, *args, **kwargs): super(DotDict, self).__init__(*args, **kwargs) for key in self: if isinstance(self[key], dict): self[key] = DotDict(self[key]) # __getattr__ = dict.__getitem__ __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ def __getattr__(self, key): """ Needed to raise the correct exceptions """ try: return super(DotDict, self).__getitem__(key) except KeyError as e: raise AttributeError(e) # TODO: Adapt traceback to not link here (Python3 does that?)
[docs] def get_subdict(self, keys, strict=True): """ See get_subdict in dict_tools. """ return DotDict(get_subdict(self, keys, strict))
[docs]def get_subdict(full_dict, keys, strict=True): """ Returns a sub-dictionary of ``full_dict`` containing only keys of ``keys``. Args: full_dict: Dictionary to extract from keys: keys to extract strict: If false it ignores keys not in full_dict. Otherwise it crashes on those. Default: True Returns: Extracted sub-dictionary """ if strict: return {k: full_dict[k] for k in keys} return {k: full_dict[k] for k in keys if k in full_dict}
# Dict Parser ##################################################################
[docs]class ParameterError(Exception): pass
[docs]class ArgumentError(Exception): pass
[docs]class Parameter(object): """ Helper Class for DictParser """ def __init__(self, name, **kwargs): self.name = name self.required = kwargs.pop('required', False) self.default = kwargs.pop('default', None) self.help = kwargs.pop('help', '') self.type = kwargs.pop('type', None) self.nargs = kwargs.pop('nargs', None) self.subtype = kwargs.pop('subtype', None) self.choices = kwargs.pop('choices', None) if len(kwargs) > 0: ParameterError("'{:s}' are not valid parameters for Argument.".format(kwargs.keys())) self._validate() def _validate(self): if not isinstance(self.name, six.string_types): raise ParameterError("Parameter '{:s}': ".format(str(self.name)) + "Name is not a valid string.") if self.default and self.type and not isinstance(self.default, self.type): raise ParameterError("Parameter '{:s}': ".format(self.name) + "Default value not of specified type.") if self.choices: try: if self.default and self.default not in self.choices: raise ParameterError("Parameter '{:s}': ".format(self.name) + "Default value not found in choices.") if self.type or self.subtype: check = self.type if self.subtype is None else self.subtype for choice in self.choices: if not isinstance(choice, check): raise ParameterError("Choice '{}' ".format(choice) + "of parameter '{:s}': ".format(self.name) + "is not of type '{:s}'.".format(check.__name__)) except TypeError: raise ParameterError("Parameter '{:s}': ".format(self.name) + "Choices seem to be not iterable.") if self.nargs: if not isinstance(self.nargs, int): raise ParameterError("Parameter '{:s}': ".format(self.name) + "nargs needs to be an integer.") if not (self.type or self.type == list): raise ParameterError("Parameter '{:s}': ".format(self.name) + "'type' needs to be 'list' if 'nargs' is given.") if self.subtype and not (self.type or self.type == list): raise ParameterError("Parameter '{:s}': ".format(self.name) + "field 'subtype' is only accepted if 'type' is list.") if self.required and self.default is not None: LOG.warn("Parameter '{:s}': ".format(self.name) + "Value is required but default value is given. The latter will be ignored.")
[docs]class DictParser(object): """ Provides functions to parse a dictionary. First build a dictionary structure with Arguments as leafs via add_argument or on init. A similar structured option dictionary with the values as leafs can then be parsed. """ def __init__(self, dictionary=None, strict=False): """ Initialize Class either empty or with preconfigured dictionary Args: dictionary: Preconfigured Dictionary for parsing strict: Strict Parsers don't accept unknown options. If False, it just logs the names. """ self.strict = strict if dictionary: self._validate_parameters(dictionary) self.dictionary = dictionary else: self.dictionary = {} ######################### # Static Methods (private) ######################### @staticmethod def _validate_parameters(dictionary): """ Validates an input dictionary that can be used as parameters. Args: dictionary: Dictionary to validate """ for key in dictionary: param = dictionary[key] if isinstance(param, dict): try: DictParser._validate_parameters(param) except ParameterError as e: e.message = "'{:s}.{:s}".format(key, e.message[1:]) e.args = (e.message,) raise elif not isinstance(param, Parameter): raise ParameterError("'{:s}' is not a valid entry.".format(key)) else: if key != param.name: raise ParameterError("'{:s}': Key and name need to be the same.".format(key)) @staticmethod def _check_value(key, arg_dict, param_dict): """ Checks if in arg_dict[key] satisfies param_dict[key] Args: key: key to check arg_dict: Arguments-structure. Can be None or empty. param_dict: Parameter-structure. Needs to contain 'key' Returns: The appropriate value for arg_dict[key] """ param = param_dict[key] if not arg_dict or key not in arg_dict: if param.required: raise ArgumentError("'{:s}' required in options.\nHelp: {:s}".format( key, param.help) ) else: return param.default opt = arg_dict[key] if opt is None: if param.required: raise ArgumentError("'{:s}' required in options.\nHelp: {:s}".format( key, param.help) ) else: if param.type and not isinstance(opt, param.type): raise ArgumentError("'{:s}' is not of type {:s}.\nHelp: {:s}".format( key, param.type.__name__, param.help) ) if param.type == list: if param.nargs and not param.nargs == len(opt): raise ArgumentError( "'{:s}' should be list of length {:d},".format(key, param.nargs) + " instead it was of length {:d}.\nHelp: {:s}".format(len(opt), param.help)) if param.subtype: for idx, item in enumerate(opt): if not isinstance(item, param.subtype): raise ArgumentError( "Item {:d} of '{:s}' is not of type '{:s}' ".format( idx, key, param.subtype.__name__) + ".\nHelp: {:s}".format(param.help)) if param.choices and any([o for o in opt if o not in param.choices]): raise ArgumentError( "All elements of '{:s}' need to be one of {:s},".format(key, param.choices) + " instead the list was {:s}.\nHelp: {:s}".format(str(opt), param.help) ) elif param.choices and opt not in param.choices: raise ArgumentError( "'{:s}' needs to be one of {:s}, instead it was {:s}.\nHelp: {:s}".format( key, param.choices, str(opt), param.help) ) return opt def _parse_options(self, arg_dict, param_dict): """ Use parse_options()! This is a helper Function for parsing options. It does all the work. Called recursively. Args: arg_dict: Dictionary with the input parameter param_dict: Dictionary with the parameters to check the parameter against Returns: Dictionary with parsed options """ checked_dict = DotDict() for key in param_dict: if isinstance(param_dict[key], Parameter): checked_dict[key] = DictParser._check_value(key, arg_dict, param_dict) elif isinstance(param_dict[key], dict): try: if not arg_dict or not (key in arg_dict): checked_dict[key] = DictParser._parse_options(None, param_dict[key]) else: checked_dict[key] = DictParser._parse_options(arg_dict[key], param_dict[key]) except ArgumentError as e: old_msg = e.message[1:] if old_msg.startswith("'"): e.message = "'{:s}.{:s}".format(key, e.message[1:]) else: e.message = "'{:s}' has {:s}".format(key, e.message) e.args = (e.message,) raise arg_dict.pop(key, None) # Default value avoids KeyError if len(arg_dict) > 0: error_message = "Unknown Options: '{:s}'.".format(str(list(arg_dict.keys()))) if self.strict: raise ArgumentError(error_message) LOG.debug(error_message) if self.strict: return checked_dict else: return checked_dict, arg_dict ######################### # Public Methods #########################
[docs] def parse_arguments(self, arguments): """ Parse a given option dictionary and return parsed options. Args: arguments: Arguments to parse Return: Parsed options """ return self._parse_options(copy.deepcopy(arguments), self.dictionary)
[docs] def parse_config_items(self, items): """ Parse a list of (name, value) items, where the values are all strings. Args: items: list of (name, value) items. Returns: Parsed options """ options = self._convert_config_items(items) return self._parse_options(options, self.dictionary)
[docs] def add_parameter(self, param, **kwargs): """ Adds an parameter to the parser. If you want it to be an parameter of a sub-dictionary add the 'loc=subdict.subdict' keyword to the input. Args: param: Argument to add (either of object of class argument or string defining the name) kwargs: Any of the argument-fields (apart from 'name') and/or 'loc' Returns: This object """ loc = kwargs.pop('loc', None) if not isinstance(param, Parameter): param = Parameter(param, **kwargs) self._add_param_to_dict(param, loc) return self
[docs] def add_argument_dict(self, dictionary, loc): """ Appends a complete subdictionary to existing argument structure at node 'loc'. Args: loc: location of the node to append the sub-dictionary dictionary: The dictionary to append Returns: This object """ fields = loc.split('.') name = fields[-1] sub_dict = self._traverse_dict('.'.join(fields[:-1])) if name in sub_dict: raise ParameterError("'{:s}' already exists in parser!".format(name)) self._validate_parameters(dictionary) sub_dict[name] = dictionary return self
def help(self): # TODO: Print Help-Message pass
[docs] def tree(self): """ Prints the current Parameter-Tree (I made dis :) ) """ def print_tree(tree, level_char): for i, key in enumerate(sorted(tree.keys())): if i == len(tree) - 1: node_char = _TC['L'] + _TC['-'] level_char_pp = level_char + ' ' else: node_char = _TC['S'] + _TC['-'] level_char_pp = level_char + _TC['|'] + ' ' LOG.info(u"{:s}{:s} {:s}".format(level_char, node_char, key)) if isinstance(tree[key], dict): print_tree(tree[key], level_char_pp) else: leaf = tree[key] LOG.info(u"{:s}{:s} {:s}: {:s}".format( level_char_pp, _TC['S'] + _TC['-'], 'Required', str(leaf.required))) LOG.info(u"{:s}{:s} {:s}: {:s}".format( level_char_pp, _TC['S'] + _TC['-'], 'Default', str(leaf.default))) LOG.info(u"{:s}{:s} {:s}: {:s}".format( level_char_pp, _TC['S'] + _TC['-'], 'Type', leaf.type.__name__ if leaf.type else 'None')) LOG.info(u"{:s}{:s} {:s}: {:s}".format( level_char_pp, _TC['S'] + _TC['-'], 'Choices', str(leaf.choices))) LOG.info(u"{:s}{:s} {:s}: {:s}".format( level_char_pp, _TC['L'] + _TC['-'], 'Help', leaf.help)) LOG.info('Parameter Dictionary') print_tree(self.dictionary, '')
######################### # Private Methods ######################### def _add_param_to_dict(self, param, loc=None): """ Adds and parameter to the parameter dictionary. These will be used to parse an incoming option structure. Args: param: Argument to add loc: Path to sub-dictionary as string (e.g. subdict.subdict.loc[.arg]) Returns: This object """ sub_dict = self._traverse_dict(loc) if param.name in sub_dict: raise ParameterError("'{:s}' already exists in parser!".format(param.name)) sub_dict[param.name] = param return self def _traverse_dict(self, loc=None): """ Traverses the dictionary to the subdict defined by loc. Adds non-existing substructures automatically. Args: loc: Path to sub-dictionary as string (e.g. argument.subparam.locination) Returns: Sub-dictionary """ d = self.dictionary if loc: traverse = loc.split('.') for i, t in enumerate(traverse): try: d = d[t] except KeyError: d[t] = {} d = d[t] if isinstance(d, Parameter): raise ParameterError( "'{:s}' is already an argument and hence cannot be a subdict.".format( '.'.join(traverse[:i] + [t]))) return d def _convert_config_items(self, items): """ Converts items list to a dictionary with types already in place """ def list_check(value): if not value.replace(" ", "").startswith("range("): value = "[" + value + "]" return value def evaluate(name, item): try: return eval(item) # sorry for using that except (NameError, SyntaxError): raise ArgumentError( "Could not evaluate argument '{:s}', unknown '{:s}'".format(name, item)) def eval_type(my_type, item): if issubclass(my_type, six.string_types): return my_type(item.strip("\'\"")) if issubclass(my_type, bool): return bool(eval(item)) else: return my_type(item) out = {} for name, value in items: if name in self.dictionary: arg = self.dictionary[name] if arg.type == list: value = list_check(value) value = evaluate(name, value) if arg.subtype: for idx, entry in enumerate(value): value[idx] = eval_type(arg.subtype, entry) elif arg.type: value = eval_type(arg.type, value) else: value = evaluate(name, value) out[name] = value else: # could check self.strict here, but result is passed to get checked anyway out[name] = evaluate(name, value) return out
# Script Mode ################################################################## if __name__ == '__main__': raise EnvironmentError("{:s} is not supposed to run as main.".format(__file__))