diff --git a/ChangeLog b/ChangeLog index 134251e..1be2c27 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,7 @@ Wed Nov 16 22:30:12 2016 +0200 Emmanuel Garette - * consistency "not_equal" works now with multi + * consistency "not_equal" works now with multi and submulti + * a multi or submulti could be "unique" (same value one time) + * consistency "not_equal" means "unique" too Wed Oct 12 21:55:53 2016 +0200 Emmanuel Garette * consistency is now check "not_equal" if one option has diff --git a/test/test_multi.py b/test/test_multi.py index 5f51942..2cb541e 100644 --- a/test/test_multi.py +++ b/test/test_multi.py @@ -24,6 +24,25 @@ def test_multi(): raises(ConfigError, "multi._getcontext()") +def test_multi_unique(): + i = IntOption('int', '', multi=True, unique=True) + o = OptionDescription('od', '', [i]) + c = Config(o) + assert c.int == [] + c.int = [0] + assert c.int == [0] + raises(ValueError, "c.int = [0, 0]") + raises(ValueError, "c.int = [1, 0, 2, 3, 4, 5, 6, 0, 7]") + raises(ValueError, "c.int.append(0)") + raises(ValueError, "c.int.extend([1, 2, 1, 3])") + raises(ValueError, "c.int.extend([1, 2, 0, 3])") + c.int.extend([4, 5, 6]) + + +def test_non_multi_unique(): + raises(ValueError, "IntOption('int', '', unique=True)") + + def test_multi_none(): s = StrOption('str', '', multi=True) o = OptionDescription('od', '', [s]) diff --git a/test/test_submulti.py b/test/test_submulti.py index d43035e..8793387 100644 --- a/test/test_submulti.py +++ b/test/test_submulti.py @@ -4,7 +4,7 @@ do_autopath() from tiramisu.setting import groups, owners from tiramisu.config import Config -from tiramisu.option import StrOption, OptionDescription, submulti +from tiramisu.option import StrOption, IntOption, OptionDescription, submulti from tiramisu.value import SubMulti, Multi from tiramisu.error import SlaveError @@ -663,3 +663,19 @@ def test_callback_submulti(): assert cfg.getowner(multi2) == owners.default assert cfg.multi == [['val']] assert cfg.multi2 == [['val']] + + +def test_submulti_unique(): + i = IntOption('int', '', multi=submulti, unique=True) + o = OptionDescription('od', '', [i]) + c = Config(o) + assert c.int == [] + c.int = [[0]] + assert c.int == [[0]] + raises(ValueError, "c.int = [[0, 0]]") + c.int = [[0], [0]] + raises(ValueError, "c.int[0] = [1, 0, 2, 3, 4, 5, 6, 0, 7]") + raises(ValueError, "c.int[0].append(0)") + raises(ValueError, "c.int[0].extend([1, 2, 1, 3])") + raises(ValueError, "c.int[0].extend([1, 2, 0, 3])") + c.int[0].extend([4, 5, 6]) diff --git a/tiramisu/option/baseoption.py b/tiramisu/option/baseoption.py index fa92fdb..c22cd5b 100644 --- a/tiramisu/option/baseoption.py +++ b/tiramisu/option/baseoption.py @@ -102,7 +102,7 @@ class Base(StorageBase): __slots__ = tuple() def __init__(self, name, doc, default=None, default_multi=None, - requires=None, multi=False, callback=None, + requires=None, multi=False, unique=undefined, callback=None, callback_params=None, validator=None, validator_params=None, properties=None, warnings_only=False, extra=None, allow_empty_list=undefined, session=None): @@ -122,6 +122,10 @@ class Base(StorageBase): _multi = submulti else: raise ValueError(_('invalid multi value')) + if unique != undefined and not isinstance(unique, bool): + raise ValueError(_('unique must be a boolean')) + if not is_multi and unique == True: + raise ValueError(_('unique must be set only with multi value')) if requires is not None: calc_properties, requires = validate_requires_arg(is_multi, requires, name) @@ -149,7 +153,7 @@ class Base(StorageBase): session = self.getsession() StorageBase.__init__(self, name, _multi, warnings_only, doc, extra, calc_properties, requires, properties, - allow_empty_list, session=session) + allow_empty_list, unique, session=session) if multi is not False and default is None: default = [] err = self.impl_validate(default, is_multi=is_multi) @@ -175,7 +179,7 @@ class Base(StorageBase): " yet for option {0}").format( self.impl_getname())) if not _init and self.impl_get_callback()[0] is not None: - raise ConfigError(_("a callback is already set for option {0}, " + raise ConfigError(_("a callback is already set for {0}, " "cannot set another one's").format(self.impl_getname())) self._validate_callback(callback, callback_params) if callback is not None: @@ -415,9 +419,8 @@ class Option(OnlyOption): all_cons_opts = [] val_consistencies = True for opt in opts: - is_multi = opt.impl_is_multi() and not opt.impl_is_master_slaves() - if not is_multi and ((isinstance(opt, DynSymLinkOption) and option._dyn == opt._dyn) or \ - option == opt): + if (isinstance(opt, DynSymLinkOption) and option._dyn == opt._dyn) or \ + option == opt: # option is current option # we have already value, so use it all_cons_vals.append(value) @@ -425,6 +428,7 @@ class Option(OnlyOption): else: #if context, calculate value, otherwise get default value path = None + is_multi = opt.impl_is_multi() and not opt.impl_is_master_slaves() if context is not undefined: if isinstance(opt, DynSymLinkOption): path = opt.impl_getpath(context) @@ -470,7 +474,7 @@ class Option(OnlyOption): def impl_validate(self, value, context=undefined, validate=True, force_index=None, force_submulti_index=None, current_opt=undefined, is_multi=None, - display_warnings=True): + display_warnings=True, multi=None): """ :param value: the option's value :param context: Config's context @@ -489,6 +493,13 @@ class Option(OnlyOption): if current_opt is undefined: current_opt = self + def _is_not_unique(value): + if self.impl_is_unique() and len(set(value)) != len(value): + for idx, val in enumerate(value): + if val in value[idx+1:]: + return ValueError(_('invalid value "{}", this value is already in "{}"').format( + val, self.impl_get_display_name())) + def calculation_validator(val): validator, validator_params = self.impl_get_validator() if validator is not None: @@ -589,43 +600,60 @@ class Option(OnlyOption): if is_multi is None: is_multi = self.impl_is_multi() + if not is_multi: return do_validation(value, None, None) elif force_index is not None: if self.impl_is_submulti() and force_submulti_index is None: + err = _is_not_unique(value) + if err: + return err if not isinstance(value, list): # pragma: optional cover - raise ValueError(_("invalid value {0} for option {1} which" - " must be a list").format( - value, self.impl_getname())) + return ValueError(_('invalid value "{0}" for "{1}" which' + ' must be a list').format( + value, self.impl_get_display_name())) for idx, val in enumerate(value): + if isinstance(val, list): + return ValueError(_('invalid value "{}" for "{}" ' + 'which must not be a list'.format(val, + self.impl_get_display_name()))) err = do_validation(val, force_index, idx) if err: return err else: + if self.impl_is_unique() and value in multi: + return ValueError(_('invalid value "{}", this value is already' + ' in "{}"').format(value, + self.impl_get_display_name())) return do_validation(value, force_index, force_submulti_index) elif not isinstance(value, list): # pragma: optional cover - return ValueError(_("invalid value {0} for option {1} which " - "must be a list").format(value, + return ValueError(_('invalid value "{0}" for "{1}" which ' + 'must be a list').format(value, self.impl_getname())) elif self.impl_is_submulti() and force_submulti_index is None: for idx, val in enumerate(value): + err = _is_not_unique(val) + if err: + return err if not isinstance(val, list): # pragma: optional cover - return ValueError(_("invalid value {0} for option {1} " - "which must be a list of list" - "").format(value, + return ValueError(_('invalid value "{0}" for "{1}" ' + 'which must be a list of list' + '').format(val, self.impl_getname())) for slave_idx, slave_val in enumerate(val): err = do_validation(slave_val, idx, slave_idx) if err: return err else: + err = _is_not_unique(value) + if err: + return err for idx, val in enumerate(value): err = do_validation(val, idx, force_submulti_index) if err: return err - else: - return self._valid_consistency(current_opt, None, context, - None, None) + return self._valid_consistency(current_opt, None, context, + None, None) def impl_is_dynsymlinkoption(self): return False @@ -717,6 +745,10 @@ class Option(OnlyOption): if err: self._del_consistency() raise err + if func in allowed_const_list: + for opt in all_cons_opts: + if getattr(opt, '_unique', undefined) == undefined: + opt._unique = True #consistency could generate warnings or errors self._set_has_dependency() @@ -948,7 +980,7 @@ class SymLinkOption(OnlyOption): session = self.getsession() super(Base, self).__init__(name, undefined, undefined, undefined, undefined, undefined, undefined, undefined, - undefined, opt, session=session) + undefined, undefined, opt=opt, session=session) opt._set_has_dependency() self.commit(session) @@ -1030,13 +1062,14 @@ class DynSymLinkOption(object): def impl_validate(self, value, context=undefined, validate=True, force_index=None, force_submulti_index=None, is_multi=None, - display_warnings=True): + display_warnings=True, multi=None): return self._impl_getopt().impl_validate(value, context, validate, force_index, force_submulti_index, current_opt=self, is_multi=is_multi, - display_warnings=display_warnings) + display_warnings=display_warnings, + multi=multi) def impl_is_dynsymlinkoption(self): return True diff --git a/tiramisu/option/optiondescription.py b/tiramisu/option/optiondescription.py index 56a86e0..e7550d6 100644 --- a/tiramisu/option/optiondescription.py +++ b/tiramisu/option/optiondescription.py @@ -310,7 +310,7 @@ class OptionDescription(BaseOption, StorageOptionDescription): if isinstance(values, Exception): raise values if len(values) > len(set(values)): - raise ConfigError(_('DynOptionDescription callback return not uniq value')) + raise ConfigError(_('DynOptionDescription callback return not unique value')) for val in values: if not isinstance(val, str) or re.match(name_regexp, val) is None: raise ValueError(_("invalid suffix: {0} for option").format(val)) diff --git a/tiramisu/storage/dictionary/option.py b/tiramisu/storage/dictionary/option.py index 2fdf18a..a8b8a62 100644 --- a/tiramisu/storage/dictionary/option.py +++ b/tiramisu/storage/dictionary/option.py @@ -34,10 +34,12 @@ if sys.version_info[0] >= 3: # pragma: optional cover class StorageBase(object): __slots__ = ('_name', '_informations', - '_multi', '_extra', '_warnings_only', '_allow_empty_list', + #multi + '_multi', + '_unique', #value '_default', '_default_multi', @@ -66,7 +68,7 @@ class StorageBase(object): ) def __init__(self, name, multi, warnings_only, doc, extra, calc_properties, - requires, properties, allow_empty_list, opt=undefined, + requires, properties, allow_empty_list, unique, opt=undefined, session=None): _setattr = object.__setattr__ _setattr(self, '_name', name) @@ -89,6 +91,8 @@ class StorageBase(object): _setattr(self, '_opt', opt) if allow_empty_list is not undefined: _setattr(self, '_allow_empty_list', allow_empty_list) + if unique is not undefined: + setattr(self, '_unique', unique) def _set_default_values(self, default, default_multi, is_multi): _setattr = object.__setattr__ @@ -343,6 +347,9 @@ class StorageBase(object): def impl_allow_empty_list(self): return getattr(self, '_allow_empty_list', undefined) + def impl_is_unique(self): + return getattr(self, '_unique', False) + def _get_extra(self, key): extra = self._extra if isinstance(extra, tuple): diff --git a/tiramisu/value.py b/tiramisu/value.py index 1a845ba..49d18de 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -298,11 +298,11 @@ class Values(object): def _get_validated_value(self, opt, path, validate, force_permissive, validate_properties, setting_properties, - self_properties, + self_properties, index=None, submulti_index=undefined, with_meta=True, masterlen=undefined, - check_frozen=False, + check_frozen=False, session=None, display_warnings=True): """same has getitem but don't touch the cache index is None for slave value, if value returned is not a list, just return [] @@ -498,7 +498,7 @@ class Values(object): self_properties=self_properties, session=session) if isinstance(value, Exception): raise value - + owner = self._p_.getowner(path, owners.default, session, only_default=only_default, index=index) if validate_meta is undefined: if opt.impl_is_master_slaves('slave'): @@ -862,14 +862,19 @@ class Multi(list): fake_context = context._gen_fake_values(session) fake_multi = fake_context.cfgimpl_get_values()._get_cached_value( self.opt, path=self.path, validate=False) - fake_multi.extend(iterable, validate=False) - self._validate(iterable, fake_context, index) + if index is None: + fake_multi.extend(iterable, validate=False) + self._validate(fake_multi, fake_context, index) + else: + fake_multi[index].extend(iterable, validate=False) + self._validate(fake_multi[index], fake_context, index) super(Multi, self).extend(iterable) self._store() def _validate(self, value, fake_context, force_index, submulti=False): err = self.opt.impl_validate(value, context=fake_context, - force_index=force_index) + force_index=force_index, + multi=self) if err: raise err @@ -939,7 +944,8 @@ class SubMulti(Multi): else: err = self.opt.impl_validate(value, context=fake_context, force_index=self._index, - force_submulti_index=force_index) + force_submulti_index=force_index, + multi=self) if err: raise err