New Param: added Param.get_default() method and detailed docstring; added corresponding unit tests

This commit is contained in:
Jason Gerard DeRose 2009-01-12 22:48:04 -07:00
parent 5c7c0b35bb
commit 11dce19225
2 changed files with 211 additions and 4 deletions

View File

@ -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)

View File

@ -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):
"""