tiramisu/option.py:

separate _consistencies (for Option) and _cache_consistencies (for OptionDescription)
  _launch_consistency need index for multi's option
  _cons_not_equal support multi options

tiramisu/value.py:
  Multi._validate support consistency
This commit is contained in:
Emmanuel Garette 2013-09-28 17:05:01 +02:00
parent 482dfec7f2
commit 70f684e70c
4 changed files with 277 additions and 174 deletions

View File

@ -5,6 +5,7 @@ from tiramisu.setting import owners, groups
from tiramisu.config import Config
from tiramisu.option import IPOption, NetworkOption, NetmaskOption, IntOption,\
BroadcastOption, SymLinkOption, OptionDescription
from tiramisu.error import ConfigError
def test_consistency_not_equal():
@ -22,6 +23,60 @@ def test_consistency_not_equal():
c.b = 2
def test_consistency_not_equal_many_opts():
a = IntOption('a', '')
b = IntOption('b', '')
c = IntOption('c', '')
d = IntOption('d', '')
e = IntOption('e', '')
f = IntOption('f', '')
od = OptionDescription('od', '', [a, b, c, d, e, f])
a.impl_add_consistency('not_equal', b, c, d, e, f)
c = Config(od)
assert c.a is None
assert c.b is None
#
c.a = 1
del(c.a)
#
c.a = 1
raises(ValueError, "c.b = 1")
#
c.b = 2
raises(ValueError, "c.f = 2")
raises(ValueError, "c.f = 1")
#
c.d = 3
raises(ValueError, "c.f = 3")
raises(ValueError, "c.a = 3")
raises(ValueError, "c.c = 3")
raises(ValueError, "c.e = 3")
def test_consistency_not_in_config():
a = IntOption('a', '')
b = IntOption('b', '')
a.impl_add_consistency('not_equal', b)
od1 = OptionDescription('od1', '', [a])
od2 = OptionDescription('od2', '', [b])
od = OptionDescription('root', '', [od1])
raises(ConfigError, "Config(od)")
od = OptionDescription('root', '', [od1, od2])
Config(od)
#with subconfig
raises(ConfigError, "Config(od.od1)")
def test_consistency_afer_config():
a = IntOption('a', '')
b = IntOption('b', '')
od1 = OptionDescription('od1', '', [a])
od2 = OptionDescription('od2', '', [b])
od = OptionDescription('root', '', [od1, od2])
Config(od)
raises(AttributeError, "a.impl_add_consistency('not_equal', b)")
def test_consistency_not_equal_symlink():
a = IntOption('a', '')
b = IntOption('b', '')
@ -29,7 +84,7 @@ def test_consistency_not_equal_symlink():
od = OptionDescription('od', '', [a, b, c])
a.impl_add_consistency('not_equal', b)
c = Config(od)
assert set(od._consistencies.keys()) == set([a, b])
assert set(od._cache_consistencies.keys()) == set([a, b])
def test_consistency_not_equal_multi():
@ -53,6 +108,14 @@ def test_consistency_default():
raises(ValueError, "a.impl_add_consistency('not_equal', b)")
def test_consistency_default_multi():
a = IntOption('a', '', [2, 1], multi=True)
b = IntOption('b', '', [1, 1], multi=True)
c = IntOption('c', '', [1, 2], multi=True)
raises(ValueError, "a.impl_add_consistency('not_equal', b)")
a.impl_add_consistency('not_equal', c)
def test_consistency_default_diff():
a = IntOption('a', '', 3)
b = IntOption('b', '', 1)
@ -99,7 +162,7 @@ def test_consistency_ip_netmask_error_multi():
a = IPOption('a', '', multi=True)
b = NetmaskOption('b', '')
od = OptionDescription('od', '', [a, b])
raises(ValueError, "b.impl_add_consistency('ip_netmask', a)")
raises(ConfigError, "b.impl_add_consistency('ip_netmask', a)")
def test_consistency_ip_netmask_multi():
@ -170,11 +233,42 @@ def test_consistency_broadcast():
b.impl_add_consistency('network_netmask', a)
c.impl_add_consistency('broadcast', a, b)
c = Config(od)
#first, test network_netmask
c.a = ['192.168.1.128']
raises(ValueError, "c.b = ['255.255.255.0']")
#
c.a = ['192.168.1.0']
c.b = ['255.255.255.0']
c.c = ['192.168.1.255']
raises(ValueError, "c.a = ['192.168.1.1']")
#
c.a = ['192.168.1.0', '192.168.2.128']
c.b = ['255.255.255.0', '255.255.255.128']
c.c = ['192.168.1.255', '192.168.2.255']
raises(ValueError, "c.c[1] = '192.168.2.128'")
c.c[1] = '192.168.2.255'
def test_consistency_broadcast_default():
a = NetworkOption('a', '', '192.168.1.0')
b = NetmaskOption('b', '', '255.255.255.128')
c = BroadcastOption('c', '', '192.168.2.127')
d = BroadcastOption('d', '', '192.168.1.127')
od = OptionDescription('a', '', [a, b, c])
raises(ValueError, "c.impl_add_consistency('broadcast', a, b)")
od2 = OptionDescription('a', '', [a, b, d])
d.impl_add_consistency('broadcast', a, b)
def test_consistency_not_all():
#_cache_consistencies is not None by not options has consistencies
a = NetworkOption('a', '', multi=True)
b = NetmaskOption('b', '', multi=True)
c = BroadcastOption('c', '', multi=True)
od = OptionDescription('a', '', [a, b, c])
od.impl_set_group_type(groups.master)
b.impl_add_consistency('network_netmask', a)
c = Config(od)
c.a = ['192.168.1.0']
c.b = ['255.255.255.0']
c.c = ['192.168.1.255']

View File

@ -40,7 +40,7 @@ def _diff_opt(opt1, opt2):
if diff2 != set():
raise Exception('more attribute in opt2 {0}'.format(list(diff2)))
for attr in attr1:
if attr in ['_cache_paths']:
if attr in ['_cache_paths', '_cache_consistencies']:
continue
err1 = False
err2 = False

View File

@ -61,9 +61,8 @@ class BaseOption(object):
__setattr__ method
"""
__slots__ = ('_name', '_requires', '_properties', '_readonly',
'_consistencies', '_calc_properties', '_impl_informations',
'_state_consistencies', '_state_readonly', '_state_requires',
'_stated')
'_calc_properties', '_impl_informations',
'_state_readonly', '_state_requires', '_stated')
def __init__(self, name, doc, requires, properties):
if not valid_name(name):
@ -73,7 +72,6 @@ class BaseOption(object):
self.impl_set_information('doc', doc)
self._calc_properties, self._requires = validate_requires_arg(
requires, self._name)
self._consistencies = None
if properties is None:
properties = tuple()
if not isinstance(properties, tuple):
@ -98,8 +96,7 @@ class BaseOption(object):
"frozen" (which has noting to do with the high level "freeze"
propertie or "read_only" property)
"""
if not name.startswith('_state') and name not in ('_cache_paths',
'_consistencies'):
if not name.startswith('_state') and not name.startswith('_cache'):
is_readonly = False
# never change _name
if name == '_name':
@ -109,15 +106,12 @@ class BaseOption(object):
is_readonly = True
except:
pass
elif name != '_readonly':
try:
if self._readonly is True:
if value is True:
# already readonly and try to re set readonly
# don't raise, just exit
return
is_readonly = True
except AttributeError:
pass
self._readonly = False
if is_readonly:
raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
" read-only").format(
@ -149,57 +143,6 @@ class BaseOption(object):
raise ValueError(_("information's item not found: {0}").format(
key))
# serialize/unserialize
def _impl_convert_consistencies(self, descr, load=False):
"""during serialization process, many things have to be done.
one of them is the localisation of the options.
The paths are set once for all.
:type descr: :class:`tiramisu.option.OptionDescription`
:param load: `True` if we are at the init of the option description
:type load: bool
"""
if not load and self._consistencies is None:
self._state_consistencies = None
elif load and self._state_consistencies is None:
self._consistencies = None
del(self._state_consistencies)
else:
if load:
consistencies = self._state_consistencies
else:
consistencies = self._consistencies
if isinstance(consistencies, list):
new_value = []
for consistency in consistencies:
values = []
for obj in consistency[1]:
if load:
values.append(descr.impl_get_opt_by_path(obj))
else:
values.append(descr.impl_get_path_by_opt(obj))
new_value.append((consistency[0], tuple(values)))
else:
new_value = {}
for key, _consistencies in consistencies.items():
new_value[key] = []
for key_cons, _cons in _consistencies:
_list_cons = []
for _con in _cons:
if load:
_list_cons.append(
descr.impl_get_opt_by_path(_con))
else:
_list_cons.append(
descr.impl_get_path_by_opt(_con))
new_value[key].append((key_cons, tuple(_list_cons)))
if load:
del(self._state_consistencies)
self._consistencies = new_value
else:
self._state_consistencies = new_value
def _impl_convert_requires(self, descr, load=False):
"""export of the requires during the serialization process
@ -245,10 +188,7 @@ class BaseOption(object):
for func in dir(self):
if func.startswith('_impl_convert_'):
getattr(self, func)(descr)
try:
self._state_readonly = self._readonly
except AttributeError:
pass
def __getstate__(self, stated=True):
"""special method to enable the serialization with pickle
@ -268,7 +208,8 @@ class BaseOption(object):
for subclass in self.__class__.__mro__:
if subclass is not object:
slots.update(subclass.__slots__)
slots -= frozenset(['_cache_paths', '__weakref__'])
slots -= frozenset(['_cache_paths', '_cache_consistencies',
'__weakref__'])
states = {}
for slot in slots:
# remove variable if save variable converted
@ -327,7 +268,8 @@ class Option(BaseOption):
"""
__slots__ = ('_multi', '_validator', '_default_multi', '_default',
'_state_callback', '_callback', '_multitype',
'_warnings_only', '_master_slaves', '__weakref__')
'_consistencies', '_warnings_only', '_master_slaves',
'_state_consistencies', '__weakref__')
_empty = ''
def __init__(self, name, doc, default=None, default_multi=None,
@ -393,66 +335,58 @@ class Option(BaseOption):
self._warnings_only = warnings_only
self.impl_validate(default)
self._default = default
self._consistencies = None
def _launch_consistency(self, func, right_opt, right_val, context, index,
left_opts):
def _launch_consistency(self, func, option, value, context, index,
all_cons_opts):
"""Launch consistency now
:param func: function name, this name should start with _cons_
:type func: `str`
:param option: option that value is changing
:type option: `tiramisu.option.Option`
:param value: new value of this option
:param context: Config's context, if None, check default value instead
:type context: `tiramisu.config.Config`
:param index: only for multi option, consistency should be launch for
specified index
:type index: `int`
:param all_cons_opts: all options concerne by this consistency
:type all_cons_opts: `list` of `tiramisu.option.Option`
"""
if context is not None:
descr = context.cfgimpl_get_description()
#right_opt is also in left_opts
if right_opt not in left_opts:
raise ConfigError(_('right_opt not in left_opts'))
#option is also in all_cons_opts
if option not in all_cons_opts:
raise ConfigError(_('option not in all_cons_opts'))
left_vals = []
for opt in left_opts:
if right_opt == opt:
value = right_val
all_cons_vals = []
for opt in all_cons_opts:
#get value
if option == opt:
opt_value = value
else:
#if context, calculate value, otherwise get default value
if context is not None:
path = descr.impl_get_path_by_opt(opt)
value = context._getattr(path, validate=False)
opt_value = context._getattr(
descr.impl_get_path_by_opt(opt), validate=False)
else:
value = opt.impl_getdefault()
if index is None:
#could be multi or not
left_vals.append(value)
opt_value = opt.impl_getdefault()
#append value
if not self.impl_is_multi() or option == opt:
all_cons_vals.append(opt_value)
else:
#value is not already set, could be higher
#value is not already set, could be higher index
try:
if right_opt == opt:
val = value
else:
val = value[index]
if val is None:
#no value so no consistencies
return
left_vals.append(val)
all_cons_vals.append(opt_value[index])
except IndexError:
#so return if no value
return
if self.impl_is_multi():
if index is None:
for idx, right_v in enumerate(right_val):
try:
left_v = []
for left_val in left_vals:
left_v.append(left_val[idx])
if None in left_v:
continue
except IndexError:
continue
getattr(self, func)(left_opts, left_v)
else:
if None in left_vals:
return
getattr(self, func)(left_opts, left_vals)
else:
if None in left_vals:
return
getattr(self, func)(left_opts, left_vals)
getattr(self, func)(all_cons_opts, all_cons_vals)
def impl_validate(self, value, context=None, validate=True,
force_no_multi=False):
force_index=None):
"""
:param value: the option's value
:param context: Config's context
@ -509,12 +443,11 @@ class Option(BaseOption):
if context is not None:
descr = context.cfgimpl_get_description()
if not self._multi or force_no_multi:
do_validation(value)
if not self._multi or force_index is not None:
do_validation(value, force_index)
else:
if not isinstance(value, list):
raise ValueError(_("invalid value {0} for option {1} "
"which must be a list").format(value,
raise ValueError(_("which must be a list").format(value,
self._name))
for index, val in enumerate(value):
do_validation(val, index)
@ -561,31 +494,45 @@ class Option(BaseOption):
def impl_is_multi(self):
return self._multi
def impl_add_consistency(self, func, *left_opts):
def impl_add_consistency(self, func, *other_opts):
"""Add consistency means that value will be validate with other_opts
option's values.
:param func: function's name
:type func: `str`
:param other_opts: options used to validate value
:type other_opts: `list` of `tiramisu.option.Option`
"""
if self._consistencies is None:
self._consistencies = []
for opt in left_opts:
for opt in other_opts:
if not isinstance(opt, Option):
raise ValueError(_('consistency should be set with an option'))
raise ConfigError(_('consistency should be set with an option'))
if self is opt:
raise ValueError(_('cannot add consistency with itself'))
raise ConfigError(_('cannot add consistency with itself'))
if self.impl_is_multi() != opt.impl_is_multi():
raise ValueError(_('options in consistency should be multi in '
'two sides'))
raise ConfigError(_('every options in consistency should be '
'multi or none'))
func = '_cons_{0}'.format(func)
opts = tuple([self] + list(left_opts))
self._launch_consistency(func, self, self.impl_getdefault(), None,
None, opts)
self._consistencies.append((func, opts))
all_cons_opts = tuple([self] + list(other_opts))
value = self.impl_getdefault()
if value is not None:
if self.impl_is_multi():
for idx, val in enumerate(value):
self._launch_consistency(func, self, val, None,
idx, all_cons_opts)
else:
self._launch_consistency(func, self, value, None,
None, all_cons_opts)
self._consistencies.append((func, all_cons_opts))
self.impl_validate(self.impl_getdefault())
def _cons_not_equal(self, opts, vals):
if len(opts) != 2:
raise ConfigError(_('invalid len for opts'))
if vals[0] == vals[1]:
raise ValueError(_("invalid value {0} for option {1} "
"must be different as {2} option"
"").format(vals[0], self._name, opts[1]._name))
for idx_inf, val_inf in enumerate(vals):
for idx_sup, val_sup in enumerate(vals[idx_inf + 1:]):
if val_inf == val_sup is not None:
raise ValueError(_("same value for {0} and {1}").format(
opts[idx_inf]._name, opts[idx_inf + idx_sup + 1]._name))
def _impl_convert_callbacks(self, descr, load=False):
if not load and self._callback is None:
@ -621,6 +568,57 @@ class Option(BaseOption):
else:
self._state_callback = (callback, cllbck_prms)
# serialize/unserialize
def _impl_convert_consistencies(self, descr, load=False):
"""during serialization process, many things have to be done.
one of them is the localisation of the options.
The paths are set once for all.
:type descr: :class:`tiramisu.option.OptionDescription`
:param load: `True` if we are at the init of the option description
:type load: bool
"""
if not load and self._consistencies is None:
self._state_consistencies = None
elif load and self._state_consistencies is None:
self._consistencies = None
del(self._state_consistencies)
else:
if load:
consistencies = self._state_consistencies
else:
consistencies = self._consistencies
if isinstance(consistencies, list):
new_value = []
for consistency in consistencies:
values = []
for obj in consistency[1]:
if load:
values.append(descr.impl_get_opt_by_path(obj))
else:
values.append(descr.impl_get_path_by_opt(obj))
new_value.append((consistency[0], tuple(values)))
else:
new_value = {}
for key, _consistencies in consistencies.items():
new_value[key] = []
for key_cons, _cons in _consistencies:
_list_cons = []
for _con in _cons:
if load:
_list_cons.append(
descr.impl_get_opt_by_path(_con))
else:
_list_cons.append(
descr.impl_get_path_by_opt(_con))
new_value[key].append((key_cons, tuple(_list_cons)))
if load:
del(self._state_consistencies)
self._consistencies = new_value
else:
self._state_consistencies = new_value
def _second_level_validation(self, value):
pass
@ -734,7 +732,7 @@ class SymLinkOption(BaseOption):
__slots__ = ('_name', '_opt', '_state_opt')
_opt_type = 'symlink'
#not return _opt consistencies
_consistencies = {}
_consistencies = None
def __init__(self, name, opt):
self._name = name
@ -760,12 +758,6 @@ class SymLinkOption(BaseOption):
del(self._state_opt)
super(SymLinkOption, self)._impl_setstate(descr)
def _impl_convert_consistencies(self, descr, load=False):
if load:
del(self._state_consistencies)
else:
self._state_consistencies = None
class IPOption(Option):
"represents the choice of an ip"
@ -904,10 +896,14 @@ class NetmaskOption(Option):
def _cons_network_netmask(self, opts, vals):
#opts must be (netmask, network) options
if None in vals:
return
self.__cons_netmask(opts, vals[0], vals[1], False)
def _cons_ip_netmask(self, opts, vals):
#opts must be (netmask, ip) options
if None in vals:
return
self.__cons_netmask(opts, vals[0], vals[1], True)
def __cons_netmask(self, opts, val_netmask, val_ipnetwork, make_net):
@ -955,6 +951,8 @@ class BroadcastOption(Option):
def _cons_broadcast(self, opts, vals):
if len(vals) != 3:
raise ConfigError(_('invalid len for vals'))
if None in vals:
return
broadcast, network, netmask = vals
if IP('{0}/{1}'.format(network, netmask)).broadcast() != IP(broadcast):
raise ValueError(_('invalid broadcast {0} ({1}) with network {2} '
@ -964,7 +962,12 @@ class BroadcastOption(Option):
class DomainnameOption(Option):
"represents the choice of a domain name"
"""represents the choice of a domain name
netbios: for MS domain
hostname: to identify the device
domainname:
fqdn: with tld, not supported yet
"""
__slots__ = ('_type', '_allow_ip')
_opt_type = 'domainname'
@ -973,10 +976,6 @@ class DomainnameOption(Option):
callback_params=None, validator=None, validator_params=None,
properties=None, allow_ip=False, type_='domainname',
warnings_only=False):
#netbios: for MS domain
#hostname: to identify the device
#domainname:
#fqdn: with tld, not supported yet
if type_ not in ['netbios', 'hostname', 'domainname']:
raise ValueError(_('unknown type_ {0} for hostname').format(type_))
self._type = type_
@ -1030,9 +1029,9 @@ class OptionDescription(BaseOption):
"""
__slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
'_state_group_type', '_properties', '_children',
'_consistencies', '_calc_properties', '__weakref__',
'_cache_consistencies', '_calc_properties', '__weakref__',
'_readonly', '_impl_informations', '_state_requires',
'_state_consistencies', '_stated', '_state_readonly')
'_stated', '_state_readonly')
_opt_type = 'optiondescription'
def __init__(self, name, doc, children, requires=None, properties=None):
@ -1053,6 +1052,7 @@ class OptionDescription(BaseOption):
old = child
self._children = (tuple(child_names), tuple(children))
self._cache_paths = None
self._cache_consistencies = None
# the group_type is useful for filtering OptionDescriptions in a config
self._group_type = groups.default
@ -1126,11 +1126,11 @@ class OptionDescription(BaseOption):
if not force_no_consistencies and \
option._consistencies is not None:
for consistency in option._consistencies:
func, left_opts = consistency
for opt in left_opts:
func, all_cons_opts = consistency
for opt in all_cons_opts:
_consistencies.setdefault(opt,
[]).append((func,
left_opts))
all_cons_opts))
else:
_currpath.append(attr)
option.impl_build_cache(cache_path,
@ -1142,7 +1142,12 @@ class OptionDescription(BaseOption):
if save:
self._cache_paths = (tuple(cache_option), tuple(cache_path))
if not force_no_consistencies:
self._consistencies = _consistencies
if _consistencies != {}:
self._cache_consistencies = {}
for opt, cons in _consistencies.items():
if opt not in cache_option:
raise ConfigError(_('consistency with option {0} which is not in Config').format(opt._name))
self._cache_consistencies[opt] = tuple(cons)
self._readonly = True
def impl_get_opt_by_path(self, path):
@ -1213,15 +1218,18 @@ class OptionDescription(BaseOption):
def impl_get_group_type(self):
return self._group_type
def _valid_consistency(self, right_opt, right_val, context=None, index=None):
#[('_cons_not_equal', (opt1, opt2))]
consistencies = self._consistencies.get(right_opt)
def _valid_consistency(self, option, value, context, index):
if self._cache_consistencies is None:
return True
#consistencies is something like [('_cons_not_equal', (opt1, opt2))]
consistencies = self._cache_consistencies.get(option)
if consistencies is not None:
for func, opts in consistencies:
#opts[0] is the option where func is set
#opts is left_opts
ret = opts[0]._launch_consistency(func, right_opt, right_val,
context, index, opts)
for func, all_cons_opts in consistencies:
#all_cons_opts[0] is the option where func is set
ret = all_cons_opts[0]._launch_consistency(func, option,
value,
context, index,
all_cons_opts)
if ret is False:
return False
return True
@ -1261,6 +1269,7 @@ class OptionDescription(BaseOption):
"""
if descr is None:
self._cache_paths = None
self._cache_consistencies = None
self.impl_build_cache(force_no_consistencies=True)
descr = self
self._group_type = getattr(groups, self._state_group_type)

View File

@ -455,10 +455,10 @@ class Multi(list):
value_slave.append(slave.impl_getdefault_multi(),
force=True)
def __setitem__(self, key, value):
self._validate(value)
def __setitem__(self, index, value):
self._validate(value, index)
#assume not checking mandatory property
super(Multi, self).__setitem__(key, value)
super(Multi, self).__setitem__(index, value)
self.context().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
def append(self, value, force=False):
@ -476,7 +476,8 @@ class Multi(list):
#Force None il return a list
if isinstance(value, list):
value = None
self._validate(value)
index = self.__len__()
self._validate(value, index)
super(Multi, self).append(value)
self.context().cfgimpl_get_values()._setvalue(self.opt, self.path,
self,
@ -486,7 +487,6 @@ class Multi(list):
path = values._get_opt_path(slave)
if not values._is_default_owner(path):
if slave.impl_has_callback():
index = self.__len__() - 1
dvalue = values._getcallback_value(slave, index=index)
else:
dvalue = slave.impl_getdefault_multi()
@ -538,11 +538,11 @@ class Multi(list):
super(Multi, self).extend(iterable)
self.context().cfgimpl_get_values()._setvalue(self.opt, self.path, self)
def _validate(self, value):
def _validate(self, value, force_index):
if value is not None:
try:
self.opt.impl_validate(value, context=self.context(),
force_no_multi=True)
force_index=force_index)
except ValueError as err:
raise ValueError(_("invalid value {0} "
"for option {1}: {2}"