summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ipalib/parameter.py136
-rw-r--r--tests/test_ipalib/test_parameter.py79
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):
"""