diff options
-rw-r--r-- | ipalib/frontend.py | 453 | ||||
-rw-r--r-- | tests/test_ipalib/test_frontend.py | 414 |
2 files changed, 3 insertions, 864 deletions
diff --git a/ipalib/frontend.py b/ipalib/frontend.py index c614e5479..baa37b176 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -27,7 +27,7 @@ import plugable from plugable import lock, check_name import errors from errors import check_type, check_isinstance, raise_TypeError -import ipa_types +import parameters from util import make_repr @@ -42,453 +42,6 @@ def is_rule(obj): return callable(obj) and getattr(obj, RULE_FLAG, False) is True -class DefaultFrom(plugable.ReadOnly): - """ - Derive a default value from other supplied values. - - For example, say you wanted to create a default for the user's login from - the user's first and last names. It could be implemented like this: - - >>> login = DefaultFrom(lambda first, last: first[0] + last) - >>> login(first='John', last='Doe') - 'JDoe' - - If you do not explicitly provide keys when you create a DefaultFrom - instance, the keys are implicitly derived from your callback by - inspecting ``callback.func_code.co_varnames``. The keys are available - through the ``DefaultFrom.keys`` instance attribute, like this: - - >>> login.keys - ('first', 'last') - - The callback is available through the ``DefaultFrom.callback`` instance - attribute, like this: - - >>> login.callback # doctest:+ELLIPSIS - <function <lambda> at 0x...> - >>> login.callback.func_code.co_varnames # The keys - ('first', 'last') - - The keys can be explicitly provided as optional positional arguments after - the callback. For example, this is equivalent to the ``login`` instance - above: - - >>> login2 = DefaultFrom(lambda a, b: a[0] + b, 'first', 'last') - >>> login2.keys - ('first', 'last') - >>> login2.callback.func_code.co_varnames # Not the keys - ('a', 'b') - >>> login2(first='John', last='Doe') - 'JDoe' - - If any keys are missing when calling your DefaultFrom instance, your - callback is not called and None is returned. For example: - - >>> login(first='John', lastname='Doe') is None - True - >>> login() is None - True - - Any additional keys are simply ignored, like this: - - >>> login(last='Doe', first='John', middle='Whatever') - 'JDoe' - - As above, because `DefaultFrom.__call__` takes only pure keyword - arguments, they can be supplied in any order. - - Of course, the callback need not be a lambda expression. This third - example is equivalent to both the ``login`` and ``login2`` instances - above: - - >>> def get_login(first, last): - ... return first[0] + last - ... - >>> login3 = DefaultFrom(get_login) - >>> login3.keys - ('first', 'last') - >>> login3.callback.func_code.co_varnames - ('first', 'last') - >>> login3(first='John', last='Doe') - 'JDoe' - """ - - def __init__(self, callback, *keys): - """ - :param callback: The callable to call when all keys are present. - :param keys: Optional keys used for source values. - """ - if not callable(callback): - raise TypeError('callback must be callable; got %r' % callback) - self.callback = callback - if len(keys) == 0: - fc = callback.func_code - self.keys = fc.co_varnames[:fc.co_argcount] - else: - self.keys = keys - for key in self.keys: - if type(key) is not str: - raise_TypeError(key, str, 'keys') - lock(self) - - def __call__(self, **kw): - """ - If all keys are present, calls the callback; otherwise returns None. - - :param kw: The keyword arguments. - """ - vals = tuple(kw.get(k, None) for k in self.keys) - if None in vals: - return - try: - return self.callback(*vals) - except StandardError: - pass - - -def parse_param_spec(spec): - """ - Parse a param spec into to (name, kw). - - The ``spec`` string determines the param name, whether the param is - required, and whether the param is multivalue according the following - syntax: - - ====== ===== ======== ========== - Spec Name Required Multivalue - ====== ===== ======== ========== - 'var' 'var' True False - 'var?' 'var' False False - 'var*' 'var' False True - 'var+' 'var' True True - ====== ===== ======== ========== - - For example, - - >>> parse_param_spec('login') - ('login', {'required': True, 'multivalue': False}) - >>> parse_param_spec('gecos?') - ('gecos', {'required': False, 'multivalue': False}) - >>> parse_param_spec('telephone_numbers*') - ('telephone_numbers', {'required': False, 'multivalue': True}) - >>> parse_param_spec('group+') - ('group', {'required': True, 'multivalue': True}) - - :param spec: A spec string. - """ - if type(spec) is not str: - raise_TypeError(spec, str, 'spec') - if len(spec) < 2: - raise ValueError( - 'param spec must be at least 2 characters; got %r' % spec - ) - _map = { - '?': dict(required=False, multivalue=False), - '*': dict(required=False, multivalue=True), - '+': dict(required=True, multivalue=True), - } - end = spec[-1] - if end in _map: - return (spec[:-1], _map[end]) - return (spec, dict(required=True, multivalue=False)) - - -class Param(plugable.ReadOnly): - """ - A parameter accepted by a `Command`. - - ============ ================= ================== - Keyword Type Default - ============ ================= ================== - cli_name str defaults to name - type ipa_type.Type ipa_type.Unicode() - doc str "" - required bool True - multivalue bool False - primary_key bool False - normalize callable None - default same as type.type None - default_from callable None - flags frozenset frozenset() - ============ ================= ================== - """ - __nones = (None, '', tuple(), []) - __defaults = dict( - cli_name=None, - type=ipa_types.Unicode(), - doc='', - required=True, - multivalue=False, - primary_key=False, - normalize=None, - default=None, - default_from=None, - flags=frozenset(), - rules=tuple(), - ) - - def __init__(self, name, **override): - self.__param_spec = name - self.__override = override - self.__kw = dict(self.__defaults) - if not ('required' in override or 'multivalue' in override): - (name, kw_from_spec) = parse_param_spec(name) - self.__kw.update(kw_from_spec) - self.__kw['cli_name'] = name - if not set(self.__kw).issuperset(override): - extra = sorted(set(override) - set(self.__kw)) - raise TypeError( - 'Param.__init__() takes no such kwargs: %s' % ', '.join(extra) - ) - self.__kw.update(override) - self.name = check_name(name) - self.cli_name = check_name(self.__kw.get('cli_name', name)) - self.type = self.__check_isinstance(ipa_types.Type, 'type') - self.doc = self.__check_type(str, 'doc') - self.required = self.__check_type(bool, 'required') - self.multivalue = self.__check_type(bool, 'multivalue') - self.default = self.__kw['default'] - df = self.__kw['default_from'] - if callable(df) and not isinstance(df, DefaultFrom): - df = DefaultFrom(df) - self.default_from = check_type(df, DefaultFrom, 'default_from', - allow_none=True - ) - self.flags = frozenset(self.__kw['flags']) - self.__normalize = self.__kw['normalize'] - self.rules = self.__check_type(tuple, 'rules') - self.all_rules = (self.type.validate,) + self.rules - self.primary_key = self.__check_type(bool, 'primary_key') - lock(self) - - def ispassword(self): - """ - Return ``True`` is this Param is a password. - """ - return 'password' in self.flags - - def __clone__(self, **override): - """ - Return a new `Param` instance similar to this one. - """ - kw = dict(self.__kw) - kw.update(override) - return self.__class__(self.name, **kw) - - def __check_type(self, type_, name, allow_none=False): - value = self.__kw[name] - return check_type(value, type_, name, allow_none) - - def __check_isinstance(self, type_, name, allow_none=False): - value = self.__kw[name] - return check_isinstance(value, type_, name, allow_none) - - def __dispatch(self, value, scalar): - """ - Helper method used by `normalize` and `convert`. - """ - if value in self.__nones: - return - if self.multivalue: - if type(value) in (tuple, list): - return tuple( - scalar(v, i) for (i, v) in enumerate(value) - ) - return (scalar(value, 0),) # tuple - return scalar(value) - - def __normalize_scalar(self, value, index=None): - """ - Normalize a scalar value. - - This method is called once with each value in multivalue. - """ - if not isinstance(value, basestring): - return value - try: - return self.__normalize(value) - except StandardError: - return value - - def normalize(self, value): - """ - Normalize ``value`` using normalize callback. - - For example: - - >>> param = Param('telephone', - ... normalize=lambda value: value.replace('.', '-') - ... ) - >>> param.normalize('800.123.4567') - '800-123-4567' - - If this `Param` instance does not have a normalize callback, - ``value`` is returned unchanged. - - If this `Param` instance has a normalize callback and ``value`` is - a basestring, the normalize callback is called and its return value - is returned. - - If ``value`` is not a basestring, or if an exception is caught - when calling the normalize callback, ``value`` is returned unchanged. - - :param value: A proposed value for this parameter. - """ - if self.__normalize is None: - return value - return self.__dispatch(value, self.__normalize_scalar) - - def __convert_scalar(self, value, index=None): - """ - Convert a scalar value. - - This method is called once with each value in multivalue. - """ - if value in self.__nones: - return - converted = self.type(value) - if converted is None: - raise errors.ConversionError( - self.name, value, self.type, index=index - ) - return converted - - def convert(self, value): - """ - Convert/coerce ``value`` to Python type for this `Param`. - - For example: - - >>> param = Param('an_int', type=ipa_types.Int()) - >>> param.convert(7.2) - 7 - >>> param.convert(" 7 ") - 7 - - If ``value`` can not be converted, ConversionError is raised, which - is as subclass of ValidationError. - - If ``value`` is None, conversion is not attempted and None is - returned. - - :param value: A proposed value for this parameter. - """ - return self.__dispatch(value, self.__convert_scalar) - - def __validate_scalar(self, value, index=None): - """ - Validate a scalar value. - - This method is called once with each value in multivalue. - """ - if type(value) is not self.type.type: - raise_TypeError(value, self.type.type, 'value') - for rule in self.rules: - error = rule(value) - if error is not None: - raise errors.RuleError( - self.name, value, error, rule, index=index - ) - - def validate(self, value): - """ - Check validity of a value. - - Each validation rule is called in turn and if any returns and error, - RuleError is raised, which is a subclass of ValidationError. - - :param value: A proposed value for this parameter. - """ - if value is None: - if self.required: - raise errors.RequirementError(self.name) - return - if self.multivalue: - if type(value) is not tuple: - raise_TypeError(value, tuple, 'value') - for (i, v) in enumerate(value): - self.__validate_scalar(v, i) - else: - self.__validate_scalar(value) - - def get_default(self, **kw): - """ - Return a default value for this parameter. - - If this `Param` instance does not have a default_from() callback, this - method always returns the static Param.default instance attribute. - - On the other hand, if this `Param` instance has a default_from() - callback, the callback is called and its return value is returned - (assuming that value is not None). - - If the default_from() callback returns None, or if an exception is - caught when calling the default_from() callback, the static - Param.default instance attribute is returned. - - :param kw: Optional keyword arguments to pass to default_from(). - """ - if self.default_from is not None: - default = self.default_from(**kw) - if default is not None: - try: - return self.convert(self.normalize(default)) - except errors.ValidationError: - return None - return self.default - - def get_values(self): - """ - Return a tuple of possible values. - - For enumerable types, a tuple containing the possible values is - returned. For all other types, an empty tuple is returned. - """ - if self.type.name in ('Enum', 'CallbackEnum'): - return self.type.values - return tuple() - - def __call__(self, value, **kw): - if value in self.__nones: - value = self.get_default(**kw) - else: - value = self.convert(self.normalize(value)) - self.validate(value) - return value - - def __repr__(self): - """ - Return an expresion that could construct this `Param` instance. - """ - return make_repr( - self.__class__.__name__, - self.__param_spec, - **self.__override - ) - - -def create_param(spec): - """ - Create a `Param` instance from a param spec. - - If ``spec`` is a `Param` instance, ``spec`` is returned unchanged. - - If ``spec`` is an str instance, then ``spec`` is parsed and an - appropriate `Param` instance is created and returned. - - See `parse_param_spec` for the definition of the spec syntax. - - :param spec: A spec string or a `Param` instance. - """ - if type(spec) is Param: - return spec - if type(spec) is not str: - raise TypeError( - 'create_param() takes %r or %r; got %r' % (str, Param, spec) - ) - return Param(spec) - - class Command(plugable.Plugin): """ A public IPA atomic operation. @@ -810,7 +363,7 @@ class LocalOrRemote(Command): """ takes_options = ( - Param('server?', type=ipa_types.Bool(), default=False, + parameters.Flag('server?', doc='Forward to server instead of running locally', ), ) @@ -1064,7 +617,7 @@ class Property(Attribute): 'type', )).union(Attribute.__public__) - type = ipa_types.Unicode() + type = parameters.Str required = False multivalue = False default = None diff --git a/tests/test_ipalib/test_frontend.py b/tests/test_ipalib/test_frontend.py index 94e586fe9..c40933175 100644 --- a/tests/test_ipalib/test_frontend.py +++ b/tests/test_ipalib/test_frontend.py @@ -70,420 +70,6 @@ def test_is_rule(): assert not is_rule(call(None)) -class test_DefaultFrom(ClassChecker): - """ - Test the `ipalib.frontend.DefaultFrom` class. - """ - _cls = frontend.DefaultFrom - - def test_class(self): - """ - Test the `ipalib.frontend.DefaultFrom` class. - """ - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_init(self): - """ - Test the `ipalib.frontend.DefaultFrom.__init__` method. - """ - def callback(*args): - return args - keys = ('givenname', 'sn') - o = self.cls(callback, *keys) - assert read_only(o, 'callback') is callback - assert read_only(o, 'keys') == keys - lam = lambda first, last: first[0] + last - o = self.cls(lam) - assert read_only(o, 'keys') == ('first', 'last') - - def test_call(self): - """ - Test the `ipalib.frontend.DefaultFrom.__call__` method. - """ - def callback(givenname, sn): - return givenname[0] + sn[0] - keys = ('givenname', 'sn') - o = self.cls(callback, *keys) - kw = dict( - givenname='John', - sn='Public', - hello='world', - ) - assert o(**kw) == 'JP' - assert o() is None - for key in ('givenname', 'sn'): - kw_copy = dict(kw) - del kw_copy[key] - assert o(**kw_copy) is None - - # Test using implied keys: - o = self.cls(lambda first, last: first[0] + last) - assert o(first='john', last='doe') == 'jdoe' - assert o(first='', last='doe') is None - assert o(one='john', two='doe') is None - - # Test that co_varnames slice is used: - def callback2(first, last): - letter = first[0] - return letter + last - o = self.cls(callback2) - assert o.keys == ('first', 'last') - assert o(first='john', last='doe') == 'jdoe' - - -def test_parse_param_spec(): - """ - Test the `ipalib.frontend.parse_param_spec` function. - """ - f = frontend.parse_param_spec - - assert f('name') == ('name', dict(required=True, multivalue=False)) - assert f('name?') == ('name', dict(required=False, multivalue=False)) - assert f('name*') == ('name', dict(required=False, multivalue=True)) - assert f('name+') == ('name', dict(required=True, multivalue=True)) - - -class test_Param(ClassChecker): - """ - Test the `ipalib.frontend.Param` class. - """ - _cls = frontend.Param - - def test_class(self): - """ - Test the `ipalib.frontend.Param` class. - """ - assert self.cls.__bases__ == (plugable.ReadOnly,) - - def test_init(self): - """ - Test the `ipalib.frontend.Param.__init__` method. - """ - name = 'sn' - o = self.cls(name) - assert o.__islocked__() is True - - # Test default values - assert read_only(o, 'name') is name - assert read_only(o, 'cli_name') is name - assert isinstance(read_only(o, 'type'), ipa_types.Unicode) - assert read_only(o, 'doc') == '' - assert read_only(o, 'required') is True - assert read_only(o, 'multivalue') is False - assert read_only(o, 'default') is None - assert read_only(o, 'default_from') is None - assert read_only(o, 'flags') == frozenset() - assert read_only(o, 'rules') == tuple() - assert len(read_only(o, 'all_rules')) == 1 - assert read_only(o, 'primary_key') is False - - # Test all kw args: - t = ipa_types.Int() - assert self.cls(name, cli_name='last').cli_name == 'last' - assert self.cls(name, type=t).type is t - assert self.cls(name, doc='the doc').doc == 'the doc' - assert self.cls(name, required=False).required is False - assert self.cls(name, multivalue=True).multivalue is True - assert self.cls(name, default=u'Hello').default == u'Hello' - df = frontend.DefaultFrom(lambda f, l: f + l, - 'first', 'last', - ) - lam = lambda first, last: first + last - for cb in (df, lam): - o = self.cls(name, default_from=cb) - assert type(o.default_from) is frontend.DefaultFrom - assert o.default_from.keys == ('first', 'last') - assert o.default_from.callback('butt', 'erfly') == 'butterfly' - assert self.cls(name, flags=('one', 'two', 'three')).flags == \ - frozenset(['one', 'two', 'three']) - rules = (lambda whatever: 'Not okay!',) - o = self.cls(name, rules=rules) - assert o.rules is rules - assert o.all_rules[1:] == rules - assert self.cls(name, primary_key=True).primary_key is True - - # Test default type_: - o = self.cls(name) - assert isinstance(o.type, ipa_types.Unicode) - - # Test param spec parsing: - o = self.cls('name?') - assert o.name == 'name' - assert o.required is False - assert o.multivalue is False - - o = self.cls('name*') - assert o.name == 'name' - assert o.required is False - assert o.multivalue is True - - o = self.cls('name+') - assert o.name == 'name' - assert o.required is True - assert o.multivalue is True - - e = raises(TypeError, self.cls, name, whatever=True, another=False) - assert str(e) == \ - 'Param.__init__() takes no such kwargs: another, whatever' - - def test_ispassword(self): - """ - Test the `ipalib.frontend.Param.ispassword` method. - """ - name = 'userpassword' - okay = 'password' - nope = ['', 'pass', 'word', 'passwd'] - for flag in nope: - o = self.cls(name, flags=[flag]) - assert o.ispassword() is False - o = self.cls(name, flags=[flag, okay]) - assert o.ispassword() is True - assert self.cls(name).ispassword() is False - assert self.cls(name, flags=[okay]).ispassword() is True - assert self.cls(name, flags=[okay]+nope).ispassword() is True - - def test_clone(self): - """ - Test the `ipalib.frontend.Param.__clone__` method. - """ - def compare(o, kw): - for (k, v) in kw.iteritems(): - assert getattr(o, k) == v, (k, v, getattr(o, k)) - default = dict( - required=False, - multivalue=False, - default=None, - default_from=None, - rules=tuple(), - ) - name = 'hair_color?' - type_ = ipa_types.Int() - o = self.cls(name, type=type_) - compare(o, default) - - override = dict(multivalue=True, default=42) - d = dict(default) - d.update(override) - clone = o.__clone__(**override) - assert clone.name == 'hair_color' - assert clone.type is o.type - compare(clone, d) - - def test_convert(self): - """ - Test the `ipalib.frontend.Param.convert` method. - """ - name = 'some_number' - type_ = ipa_types.Int() - okay = (7, 7L, 7.0, ' 7 ') - fail = ('7.0', '7L', 'whatever', object) - none = (None, '', u'', tuple(), []) - - # Scenario 1: multivalue=False - o = self.cls(name, type=type_) - for n in none: - assert o.convert(n) is None - for value in okay: - new = o.convert(value) - assert new == 7 - assert type(new) is int - for value in fail: - e = raises(errors.ConversionError, o.convert, value) - assert e.name is name - assert e.value is value - assert e.error is type_.conversion_error - assert e.index is None - - # Scenario 2: multivalue=True - o = self.cls(name, type=type_, multivalue=True) - for n in none: - assert o.convert(n) is None - for value in okay: - assert o.convert((value,)) == (7,) - assert o.convert([value]) == (7,) - assert o.convert(okay) == tuple(int(v) for v in okay) - cnt = 5 - for value in fail: - for i in xrange(cnt): - others = list(7 for x in xrange(cnt)) - others[i] = value - for v in [tuple(others), list(others)]: - e = raises(errors.ConversionError, o.convert, v) - assert e.name is name - assert e.value is value - assert e.error is type_.conversion_error - assert e.index == i - - def test_normalize(self): - """ - Test the `ipalib.frontend.Param.normalize` method. - """ - name = 'sn' - callback = lambda value: value.lower() - values = (None, u'Hello', (u'Hello',), 'hello', ['hello']) - none = (None, '', u'', tuple(), []) - - # Scenario 1: multivalue=False, normalize=None - o = self.cls(name) - for v in values: - # When normalize=None, value is returned, no type checking: - assert o.normalize(v) is v - - # Scenario 2: multivalue=False, normalize=callback - o = self.cls(name, normalize=callback) - for v in (u'Hello', u'hello', 'Hello'): # Okay - assert o.normalize(v) == 'hello' - for v in [None, 42, (u'Hello',)]: # Not basestring - assert o.normalize(v) is v - for n in none: - assert o.normalize(n) is None - - # Scenario 3: multivalue=True, normalize=None - o = self.cls(name, multivalue=True) - for v in values: - # When normalize=None, value is returned, no type checking: - assert o.normalize(v) is v - - # Scenario 4: multivalue=True, normalize=callback - o = self.cls(name, multivalue=True, normalize=callback) - assert o.normalize([]) is None - assert o.normalize(tuple()) is None - for value in [(u'Hello',), (u'hello',), 'Hello', ['Hello']]: # Okay - assert o.normalize(value) == (u'hello',) - fail = 42 # Not basestring - for v in [[fail], (u'hello', fail)]: # Non basestring member - assert o.normalize(v) == tuple(v) - for n in none: - assert o.normalize(n) is None - - def test_validate(self): - """ - Test the `ipalib.frontend.Param.validate` method. - """ - name = 'sn' - type_ = ipa_types.Unicode() - def case_rule(value): - if not value.islower(): - return 'Must be lower case' - my_rules = (case_rule,) - okay = u'whatever' - fail_case = u'Whatever' - fail_type = 'whatever' - - # Scenario 1: multivalue=False - o = self.cls(name, type=type_, rules=my_rules) - assert o.rules == my_rules - assert o.all_rules == (type_.validate, case_rule) - o.validate(okay) - e = raises(errors.RuleError, o.validate, fail_case) - assert e.name is name - assert e.value is fail_case - assert e.error == 'Must be lower case' - assert e.rule is case_rule - assert e.index is None - check_TypeError(fail_type, unicode, 'value', o.validate, fail_type) - - ## Scenario 2: multivalue=True - o = self.cls(name, type=type_, multivalue=True, rules=my_rules) - o.validate((okay,)) - cnt = 5 - for i in xrange(cnt): - others = list(okay for x in xrange(cnt)) - others[i] = fail_case - value = tuple(others) - e = raises(errors.RuleError, o.validate, value) - assert e.name is name - assert e.value is fail_case - assert e.error == 'Must be lower case' - assert e.rule is case_rule - assert e.index == i - for not_tuple in (okay, [okay]): - check_TypeError(not_tuple, tuple, 'value', o.validate, not_tuple) - for has_str in [(fail_type,), (okay, fail_type)]: - check_TypeError(fail_type, unicode, 'value', o.validate, has_str) - - def test_get_default(self): - """ - Test the `ipalib.frontend.Param.get_default` method. - """ - name = 'greeting' - default = u'Hello, world!' - default_from = frontend.DefaultFrom( - lambda first, last: u'Hello, %s %s!' % (first, last), - 'first', 'last' - ) - - # Scenario 1: multivalue=False - o = self.cls(name, - default=default, - default_from=default_from, - ) - assert o.default is default - assert o.default_from is default_from - assert o.get_default() == default - assert o.get_default(first='John', last='Doe') == 'Hello, John Doe!' - - # Scenario 2: multivalue=True - default = (default,) - o = self.cls(name, - default=default, - default_from=default_from, - multivalue=True, - ) - assert o.default is default - assert o.default_from is default_from - assert o.get_default() == default - assert o.get_default(first='John', last='Doe') == ('Hello, John Doe!',) - - def test_get_value(self): - """ - Test the `ipalib.frontend.Param.get_values` method. - """ - name = 'status' - values = (u'Active', u'Inactive') - o = self.cls(name, type=ipa_types.Unicode()) - assert o.get_values() == tuple() - o = self.cls(name, type=ipa_types.Enum(*values)) - assert o.get_values() == values - - def test_repr(self): - """ - Test the `ipalib.frontend.Param.__repr__` method. - """ - for name in ['name', 'name?', 'name*', 'name+']: - o = self.cls(name) - assert repr(o) == 'Param(%r)' % name - o = self.cls('name', required=False) - assert repr(o) == "Param('name', required=False)" - o = self.cls('name', multivalue=True) - assert repr(o) == "Param('name', multivalue=True)" - - -def test_create_param(): - """ - Test the `ipalib.frontend.create_param` function. - """ - f = frontend.create_param - for name in ['arg', 'arg?', 'arg*', 'arg+']: - o = f(name) - assert type(o) is frontend.Param - assert type(o.type) is ipa_types.Unicode - assert o.name == 'arg' - assert f(o) is o - o = f('arg') - assert o.required is True - assert o.multivalue is False - o = f('arg?') - assert o.required is False - assert o.multivalue is False - o = f('arg*') - assert o.required is False - assert o.multivalue is True - o = f('arg+') - assert o.required is True - assert o.multivalue is True - - class test_Command(ClassChecker): """ Test the `ipalib.frontend.Command` class. |