add unique parameter to option
authorEmmanuel Garette <egarette@cadoles.com>
Sat, 19 Nov 2016 18:16:31 +0000 (19:16 +0100)
committerEmmanuel Garette <egarette@cadoles.com>
Sat, 19 Nov 2016 18:16:31 +0000 (19:16 +0100)
ChangeLog
test/test_multi.py
test/test_submulti.py
tiramisu/option/baseoption.py
tiramisu/option/optiondescription.py
tiramisu/storage/dictionary/option.py
tiramisu/value.py

index 134251e..1be2c27 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,7 @@
 Wed Nov 16 22:30:12 2016 +0200 Emmanuel Garette <egarette@cadoles.com>
-       * 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 <egarette@cadoles.com>
        * consistency is now check "not_equal" if one option has
index 5f51942..2cb541e 100644 (file)
@@ -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])
index d43035e..8793387 100644 (file)
@@ -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])
index fa92fdb..c22cd5b 100644 (file)
@@ -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
index 56a86e0..e7550d6 100644 (file)
@@ -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))
index 2fdf18a..a8b8a62 100644 (file)
@@ -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):
index 1a845ba..49d18de 100644 (file)
@@ -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