diff options
-rw-r--r-- | ipalib/parameter.py | 136 | ||||
-rw-r--r-- | tests/test_ipalib/test_parameter.py | 79 |
2 files changed, 211 insertions, 4 deletions
diff --git a/ipalib/parameter.py b/ipalib/parameter.py index 0890160a0..b5adf58f8 100644 --- a/ipalib/parameter.py +++ b/ipalib/parameter.py @@ -207,7 +207,7 @@ class Param(ReadOnly): ('multivalue', bool, False), ('primary_key', bool, False), ('normalizer', callable, None), - ('default_from', callable, None), + ('default_from', DefaultFrom, None), ('create_default', callable, None), ('flags', frozenset, frozenset()), @@ -282,6 +282,20 @@ class Param(ReadOnly): class_rules.append(getattr(self, rule_name)) check_name(self.cli_name) + # Check that only default_from or create_default was provided: + assert not hasattr(self, '_get_default'), self.nice + if callable(self.default_from): + if callable(self.create_default): + raise ValueError( + '%s: cannot have both %r and %r' % ( + self.nice, 'default_from', 'create_default') + ) + self._get_default = self.default_from + elif callable(self.create_default): + self._get_default = self.create_default + else: + self._get_default = None + # Check that all the rules are callable self.class_rules = tuple(class_rules) self.rules = rules @@ -468,6 +482,117 @@ class Param(ReadOnly): if error is not None: raise ValidationError(name=self.name, error=error, index=index) + def get_default(self, **kw): + """ + Return the static default or construct and return a dynamic default. + + (In these examples, we will use the `Str` and `Bytes` classes, which + both subclass from `Param`.) + + The *default* static default is ``None``. For example: + + >>> s = Str('my_str') + >>> s.default is None + True + >>> s.get_default() is None + True + + However, you can provide your own static default via the ``default`` + keyword argument when you create your `Param` instance. For example: + + >>> s = Str('my_str', default=u'My Static Default') + >>> s.default + u'My Static Default' + >>> s.get_default() + u'My Static Default' + + If you need to generate a dynamic default from other supplied parameter + values, provide a callback via the ``default_from`` keyword argument. + This callback will be automatically wrapped in a `DefaultFrom` instance + if it isn't one already (see the `DefaultFrom` class for all the gory + details). For example: + + >>> login = Str('login', default=u'my-static-login-default', + ... default_from=lambda first, last: (first[0] + last).lower(), + ... ) + >>> isinstance(login.default_from, DefaultFrom) + True + >>> login.default_from.keys + ('first', 'last') + + Then when all the keys needed by the `DefaultFrom` instance are present, + the dynamic default is constructed and returned. For example: + + >>> kw = dict(last=u'Doe', first=u'John') + >>> login.get_default(**kw) + u'jdoe' + + Or if any keys are missing, your *static* default is returned. + For example: + + >>> kw = dict(first=u'John', department=u'Engineering') + >>> login.get_default(**kw) + u'my-static-login-default' + + The second, less common way to construct a dynamic default is to provide + a callback via the ``create_default`` keyword argument. Unlike a + ``default_from`` callback, your ``create_default`` callback will not get + wrapped in any dispatcher. Instead, it will be called directly, which + means your callback must accept arbitrary keyword arguments, although + whether your callback utilises these values is up to your + implementation. For example: + + >>> def make_csr(**kw): + ... print ' make_csr(%r)' % (kw,) # Note output below + ... return 'Certificate Signing Request' + ... + >>> csr = Bytes('csr', create_default=make_csr) + + Your ``create_default`` callback will be called with whatever keyword + arguments are passed to `Param.get_default()`. For example: + + >>> kw = dict(arbitrary='Keyword', arguments='Here') + >>> csr.get_default(**kw) + make_csr({'arguments': 'Here', 'arbitrary': 'Keyword'}) + 'Certificate Signing Request' + + And your ``create_default`` callback is called even if + `Param.get_default()` is called with *zero* keyword arguments. + For example: + + >>> csr.get_default() + make_csr({}) + 'Certificate Signing Request' + + The ``create_default`` callback will most likely be used as a + pre-execute hook to perform some special client-side operation. For + example, the ``csr`` parameter above might make a call to + ``/usr/bin/openssl``. However, often a ``create_default`` callback + could also be implemented as a ``default_from`` callback. When this is + the case, a ``default_from`` callback should be used as they are more + structured and therefore less error-prone. + + The ``default_from`` and ``create_default`` keyword arguments are + mutually exclusive. If you provide both, a ``ValueError`` will be + raised. For example: + + >>> homedir = Str('home', + ... default_from=lambda login: '/home/%s' % login, + ... create_default=lambda **kw: '/lets/use/this', + ... ) + Traceback (most recent call last): + ... + ValueError: Str('home'): cannot have both 'default_from' and 'create_default' + """ + if self._get_default is not None: + default = self._get_default(**kw) + if default is not None: + try: + return self.convert(self.normalize(default)) + except StandardError: + pass + return self.default + class Bool(Param): """ @@ -535,6 +660,12 @@ class Bytes(Param): self.nice, self.minlength) ) + def _convert_scalar(self, value, index=None): + """ + Implement in subclass. + """ + return value + def _rule_minlength(self, _, name, value): """ Check minlength constraint. @@ -582,9 +713,6 @@ class Str(Bytes): ('pattern', unicode, None), ) - def __init__(self, name, **kw): - super(Str, self).__init__(name, **kw) - def _convert_scalar(self, value, index=None): if type(value) in (self.type, int, float, bool): return self.type(value) diff --git a/tests/test_ipalib/test_parameter.py b/tests/test_ipalib/test_parameter.py index b379379cd..7f1b11ea4 100644 --- a/tests/test_ipalib/test_parameter.py +++ b/tests/test_ipalib/test_parameter.py @@ -161,6 +161,7 @@ class test_Param(ClassChecker): assert o.default is None assert o.default_from is None assert o.create_default is None + assert o._get_default is None assert o.flags == frozenset() # Test that ValueError is raised when a kwarg from a subclass @@ -205,6 +206,26 @@ class test_Param(ClassChecker): assert str(e) == \ "Param('my_param'): takes no such kwargs: 'ape', 'great'" + # Test that ValueError is raised if you provide both default_from and + # create_default: + e = raises(ValueError, self.cls, 'my_param', + default_from=lambda first, last: first[0] + last, + create_default=lambda **kw: 'The Default' + ) + assert str(e) == '%s: cannot have both %r and %r' % ( + "Param('my_param')", 'default_from', 'create_default', + ) + + # Test that _get_default gets set: + call1 = lambda first, last: first[0] + last + call2 = lambda **kw: 'The Default' + o = self.cls('my_param', default_from=call1) + assert o.default_from.callback is call1 + assert o._get_default is o.default_from + o = self.cls('my_param', create_default=call2) + assert o.create_default is call2 + assert o._get_default is call2 + def test_repr(self): """ Test the `ipalib.parameter.Param.__repr__` method. @@ -427,6 +448,64 @@ class test_Param(ClassChecker): (request.ugettext, False), ] + def test_get_default(self): + """ + Test the `ipalib.parameter.Param._get_default` method. + """ + class PassThrough(object): + value = None + + def __call__(self, value): + assert self.value is None + assert value is not None + self.value = value + return value + + def reset(self): + assert self.value is not None + self.value = None + + class Str(self.cls): + type = unicode + + def __init__(self, name, **kw): + self._convert_scalar = PassThrough() + super(Str, self).__init__(name, **kw) + + # Test with only a static default: + o = Str('my_str', + normalizer=PassThrough(), + default=u'Static Default', + ) + assert_equal(o.get_default(), u'Static Default') + assert o._convert_scalar.value is None + assert o.normalizer.value is None + + # Test with default_from: + o = Str('my_str', + normalizer=PassThrough(), + default=u'Static Default', + default_from=lambda first, last: first[0] + last, + ) + assert_equal(o.get_default(), u'Static Default') + assert o._convert_scalar.value is None + assert o.normalizer.value is None + default = o.get_default(first=u'john', last='doe') + assert_equal(default, u'jdoe') + assert o._convert_scalar.value is default + assert o.normalizer.value is default + + # Test with create_default: + o = Str('my_str', + normalizer=PassThrough(), + default=u'Static Default', + create_default=lambda **kw: u'The created default', + ) + default = o.get_default(first=u'john', last='doe') + assert_equal(default, u'The created default') + assert o._convert_scalar.value is default + assert o.normalizer.value is default + class test_Bytes(ClassChecker): """ |