Change parameters to use only default_from for dynamic default values.

Replace all occurences of create_default with equivalent default_from
and remove create_default from the framework. This is needed for
proper parameter validation, as there is no way to tell which
parameters to validate prior to calling create_default, because
create_default does not provide information about which parameters are
used for generating the default value.
This commit is contained in:
Jan Cholasta
2012-03-15 04:32:37 -04:00
committed by Martin Kosek
parent 5a55e11a25
commit a2299070c8
7 changed files with 30 additions and 115 deletions

View File

@@ -198,6 +198,8 @@ class DefaultFrom(ReadOnly):
self.callback = callback self.callback = callback
if len(keys) == 0: if len(keys) == 0:
fc = callback.func_code fc = callback.func_code
if fc.co_flags & 0x0c:
raise ValueError("callback: variable-length argument list not allowed")
self.keys = fc.co_varnames[:fc.co_argcount] self.keys = fc.co_varnames[:fc.co_argcount]
else: else:
self.keys = keys self.keys = keys
@@ -308,13 +310,9 @@ class Param(ReadOnly):
encoder encoder
- default_from: a custom function for generating default values of - default_from: a custom function for generating default values of
parameter instance parameter instance
- create_default: a custom function for generating default values of
parameter instance. Unlike default_from attribute, this function
is not wrapped. `Param.get_default()` documentation provides further
details
- autofill: by default, only `required` parameters get a default value - autofill: by default, only `required` parameters get a default value
from default_from or create_default functions. When autofill is from the default_from function. When autofill is enabled, optional
enabled, optional attributes get the default value filled too attributes get the default value filled too
- query: this attribute is controlled by framework. When the `query` - query: this attribute is controlled by framework. When the `query`
is enabled, framework assumes that the value is only queried and not is enabled, framework assumes that the value is only queried and not
inserted in the LDAP. Validation is then relaxed - custom inserted in the LDAP. Validation is then relaxed - custom
@@ -379,7 +377,6 @@ class Param(ReadOnly):
('normalizer', callable, None), ('normalizer', callable, None),
('encoder', callable, None), ('encoder', callable, None),
('default_from', DefaultFrom, None), ('default_from', DefaultFrom, None),
('create_default', callable, None),
('autofill', bool, False), ('autofill', bool, False),
('query', bool, False), ('query', bool, False),
('attribute', bool, False), ('attribute', bool, False),
@@ -481,20 +478,6 @@ class Param(ReadOnly):
class_rules.append(getattr(self, rule_name)) class_rules.append(getattr(self, rule_name))
check_name(self.cli_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 only 'include' or 'exclude' was provided: # Check that only 'include' or 'exclude' was provided:
if None not in (self.include, self.exclude): if None not in (self.include, self.exclude):
raise ValueError( raise ValueError(
@@ -990,59 +973,9 @@ class Param(ReadOnly):
>>> kw = dict(first=u'John', department=u'Engineering') >>> kw = dict(first=u'John', department=u'Engineering')
>>> login.get_default(**kw) >>> login.get_default(**kw)
u'my-static-login-default' 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: if self.default_from is not None:
default = self._get_default(**kw) default = self.default_from(**kw)
if default is not None: if default is not None:
try: try:
return self.convert(self.normalize(default)) return self.convert(self.normalize(default))

View File

@@ -234,7 +234,7 @@ def _rname_validator(ugettext, zonemgr):
return unicode(e) return unicode(e)
return None return None
def _create_zone_serial(**kwargs): def _create_zone_serial():
""" Generate serial number for zones. The format follows RFC 1912 """ """ Generate serial number for zones. The format follows RFC 1912 """
return int('%s01' % time.strftime('%Y%m%d')) return int('%s01' % time.strftime('%Y%m%d'))
@@ -1554,7 +1554,7 @@ class dnszone(LDAPObject):
label=_('SOA serial'), label=_('SOA serial'),
doc=_('SOA record serial number'), doc=_('SOA record serial number'),
minvalue=1, minvalue=1,
create_default=_create_zone_serial, default_from=_create_zone_serial,
autofill=True, autofill=True,
), ),
Int('idnssoarefresh', Int('idnssoarefresh',

View File

@@ -69,7 +69,7 @@ class passwd(Command):
label=_('User name'), label=_('User name'),
primary_key=True, primary_key=True,
autofill=True, autofill=True,
create_default=lambda **kw: util.get_current_principal(), default_from=lambda: util.get_current_principal(),
normalizer=lambda value: normalize_principal(value), normalizer=lambda value: normalize_principal(value),
), ),
Password('password', Password('password',

View File

@@ -52,7 +52,7 @@ class join(Command):
validate_host, validate_host,
cli_name='hostname', cli_name='hostname',
doc=_("The hostname to register as"), doc=_("The hostname to register as"),
create_default=lambda **kw: unicode(util.get_fqdn()), default_from=lambda: unicode(util.get_fqdn()),
autofill=True, autofill=True,
#normalizer=lamda value: value.lower(), #normalizer=lamda value: value.lower(),
), ),
@@ -60,7 +60,7 @@ class join(Command):
takes_options= ( takes_options= (
Str('realm', Str('realm',
doc=_("The IPA realm"), doc=_("The IPA realm"),
create_default=lambda **kw: get_realm(), default_from=lambda: get_realm(),
autofill=True, autofill=True,
), ),
Str('nshardwareplatform?', Str('nshardwareplatform?',

View File

@@ -56,9 +56,9 @@ class IPATypeChecker(TypeChecker):
'ipalib.plugins.misc.env': ['env'], 'ipalib.plugins.misc.env': ['env'],
'ipalib.parameters.Param': ['cli_name', 'cli_short_name', 'label', 'ipalib.parameters.Param': ['cli_name', 'cli_short_name', 'label',
'doc', 'required', 'multivalue', 'primary_key', 'normalizer', 'doc', 'required', 'multivalue', 'primary_key', 'normalizer',
'default', 'default_from', 'create_default', 'autofill', 'query', 'default', 'default_from', 'autofill', 'query', 'attribute',
'attribute', 'include', 'exclude', 'flags', 'hint', 'alwaysask', 'include', 'exclude', 'flags', 'hint', 'alwaysask', 'sortorder',
'sortorder', 'csv', 'csv_separator', 'csv_skipspace'], 'csv', 'csv_separator', 'csv_skipspace'],
'ipalib.parameters.Bool': ['truths', 'falsehoods'], 'ipalib.parameters.Bool': ['truths', 'falsehoods'],
'ipalib.parameters.Int': ['minvalue', 'maxvalue'], 'ipalib.parameters.Int': ['minvalue', 'maxvalue'],
'ipalib.parameters.Decimal': ['minvalue', 'maxvalue', 'precision'], 'ipalib.parameters.Decimal': ['minvalue', 'maxvalue', 'precision'],

View File

@@ -45,7 +45,6 @@ PARAM_IGNORED_KW_ATTRIBUTES = ('label',
'normalizer', 'normalizer',
'encoder', 'encoder',
'default_from', 'default_from',
'create_default',
'hint', 'hint',
'flags', 'flags',
'sortorder',) 'sortorder',)

View File

@@ -64,6 +64,16 @@ class test_DefaultFrom(ClassChecker):
e = raises(TypeError, self.cls, callback, 'givenname', 17) e = raises(TypeError, self.cls, callback, 'givenname', 17)
assert str(e) == TYPE_ERROR % ('keys', str, 17, int) assert str(e) == TYPE_ERROR % ('keys', str, 17, int)
# Test that ValueError is raised when inferring keys from a callback
# which has *args:
e = raises(ValueError, self.cls, lambda foo, *args: None)
assert str(e) == "callback: variable-length argument list not allowed"
# Test that ValueError is raised when inferring keys from a callback
# which has **kwargs:
e = raises(ValueError, self.cls, lambda foo, **kwargs: None)
assert str(e) == "callback: variable-length argument list not allowed"
def test_repr(self): def test_repr(self):
""" """
Test the `ipalib.parameters.DefaultFrom.__repr__` method. Test the `ipalib.parameters.DefaultFrom.__repr__` method.
@@ -185,8 +195,6 @@ class test_Param(ClassChecker):
assert o.normalizer is None assert o.normalizer is None
assert o.default is None assert o.default is None
assert o.default_from is None assert o.default_from is None
assert o.create_default is None
assert o._get_default is None
assert o.autofill is False assert o.autofill is False
assert o.query is False assert o.query is False
assert o.attribute is False assert o.attribute is False
@@ -250,16 +258,6 @@ class test_Param(ClassChecker):
assert str(e) == \ assert str(e) == \
"Param('my_param'): takes no such kwargs: 'ape', 'great'" "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 ValueError is raised if you provide both include and # Test that ValueError is raised if you provide both include and
# exclude: # exclude:
e = raises(ValueError, self.cls, 'my_param', e = raises(ValueError, self.cls, 'my_param',
@@ -276,15 +274,11 @@ class test_Param(ClassChecker):
e = raises(ValueError, self.cls, 'my_param', csv=True) e = raises(ValueError, self.cls, 'my_param', csv=True)
assert str(e) == '%s: cannot have csv without multivalue' % "Param('my_param')" assert str(e) == '%s: cannot have csv without multivalue' % "Param('my_param')"
# Test that _get_default gets set: # Test that default_from gets set:
call1 = lambda first, last: first[0] + last call = lambda first, last: first[0] + last
call2 = lambda **kw: 'The Default' o = self.cls('my_param', default_from=call)
o = self.cls('my_param', default_from=call1) assert type(o.default_from) is parameters.DefaultFrom
assert o.default_from.callback is call1 assert o.default_from.callback is call
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): def test_repr(self):
""" """
@@ -579,7 +573,7 @@ class test_Param(ClassChecker):
def test_get_default(self): def test_get_default(self):
""" """
Test the `ipalib.parameters.Param._get_default` method. Test the `ipalib.parameters.Param.get_default` method.
""" """
class PassThrough(object): class PassThrough(object):
value = None value = None
@@ -624,17 +618,6 @@ class test_Param(ClassChecker):
assert o._convert_scalar.value is default assert o._convert_scalar.value is default
assert o.normalizer.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
def test_split_csv(self): def test_split_csv(self):
""" """
Test the `ipalib.parameters.Param.split_csv` method with csv. Test the `ipalib.parameters.Param.split_csv` method with csv.