Source code for clize.parameters

# clize -- A command-line argument parser for Python
# Copyright (C) 2011-2021 by Yann Kaiser and contributors. See AUTHORS and
# COPYING for details.

import inspect
from functools import update_wrapper

import six
from sigtools import modifiers, specifiers, signatures

from clize import parser, errors, util


class _ShowList(BaseException):
    pass


class MappedParameter(parser.ParameterWithValue):
    def __init__(self, list_name, values, case_sensitive, **kwargs):
        super(MappedParameter, self).__init__(**kwargs)
        self.list_name = list_name
        self.case_sensitive = case_sensitive
        self.values = values

    def _uncase_values(self, values):
        used = set()
        for target, names, _ in values:
            for name in names:
                name_ = name.lower()
                if name_ in used:
                    raise ValueError(
                        "Duplicate allowed values for parameter {}: {}"
                        .format(self, name_))
                used.add(name_)
                yield name_, target

    def _ensure_no_duplicate_names(self, values):
        used = set()
        for value in values:
            _, names, _ = value
            for name in names:
                if name in used:
                    raise ValueError(
                        "Duplicate allowed values for parameter {}: {}"
                        .format(self, name))
                used.add(name)
            yield value

    @util.property_once
    def values_table(self):
        if not self.case_sensitive:
            try:
                new_values = dict(self._uncase_values(self.values))
            except ValueError:
                if self.case_sensitive is not None:
                    raise
                self.case_sensitive = True
            else:
                self.case_sensitive = False
                return new_values
        return dict(
            (name, target)
            for target, names, _ in self._ensure_no_duplicate_names(self.values)
            for name in names)

    def coerce_value(self, value, ba):
        table = self.values_table
        key = value if self.case_sensitive else value.lower()
        if key == self.list_name:
            raise _ShowList
        try:
            return table[key]
        except KeyError:
            raise errors.BadArgumentFormat(value)

    def read_argument(self, ba, i):
        try:
            super(MappedParameter, self).read_argument(ba, i)
        except _ShowList:
            ba.args[:] = [ba.name]
            ba.kwargs.clear()
            ba.func = self.show_list
            ba.sticky = parser.IgnoreAllArguments()
            ba.posarg_only = True

    def show_list(self, name):
        f = util.Formatter()
        f.append('{name}: Possible values for {self.display_name}:'
                 .format(self=self, name=name))
        f.new_paragraph()
        with f.indent():
            with f.columns() as cols:
                for _, names, desc in self.values:
                    cols.append(', '.join(names), desc)
        f.new_paragraph()
        return str(f)

    def help_parens(self):
        backup = self.default
        try:
            for arg, keys, _ in self.values:
                if arg == self.default:
                    self.default = keys[0]
                    break
            else:
                self.default = util.UNSET
            for s in super(MappedParameter, self).help_parens():
                yield s
        finally:
            self.default = backup
        if self.list_name:
            yield 'use "{0}" for options'.format(self.list_name)


[docs]@modifiers.autokwoargs def mapped(values, list_name='list', case_sensitive=None): """Creates an annotation for parameters that maps input values to Python objects. :param sequence values: A sequence of ``pyobj, names, description`` tuples. For each item, the user can specify a name from ``names`` and the parameter will receive the corresponding ``pyobj`` value. ``description`` is used when listing the possible values. :param str list_name: The value the user can use to show a list of possible values and their description. :param bool case_sensitive: Force case-sensitiveness for the input values. The default is to guess based on the contents of values. .. literalinclude:: /../examples/mapped.py :lines: 4-15 """ return parser.use_mixin(MappedParameter, kwargs={ 'case_sensitive': case_sensitive, 'list_name': list_name, 'values': values, })
def _conv_oneof(values): for value in values: if isinstance(value, six.string_types): yield value, [value], '' else: yield value[0], [value[0]], value[1]
[docs]@modifiers.autokwoargs def one_of(case_sensitive=None, list_name='list', *values): """Creates an annotation for a parameter that only accepts the given values. :param values: ``value, description`` tuples, or just the accepted values :param str list_name: The value the user can use to show a list of possible values and their description. :param bool case_sensitive: Force case-sensitiveness for the input values. The default is to guess based on the contents of values. """ return mapped( list(_conv_oneof(values)), case_sensitive=case_sensitive, list_name=list_name)
class MultiOptionParameter(parser.MultiParameter, parser.OptionParameter): """Named parameter that can collect multiple values.""" def get_collection(self, ba): return ba.kwargs.setdefault(self.argument_name, []) def post_parse(self, ba): super(MultiOptionParameter, self).post_parse(ba) ba.kwargs.setdefault(self.argument_name, []) def unsatisfied(self, ba): if not ba.kwargs.get(self.argument_name): return True raise errors.NotEnoughValues
[docs]def multi(min=0, max=None): """For option parameters, allows the parameter to be repeated on the command-line with an optional minimum or maximum. For ``*args``-like parameters, just adds the optional bounds. .. literalinclude:: /../examples/multi.py :lines: 4-10 """ return parser.use_class( named=MultiOptionParameter, varargs=parser.ExtraPosArgsParameter, kwargs={ 'min': min, 'max': max, }, name="multi")
class _ComposedProperty(object): def __init__(self, name): self.name = name def __get__(self, instance, owner): return getattr(instance.real, self.name) def __set__(self, instance, value): return setattr(instance.real, self.name, value) def __delete__(self, instance): return delattr(instance.real, self.name) class _SubBoundArguments(object): def __init__(self, real): self.real = real self.args = [] self.kwargs = {} sig = _ComposedProperty('sig') name = _ComposedProperty('name') in_args = _ComposedProperty('in_args') func = _ComposedProperty('func') post_name = _ComposedProperty('post_name') meta = _ComposedProperty('meta') sticky = _ComposedProperty('sticky') posarg_only = _ComposedProperty('posarg_only') skip = _ComposedProperty('skip') unsatisfied = _ComposedProperty('unsatisfied') not_provided = _ComposedProperty('not_provided') class _DerivBoundArguments(object): def __init__(self, deriv, real): self.real = real u = self.unsatisfied = set() n = self.not_provided = set() if deriv.sub_required: u.add(deriv) else: n.add(deriv) args = _ComposedProperty('args') kwargs = _ComposedProperty('kwargs') sig = _ComposedProperty('sig') name = _ComposedProperty('name') in_args = _ComposedProperty('in_args') func = _ComposedProperty('func') post_name = _ComposedProperty('post_name') meta = _ComposedProperty('meta') sticky = _ComposedProperty('sticky') posarg_only = _ComposedProperty('posarg_only') skip = _ComposedProperty('skip') class ForwarderParameter(parser.NamedParameter, parser.ParameterWithSourceEquivalent): def __init__(self, real, parent, **kwargs): super(ForwarderParameter, self).__init__( aliases=real.aliases, argument_name=real.argument_name, undocumented=True, **kwargs) self.real = real self.parent = parent self.orig_redispatch = real.redispatch_short_arg real.redispatch_short_arg = self.redispatch_short_arg def get_fba(self, ba): return self.parent.get_meta(ba).get_sub() def read_argument(self, ba, i): self.real.read_argument(self.get_fba(ba), i) def apply_generic_flags(self, ba): self.real.apply_generic_flags(self.get_fba(ba)) def redispatch_short_arg(self, rest, ba, i): self.orig_redispatch(rest, ba.real, i) def _redirect_ba(param, dap): if isinstance(param, parser.NamedParameter): return ForwarderParameter(real=param, parent=dap) raise ValueError("Parameter \"{0}\" cannot be used in an " "argument decorator".format(param)) class _DapMeta(object): def __init__(self, ba, parent): self.ba = ba self.parent = parent self.sub = None self.deriv = None def get_sub(self): if self.sub is None: fba = self.sub = _SubBoundArguments(self.ba) self.ba.unsatisfied.update(self.parent.cli.required) self.ba.not_provided.update(self.parent.cli.optional) return fba else: return self.sub def pop_sub(self): s = self.sub self.sub = None return s def get_deriv(self): if self.deriv is None: fba = self.deriv = _DerivBoundArguments(self.parent, self.ba) return fba else: return self.deriv class DecoratedArgumentParameter(parser.ParameterWithSourceEquivalent): required = True @property def sub_required(self): try: return self._sub_required except AttributeError: attr = super(DecoratedArgumentParameter, type(self)).required return attr.__get__(self, type(self)) def __init__(self, decorator, **kwargs): super(DecoratedArgumentParameter, self).__init__(**kwargs) self.decorator = decorator self.cli = parser.CliSignature.from_signature( signatures.mask(specifiers.signature(decorator), 1)) self.extras = [ _redirect_ba(p, self) for p in self.cli.parameters.values() #if not isinstance(p, ForwarderParameter) ] try: super(DecoratedArgumentParameter, type(self)).required.__get__ except AttributeError: self._sub_required = self.required self.required = True def get_meta(self, ba): return ba.meta.setdefault(self.argument_name, _DapMeta(ba, self)) def coerce_value(self, arg, ba): val = super(DecoratedArgumentParameter, self).coerce_value(arg, ba) d = self.get_meta(ba).pop_sub() if d is None: if self.cli.required: raise errors.MissingRequiredArguments(self.cli.required) args = [] kwargs = {} else: args = d.args kwargs = d.kwargs return self.decorator(val, *args, **kwargs) def __str__(self): pstr = super(DecoratedArgumentParameter, self).__str__() decos = ' '.join( str(p) for p in self.cli.parameters.values() if not p.undocumented ) if not decos: if self.sub_required: return pstr return '[{0}]'.format(pstr) elif self.sub_required: return '{0} {1}'.format(decos, pstr) else: return '[{0} {1}]'.format(decos, pstr) def read_argument(self, ba, i): super(DecoratedArgumentParameter, self).read_argument( self.get_meta(ba).get_deriv(), i) def apply_generic_flags(self, ba): super(DecoratedArgumentParameter, self).apply_generic_flags( self.get_meta(ba).get_deriv()) def unsatisfied(self, ba): m = self.get_meta(ba) if m.sub is not None: raise errors.MissingRequiredArguments((self,)) if m.get_deriv().unsatisfied: return super(DecoratedArgumentParameter, self).unsatisfied(ba) else: return False def prepare_help(self, helper): from clize import help # prevent circular import for p in self.cli.parameters.values(): if not p.undocumented: helper.sections[help.LABEL_OPT][p.argument_name] = (p, '') doc = inspect.getdoc(self.decorator) if doc: helper.parse_docstring( doc.format( param=self, pname=self.display_name )) for p in self.cli.parameters.values(): p.prepare_help(helper)
[docs]def argument_decorator(f): """Decorates a function to create an annotation for adding parameters to qualify another. .. literalinclude:: /../examples/argdeco.py :lines: 5-24 """ return parser.use_mixin( DecoratedArgumentParameter, kwargs={'decorator': f})
class InserterParameter(parser.ParameterWithSourceEquivalent): """Parameter that provides an argument to the called function without requiring an argument on the command line.""" def __init__(self, value_factory, undocumented, default, conv, aliases=None, display_name='constant_parameter', **kwargs): super(InserterParameter, self).__init__( undocumented=True, display_name=display_name, **kwargs) self.required = True self.value_factory = value_factory class InserterPositionalParameter(InserterParameter): def read_argument(self, ba, i): ba.args.append(self.value_factory(ba)) # Get the next pos parameter to process this argument try: param = next(ba.posparam) except StopIteration: raise errors.TooManyArguments(ba.in_args[i]) with errors.SetArgumentErrorContext(param=param): param.read_argument(ba, i) param.apply_generic_flags(ba) def unsatisfied(self, ba): ba.args.append(self.value_factory(ba)) class InserterNamedParameter(InserterParameter): def unsatisfied(self, ba): ba.kwargs[self.argument_name] = self.value_factory(ba)
[docs]def value_inserter(value_factory): """Create an annotation that hides a parameter from the command-line and always gives it the result of a function. :param function value_factory: Called to determine the value to provide for the parameter. The current `.parser.CliBoundArguments` instance is passed as argument, ie. ``value_factory(ba)``. """ try: name = value_factory.__name__ except AttributeError: name = repr(value_factory) uc = parser.use_class( pos=InserterPositionalParameter, named=InserterNamedParameter, kwargs={'value_factory': value_factory}, name='value_inserter({})'.format(name)) update_wrapper(uc, value_factory) return uc
[docs]@value_inserter def pass_name(ba): """Parameters decorated with this will receive the executable name as argument. This can be either the path to a Python file, or ``python -m some.module``. It is also appended with sub-command names. """ return ba.name