def verify(self, val): ''' Verify against choices, if available Args: val: The value to be verified Raises: FailedVerificationError: If the checks failed ''' if self.type == list and not isinstance(val, list): message = log.error('\'{}\' is not a list'.format(val)) raise FailedVerificationError(message) if hasattr(self, 'choices'): if self.type == list: if hasattr(self, 'length') and len(val) != self.length: message = log.error('Invalid length of list \'{}\', \ should be {}'.format(val, self.length)) raise FailedVerificationError(message) for v in val: if v not in self.choices: message = log.error('Invalid selection \'{}\'. \ Possible choices are \'{}\''.format( val, self.choices)) raise FailedVerificationError(message) elif val not in self.choices: message = log.error('Invalid selection \'{}\'. \ Possible choices are \'{}\''.format(val, self.choices)) raise FailedVerificationError(message)
def __init__(self, **kwargs): ''' Setup the valid options through keywords. The ValidOption objects are configured through the use of keywords. A required keyword is 'type', which specifies the option type and takes care of casting. Additional keywords may be: 'choices': takes a list with valid options 'help': specifies a help string, 'subtype': gives the type of options in within a type=list object 'length': specifies the number of entries within a type=list object 'alias': specifies aliases for the option Example: An option 'age' that shall use integer values, have a default of 18 and be addressable under the alisa 'a' on the cmd line: {'age': ValidOption(type=int, default=18, help='The persons age', alias=['--a'])} A option with a list of 2 specific strings: {'code_type': ValidOption( type=list, subtype=str, default=['python', 'messy'], length=2, choices=['python', 'c', 'c++', 'messy', 'clean'], help='Specifiy what best describes the code')} Args: kwargs: The keywords for the option configuration. Raises: RuntimeError: On missing or invalid options. ''' # defaults self.default = None self.help = 'Not specified' self.alias = [] # process kwargs for key in self._required_attrs: if key not in kwargs.keys(): message = log.error('Missing required option {} to \ ValidOptions'.format(key)) raise KeyError(message) for key, arg in kwargs.items(): if key not in self._valid_attrs: message = log.error('Specified unknown option {} to \ ValidOptions'.format(key)) raise KeyError(message) setattr(self, key, arg) # special cases if self.type == list and not hasattr(self, 'subtype'): message = log.error('ValidOptions with type list \ require a subtype') raise RuntimeError(message) # verify the default type if self.default is not None: self.verify(self.default)
def cast(self, val, ltype=None): ''' Cast to type, to subtype if type is a list. Args: val: The value to be cast. ltype: Manually specified type to cast to. Returns: The value, cast to the proper type. Raises: FailedCastError: If the value could not be cast. ''' if ltype is None: ltype = self.type # special cases if ltype == bool: return self._cast_bool(val) elif ltype == list: return self._cast_list(val) # regular case try: return ltype(val) except ValueError: message = log.error('Unable to cast \'{}\' to \ type \'{}\''.format(val, ltype)) raise FailedCastError(message)
def str2bool(v): if v.lower() in ('yes', 'true', 't', 'y', '1'): return True elif v.lower() in ('no', 'false', 'f', 'n', '0'): return False message = log.error('Boolean value expected.') raise argparse.ArgumentTypeError(message)
def _cast_bool(val): ''' Handle the bool separately, as too much can be true or false ''' if (val is True or (isinstance(val, str) and val.lower() == 'true') or (isinstance(val, int) and val != 0)): return True elif (val is False or (isinstance(val, str) and val.lower() == 'false') or (isinstance(val, int) and val == 0)): return False message = log.error('Unable to cast \ \'{}\' to type \'bool\''.format(val)) raise FailedCastError(message)
def process(self, argv=sys.argv): ''' Process the defaults, the config file and the cmd args into the final configuration. Args: argv (sliceable): a list of arguments to be parsed Return: dict: a hierarchy of dicts with the assembled configuration ''' default_opts = self.opt_tree.get_defaults() flat_opt = self.opt_tree.get_flat_validoptions() parser_handler = ParserHandler(flat_opt) common = parser_handler.get_common_group() common.add_argument( '-i', '--input', help='Provide the configuration through a file') common.add_argument( '--print-defaults', action=PrintOptionsAction, options_callback=self.get_defaults, help='Print the defaults in JSON format') common.add_argument( '--print-current-config', action=PrintOptionsAction, options_callback=self.get_current_config, help='Print the current configuration in JSON format') common.add_argument( '--version', action='version', version='%(prog)s TBD') flat_args = parser_handler.parse_args(argv) full_args = self.opt_tree.inflate_flat_options(flat_args) full_args = self._handle_cmd_specification(full_args) input_opts = {} if 'input' in flat_args: input_opts = self.read_input(full_args['input']) full_args.pop('input') # check if input file and cmd line args are conflicting for current_key, current_val in input_opts.items(): if current_val is None and current_key in full_args: msg = log.error('RED', 'The command line argument {} ' 'conflicts with the deactivation in ' 'the input file.'.format(current_key)) raise ValueError(msg) self._recursive_update(default_opts, input_opts) self._recursive_update(default_opts, full_args) return default_opts
def _cast_list(self, val): ''' Handle the casting of lists recursively ''' if isinstance(val, list): return list(map(lambda x: self.cast(x, self.subtype), val)) elif isinstance(val, str): mod = val.strip() if len(mod) >= 2 and mod[0] == '[' and mod[-1] == ']': mod = mod[1:-1].split(',') return list(map(lambda x: self.cast(x.strip(), self.subtype), mod)) message = log.error('Unable to cast \'{}\' \ to \type \'list({})\''.format( val, self.subtype)) raise FailedCastError(message)
def _recursive_update(cls, old, new): ''' Recursively update the old configuration hierarchy w/ a new one. Args: old (dict): a hierarchy of dicts to be updated new (dict): a hierarchy of dicts with which the prev config is updated ''' for k, v in new.items(): if k not in old: msg = log.error('RED', 'Tried to set the unknown option: {}\n' 'Valid choices are {}'.format(k, old)) raise KeyError(msg) if isinstance(new[k], dict) and isinstance(old[k], dict): cls._recursive_update(old[k], new[k]) else: old[k] = v
def _recurse_cast_verify(cls, validator, config, trace): ''' Recurse the tree and validate/cast all leaves. Args: validator (dict): tree of dicts to verify against config (dict): tree of dicts to be verified/cast trace (list): the current stack of options for meaningfull error Returns: dict: tree with cast leaves ''' if isinstance(validator, ValidOption): converted = validator.cast(config) validator.verify(converted) return converted ret = {} for k, v in config.items(): try: ret[k] = cls._recurse_cast_verify(validator[k], v, [k]+trace) except KeyError as error: message = log.error('Invalid key\'{}\' in option\'{}\''.format( k, ':'.join(reversed(trace)))) raise InvalidKeyError(message) from error return ret