Source code for scriptconfig.value

from . import smartcast as smartcast_mod
import re
import ubelt as ub


long_prefix_pat = re.compile('--[^-].*')
short_prefix_pat = re.compile('-[^-].*')


[docs] def normalize_option_str(s): return s.lstrip('-').replace('-', '_')
__note__ = """ TODO: After we remove 3.6 support, deprecate position and add the ispositional argument. Or maybe just "positional"? ispositional (bool): if True the argument will be treated as a positional argument with its order determined by its location in the config. """
[docs] class Value(ub.NiceRepr): """ You may set any item in the config's default to an instance of this class. Using this class allows you to declare the desired default value as well as the type that the value should be (Used when parsing sys.argv). Attributes: value (Any): A float, int, etc... type (type | None): the "type" of the value. This is usually used if the value specified is not the type that `self.value` would usually be set to. parsekw (dict): kwargs for to argparse add_argument position (None | int): if an integer, then we allow this value to be a positional argument in the argparse CLI. Note, that values with the same position index will cause conflicts. Also note: positions indexes should start from 1. isflag (bool): if True, args will be parsed as booleans. Default to False. alias (List[str] | None): other long names (that will be prefixed with '--') that will be accepted by the argparse CLI. short_alias (List[str] | None): other short names (that will be prefixed with '-') that will be accepted by the argparse CLI. group (str | None): Impacts display of underlying argparse object by grouping values with the same type together. There is no other impact. mutex_group (str | None): Indicates that only one of the values in a group should be given on the command line. This has no impact on python usage. tags (Any): for external program use CommandLine: xdoctest -m /home/joncrall/code/scriptconfig/scriptconfig/value.py Value xdoctest -m scriptconfig.value Value Example: >>> self = Value(None, type=float) >>> print('self.value = {!r}'.format(self.value)) self.value = None >>> self.update('3.3') >>> print('self.value = {!r}'.format(self.value)) self.value = 3.3 """ # hack to work around isinstance with IPython %autoreload magic __scfg_class__ = 'Value' def __init__(self, value=None, type=None, help=None, choices=None, position=None, isflag=False, nargs=None, alias=None, required=False, short_alias=None, group=None, mutex_group=None, tags=None): self.value = None self.type = type self.alias = alias self.position = position self.isflag = isflag self.parsekw = { 'help': help, 'type': type, 'choices': choices, 'nargs': nargs, } self.group = group self.mutex_group = mutex_group self.required = required self.short_alias = short_alias self.tags = tags self.update(value) # TODO: opposite # for use with flags, this indicates that there is another variable # that should always be the opposite of this one. # i.e. force / dry # i.e. verbose / quiet def __nice__(self): # return '{!r}: {!r}'.format(self.type, self.value) return f'{self.value!r}'
[docs] def update(self, value): self.value = self.cast(value) return self
[docs] def cast(self, value): if isinstance(value, str): value = smartcast_mod.smartcast(value, self.type) return value
[docs] def copy(self): import copy return copy.copy(self)
[docs] def _to_value_kw(self): """ Used in port-to-dataconf and port-to-argparse """ value = self orig_help = self.parsekw['help'] orig_type = self.parsekw['type'] value_kw = {k: v for k, v in self.__dict__.items() if v} value_kw.pop('parsekw') value_kw.update(value.parsekw) value_kw['help'] = CodeRepr(repr(orig_help)) value_kw['nargs'] = CodeRepr(repr(value.parsekw['nargs'])) if orig_type is not None: if isinstance(orig_type, str): value_kw['type'] = repr(orig_type) else: value_kw['type'] = CodeRepr(orig_type.__name__) value_kw = ub.udict(value_kw) order = value_kw & ['value', 'nargs', 'type', 'isflag', 'position', 'required', 'choices', 'alias', 'short_alias', 'group', 'mutex_group', 'help'] value_kw = order | (value_kw - order) if value_kw.get('nargs', None) in {None, 'None'}: value_kw.pop('nargs', None) HACKS = 1 if HACKS: if value_kw['type'] == 'smartcast': value_kw.pop('type') if orig_help and len(orig_help) > 40: import textwrap wrapped = ub.indent('\n'.join(textwrap.wrap(orig_help, width=60)), ' ' * 4) block = ub.codeblock( """ ub.paragraph( ''' {} ''') """ ).format(wrapped) value_kw['help'] = CodeRepr(ub.indent(block, ' ' * 8).lstrip()) # "ub.paragraph(\n'''\n{}\n''')".format(ub.indent(value.help, ' ' * 16)) value_kw['default'] = value.value value_kw.pop('value', None) return value_kw
[docs] @classmethod def _from_action(cls, action, actionid_to_groupkey, actionid_to_mgroupkey, pos_counter): """ Used in port_argparse Example: import argparse from scriptconfig.value import * # NOQA action = argparse._StoreAction('foo', 'bar', default=3) value = Value._from_action(action, {}, {}, 0) action = argparse._CountAction('foo', 'bar') value = Value._from_action(action, {}, {}, 0) """ import argparse key = action.dest long_option_strings = [ s for s in action.option_strings if long_prefix_pat.match(s) ] short_option_strings = [ s for s in action.option_strings if short_prefix_pat.match(s) ] alias = ub.oset(normalize_option_str(s) for s in long_option_strings) alias = list(alias - {key}) short_alias = ub.oset(normalize_option_str(s) for s in short_option_strings) short_alias = list(short_alias - {key}) real_value_kw = { 'value': action.default, 'type': action.type, 'alias': alias, 'short_alias': short_alias, 'required': action.required, 'choices': action.choices, 'help': action.help, } if action.nargs == 0 and action.const is True: # This is a boolean flag real_value_kw['isflag'] = True elif isinstance(action, argparse._CountAction): real_value_kw['isflag'] = 'counter' else: real_value_kw.pop('isflag', None) if action.nargs is not None: real_value_kw['nargs'] = action.nargs action_id = id(action) if action_id in actionid_to_groupkey: real_value_kw['group'] = repr(actionid_to_groupkey[action_id]) if action_id in actionid_to_mgroupkey: real_value_kw['mutex_group'] = repr(actionid_to_mgroupkey[action_id]) if len(action.option_strings) == 0: real_value_kw['position'] = next(pos_counter) value = Value(**real_value_kw) return value
[docs] class Flag(Value): """ Exactly the same as a Value except isflag default to True """ def __init__(self, value=False, **kwargs): assert 'isflag' not in kwargs kwargs['isflag'] = True super().__init__(value=value, **kwargs)
[docs] class Path(Value): """ Note this is mean to be used only with scriptconfig.Config. It does NOT represent a pathlib object. """ def __init__(self, value=None, help=None, alias=None): super(Path, self).__init__(value, str, help=help, alias=alias)
[docs] def cast(self, value): if isinstance(value, str): value = ub.expandpath(value) return value
[docs] class PathList(Value): """ Can be specified as a list or as a globstr FIXME: will fail if there are any commas in the path name Example: >>> from os.path import join >>> path = ub.modname_to_modpath('scriptconfig', hide_init=True) >>> globstr = join(path, '*.py') >>> # Passing in a globstr is accepted >>> assert len(PathList(globstr).value) > 0 >>> # Smartcast should separate these >>> assert len(PathList('/a,/b').value) == 2 >>> # Passing in a list is accepted >>> assert len(PathList(['/a', '/b']).value) == 2 """
[docs] def cast(self, value=None): if isinstance(value, str): import glob paths1 = sorted(glob.glob(ub.expandpath(value))) paths2 = smartcast_mod.smartcast(value) if paths1: value = paths1 else: value = paths2 return value
[docs] def _value_add_argument_to_parser(value, _value, self, parser, key, fuzzy_hyphens=0): """ POC for a new simplified way for a value to add itself as an argument to a parser. Args: value (Any): the unwrapped default value _value (Value): the value metadata """ # import argparse from scriptconfig import argparse_ext # value: Any | Value name = key argkw = {} argkw['help'] = '' positional = None isflag = False required = False group_lut = getattr(parser, '_sc_group_lut', {}) mutex_group_lut = getattr(parser, '_sc_mutex_group_lut', {}) parser._sc_mutex_group_lut = mutex_group_lut parser._sc_group_lut = group_lut parent = parser if _value is not None: # Use the metadata in the Value class to enhance argparse # _value = _metadata[name] argkw.update(_value.parsekw) required = _value.required value = _value.value isflag = _value.isflag positional = _value.position # If the args are flagged as belonging to a group, resepct that. if _value.group is not None: if _value.group not in group_lut: groupkw = {} if isinstance(_value.group, str): groupkw['title'] = _value.group group_lut[_value.group] = parent.add_argument_group(**groupkw) parent = group_lut[_value.group] if _value.mutex_group is not None: if _value.mutex_group not in mutex_group_lut: mutex_group_lut[_value.mutex_group] = parent.add_mutually_exclusive_group() parent = mutex_group_lut[_value.mutex_group] if not argkw['help']: # argkw['help'] = '<undocumented>' argkw['help'] = '' argkw['default'] = value argkw['action'] = _maker_smart_parse_action(self) if positional: parent.add_argument(name, **argkw) argkw['dest'] = name option_strings = _resolve_alias(name, _value, fuzzy_hyphens) if isflag: # Can we support both flag and setitem methods of cli # parsing? argkw.pop('type', None) argkw.pop('choices', None) argkw.pop('action', None) argkw.pop('nargs', None) argkw['dest'] = name if isflag == 'counter': argkw['action'] = argparse_ext.CounterOrKeyValAction else: argkw['action'] = argparse_ext.BooleanFlagOrKeyValAction try: parent.add_argument(*option_strings, required=required, **argkw) except Exception: print('ERROR: Failed to add argument') print('argkw = {}'.format(ub.urepr(argkw, nl=1))) print('required = {}'.format(ub.urepr(required, nl=1))) print('option_strings = {}'.format(ub.urepr(option_strings, nl=1))) raise
[docs] def _value_add_argument_kw(value, _value, self, key, fuzzy_hyphens=0): """ TODO: resolve with :func:`_value_add_argument_to_parser`. This just creates one or more kwargs for add_argument. (Depending on how many variants of the argument we want). Args: value (Any): the unwrapped default value _value (Value): the value metadata Returns: Dict[str, Tuple[str, Tuple, Dict]]: special keys to the method name, args, kwargs invocations. """ # import argparse from scriptconfig import argparse_ext # value: Any | Value name = key argkw = {} argkw['help'] = '' positional = None isflag = False required = False # group_lut = getattr(parser, '_sc_group_lut', {}) # mutex_group_lut = getattr(parser, '_sc_mutex_group_lut', {}) # parser._sc_mutex_group_lut = mutex_group_lut # parser._sc_group_lut = group_lut invocations = {} # parent = parser if _value is not None: # Use the metadata in the Value class to enhance argparse # _value = _metadata[name] argkw.update(_value.parsekw) required = _value.required value = _value.value isflag = _value.isflag positional = _value.position # TODO: handle groups # If the args are flagged as belonging to a group, resepct that. # if _value.group is not None: # if _value.group not in group_lut: # groupkw = {} # if isinstance(_value.group, str): # groupkw['title'] = _value.group # group_lut[_value.group] = parent.add_argument_group(**groupkw) # parent = group_lut[_value.group] # if _value.mutex_group is not None: # if _value.mutex_group not in mutex_group_lut: # mutex_group_lut[_value.mutex_group] = parent.add_mutually_exclusive_group() # parent = mutex_group_lut[_value.mutex_group] if not argkw['help']: # argkw['help'] = '<undocumented>' argkw['help'] = '' argkw['default'] = value argkw['action'] = _maker_smart_parse_action(self) if positional: invocations['positional'] = ( 'add_argument', (name,), argkw.copy(), ) argkw['dest'] = name option_strings = _resolve_alias(name, _value, fuzzy_hyphens) if isflag: # Can we support both flag and setitem methods of cli # parsing? argkw.pop('type', None) argkw.pop('choices', None) argkw.pop('action', None) argkw.pop('nargs', None) argkw['dest'] = name argkw['action'] = argparse_ext.BooleanFlagOrKeyValAction argkw['required'] = required # parent.add_argument(*option_strings, required=required, **argkw) invocations['key_value'] = ( 'add_argument', option_strings, argkw, ) return invocations
[docs] def _resolve_alias(name, _value, fuzzy_hyphens): if _value is None: aliases = None short_aliases = None else: aliases = _value.alias short_aliases = _value.short_alias if isinstance(aliases, str): aliases = [aliases] if isinstance(short_aliases, str): short_aliases = [short_aliases] long_names = [name] + list((aliases or [])) short_names = list(short_aliases or []) if fuzzy_hyphens: # Do we want to allow for people to use hyphens on the CLI? # Maybe, we can make it optional. unique_long_names = set(long_names) modified_long_names = {n.replace('_', '-') for n in unique_long_names} extra_long_names = modified_long_names - unique_long_names long_names += sorted(extra_long_names) short_option_strings = ['-' + n for n in short_names] long_option_strings = ['--' + n for n in long_names] option_strings = short_option_strings + long_option_strings return option_strings
[docs] def scfg_isinstance(item, cls): """ use instead isinstance for scfg types when reloading Args: item (object): instance to check cls (type): class to check against Returns: bool """ # Note: it is safe to simply use isinstance(item, cls) when # not reloading if hasattr(item, '__scfg_class__') and hasattr(cls, '__scfg_class__'): return item.__scfg_class__ == cls.__scfg_class__ else: return isinstance(item, cls)
[docs] def _maker_smart_parse_action(self): import argparse from itertools import chain scfg_object = self ### TODO: be slightly less smart class ParseAction(argparse._StoreAction): def __init__(self, *args, **kwargs): # required/= kwargs.pop('required', False) super().__init__(*args, **kwargs) # with script config nothing should be required by default # (unless specified) all positional arguments should have # keyword arg variants Setting required=False here will prevent # positional args from erroring if they are not specified. I # dont think there are other side effects, but we should make # sure that is actually the case. self.required = False # hack if self.type is None: # If a type isn't explicitly declared, we will either use # the template (if it exists) or try using a smartcast. def _smart_type(value): key = self.dest template = scfg_object.default[key] if not isinstance(template, Value): # smartcast non-valued params from commandline value = smartcast_mod.smartcast(value) else: value = template.cast(value) return value self.type = _smart_type def __call__(action, parser, namespace, values, option_string=None): # print('CALL action = {!r}'.format(action)) # print('option_string = {!r}'.format(option_string)) # print('values = {!r}'.format(values)) if isinstance(values, list) and len(values): # We got a list of lists, which we hack into a flat list if isinstance(values[0], list): values = list(chain(*values)) setattr(namespace, action.dest, values) if not hasattr(parser, '_explicitly_given'): # We might be given a subparser / parent parser # and not the original one we created. parser._explicitly_given = set() parser._explicitly_given.add(action.dest) return ParseAction
[docs] class CodeRepr(str): # When we want to write out the exact code that should be inserted. def __repr__(self): return self