From 70f684e70c5e4964a9e1acb810622b482ba92551 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 28 Sep 2013 17:05:01 +0200 Subject: [PATCH] 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 --- test/test_option_consistency.py | 98 +++++++++- test/test_state.py | 2 +- tiramisu/option.py | 337 ++++++++++++++++---------------- tiramisu/value.py | 14 +- 4 files changed, 277 insertions(+), 174 deletions(-) diff --git a/test/test_option_consistency.py b/test/test_option_consistency.py index 1c23dac..d5226db 100644 --- a/test/test_option_consistency.py +++ b/test/test_option_consistency.py @@ -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'] diff --git a/test/test_state.py b/test/test_state.py index 4430bd9..ef46ce2 100644 --- a/test/test_state.py +++ b/test/test_state.py @@ -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 diff --git a/tiramisu/option.py b/tiramisu/option.py index d3c4bf9..c7a28c2 100644 --- a/tiramisu/option.py +++ b/tiramisu/option.py @@ -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 - 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 + elif name != '_readonly': + try: + if self._readonly is True: + is_readonly = True + except AttributeError: + 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 + self._state_readonly = self._readonly 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) diff --git a/tiramisu/value.py b/tiramisu/value.py index 6942453..4426742 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -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}"