add SubMulti
authorEmmanuel Garette <egarette@cadoles.com>
Fri, 25 Apr 2014 20:57:08 +0000 (22:57 +0200)
committerEmmanuel Garette <egarette@cadoles.com>
Sat, 26 Apr 2014 20:37:01 +0000 (22:37 +0200)
13 files changed:
ChangeLog
test/test_dereference.py
test/test_option_calculation.py
test/test_parsing_group.py
test/test_submulti.py [new file with mode: 0644]
tiramisu/option/__init__.py
tiramisu/option/baseoption.py
tiramisu/option/masterslave.py
tiramisu/option/option.py
tiramisu/option/optiondescription.py
tiramisu/setting.py
tiramisu/storage/dictionary/option.py
tiramisu/value.py

index e89d792..4794942 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,8 @@
+XXXXXXXXXXXXX Emmanuel Garette <egarette@cadoles.com>
+
+       * add SubMulti:
+       a SubMulti is a multi in a multi variable
+
 Sat Apr 12 11:37:27 CEST 2014  Emmanuel Garette <egarette@cadoles.com>
 
        * behavior change in master/slave part of code:
index 0e59f36..c39bfcb 100644 (file)
@@ -3,7 +3,7 @@ import autopath
 #from py.test import raises
 
 from tiramisu.config import Config, GroupConfig, MetaConfig
-from tiramisu.option import BoolOption, IntOption, OptionDescription
+from tiramisu.option import BoolOption, IntOption, StrOption, OptionDescription, submulti
 import weakref
 
 
@@ -137,3 +137,21 @@ def test_deref_metaconfig():
     assert w() is not None
     del(meta)
     assert w() is None
+
+
+def test_deref_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    od = OptionDescription('od', '', [multi])
+    cfg = Config(od)
+    cfg.cfgimpl_get_settings().remove('cache')
+    w = weakref.ref(cfg.multi)
+    assert w() is None
+    cfg.multi.append([])
+    w = weakref.ref(cfg.multi)
+    assert w() is None
+    m = cfg.multi
+    w = weakref.ref(m)
+    z = weakref.ref(w()[0])
+    del(m)
+    assert w() is None
+    assert z() is None
index 3199308..881d661 100644 (file)
@@ -438,8 +438,9 @@ def test_callback_multi_callback():
     cfg = Config(maconfig)
     cfg.read_write()
     assert cfg.val1.val1 == ['val']
+    cfg.val1.val1 = ['val1']
     cfg.val1.val1.append()
-    assert cfg.val1.val1 == ['val', 'val']
+    assert cfg.val1.val1 == ['val1', 'val']
 
 
 def test_callback_master_and_slaves_master():
index 46bd82d..3c3175d 100644 (file)
@@ -355,6 +355,7 @@ def test_multi_insert():
     c.var.insert(0, 'nok')
     assert c.var == ['nok', 'ok']
     assert c.getowner(var) != owners.default
+    raises(ValueError, 'c.var.insert(0, 1)')
 
 
 def test_multi_insert_master():
@@ -427,6 +428,7 @@ def test_multi_extend():
     c.var.extend(['pok'])
     assert c.var == ['ok', 'nok', 'pok']
     assert c.getowner(var) != owners.default
+    raises(ValueError, 'c.var.extend([1])')
 
 
 def test_multi_extend_master():
diff --git a/test/test_submulti.py b/test/test_submulti.py
new file mode 100644 (file)
index 0000000..6b5108f
--- /dev/null
@@ -0,0 +1,639 @@
+# coding: utf-8
+import autopath
+from tiramisu.setting import groups, owners
+from tiramisu.config import Config
+from tiramisu.option import StrOption, OptionDescription, submulti
+from tiramisu.value import SubMulti, Multi
+from tiramisu.error import SlaveError
+
+from py.test import raises
+
+
+def return_val():
+    return 'val'
+
+
+def return_list(value=None):
+    return ['val', 'val']
+
+
+def return_list2(value=None):
+    return [['val', 'val']]
+
+
+def test_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    multi2 = StrOption('multi2', '', default_multi='yes', multi=submulti)
+    multi3 = StrOption('multi3', '', default=[['yes']], multi=submulti)
+    od = OptionDescription('od', '', [multi, multi2, multi3])
+    cfg = Config(od)
+    assert cfg.getowner(multi) == owners.default
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    assert cfg.getowner(multi) == owners.default
+    assert cfg.multi3 == [['yes']]
+    assert cfg.multi3[0] == ['yes']
+    assert cfg.multi3[0][0] == 'yes'
+    cfg.multi3[0]
+    assert cfg.getowner(multi) == owners.default
+
+
+def test_append_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    multi2 = StrOption('multi2', '', default_multi='yes', multi=submulti)
+    multi3 = StrOption('multi3', '', default=[['yes']], multi=submulti)
+    od = OptionDescription('od', '', [multi, multi2, multi3])
+    cfg = Config(od)
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    cfg.multi.append()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi == [[]]
+    cfg.multi.append(['no'])
+    assert cfg.multi == [[], ['no']]
+    #
+    assert cfg.multi2 == []
+    assert cfg.getowner(multi2) == owners.default
+    cfg.multi2.append()
+    assert cfg.getowner(multi2) == owner
+    assert cfg.multi2 == [['yes']]
+    cfg.multi2.append(['no'])
+    assert cfg.multi2 == [['yes'], ['no']]
+    #
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3.append()
+    assert cfg.getowner(multi3) == owner
+    assert cfg.multi3 == [['yes'], []]
+    cfg.multi3.append(['no'])
+    assert cfg.multi3 == [['yes'], [], ['no']]
+
+
+def test_append_unvalide_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    multi2 = StrOption('multi2', '', default_multi='yes', multi=submulti)
+    multi3 = StrOption('multi3', '', default=[['yes']], multi=submulti)
+    od = OptionDescription('od', '', [multi, multi2, multi3])
+    cfg = Config(od)
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    raises(ValueError, "cfg.multi.append(1)")
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    #
+    assert cfg.multi2 == []
+    raises(ValueError, "cfg.multi2.append('no')")
+    assert cfg.getowner(multi) == owners.default
+    assert cfg.multi2 == []
+    #
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+    raises(ValueError, "cfg.multi3[0].append(1)")
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+    raises(ValueError, "cfg.multi3[0].append([])")
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+
+
+def test_pop_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    multi2 = StrOption('multi2', '', default_multi='yes', multi=submulti)
+    multi3 = StrOption('multi3', '', default=[['yes']], multi=submulti)
+    od = OptionDescription('od', '', [multi, multi2, multi3])
+    cfg = Config(od)
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert cfg.multi == []
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi = [['no', 'yes'], ['peharps']]
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi == [['no', 'yes'], ['peharps']]
+    cfg.multi[0].pop(1)
+    assert cfg.multi == [['no'], ['peharps']]
+    cfg.multi[0].pop(0)
+    assert cfg.multi == [[], ['peharps']]
+    cfg.multi.pop(1)
+    assert cfg.multi == [[]]
+    cfg.multi.pop(0)
+    assert cfg.multi == []
+    #
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3.pop(0)
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi3 == []
+    del(cfg.multi3)
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3[0].pop(0)
+    assert cfg.getowner(multi3) == owner
+    assert cfg.multi3 == [[]]
+
+
+def test_sort_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    multi2 = StrOption('multi2', '', default_multi='yes', multi=submulti)
+    multi3 = StrOption('multi3', '', default=[['yes']], multi=submulti)
+    od = OptionDescription('od', '', [multi, multi2, multi3])
+    cfg = Config(od)
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    cfg.multi.sort()
+    assert cfg.getowner(multi) == owner
+    cfg.multi = [['no', 'yes'], ['peharps']]
+    cfg.multi.sort()
+    assert cfg.multi == [['no', 'yes'], ['peharps']]
+    cfg.multi.sort(reverse=True)
+    assert cfg.multi == [['peharps'], ['no', 'yes']]
+    cfg.multi[1].sort(reverse=True)
+    assert cfg.multi == [['peharps'], ['yes', 'no']]
+    cfg.multi[1].sort()
+    assert cfg.multi == [['peharps'], ['no', 'yes']]
+    #
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3.sort()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi3 == [['yes']]
+    del(cfg.multi3)
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3[0].sort()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi3 == [['yes']]
+
+
+def test_reverse_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    multi2 = StrOption('multi2', '', default_multi='yes', multi=submulti)
+    multi3 = StrOption('multi3', '', default=[['yes']], multi=submulti)
+    od = OptionDescription('od', '', [multi, multi2, multi3])
+    cfg = Config(od)
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    cfg.multi.reverse()
+    assert cfg.getowner(multi) == owner
+    cfg.multi = [['no', 'yes'], ['peharps']]
+    cfg.multi.reverse()
+    assert cfg.multi == [['peharps'], ['no', 'yes']]
+    cfg.multi[1].reverse()
+    assert cfg.multi == [['peharps'], ['yes', 'no']]
+    #
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3.reverse()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi3 == [['yes']]
+    del(cfg.multi3)
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3[0].reverse()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi3 == [['yes']]
+
+
+def test_insert_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    multi2 = StrOption('multi2', '', default_multi='yes', multi=submulti)
+    multi3 = StrOption('multi3', '', default=[['yes']], multi=submulti)
+    od = OptionDescription('od', '', [multi, multi2, multi3])
+    cfg = Config(od)
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    cfg.multi.insert(0, ['no'])
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi == [['no']]
+    assert isinstance(cfg.multi, Multi)
+    assert isinstance(cfg.multi[0], SubMulti)
+    #
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3.insert(1, [])
+    assert cfg.getowner(multi3) == owner
+    assert cfg.multi3 == [['yes'], []]
+    cfg.multi3.insert(0, ['no'])
+    assert cfg.multi3 == [['no'], ['yes'], []]
+    del(cfg.multi3)
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3[0].insert(0, 'no')
+    assert cfg.getowner(multi3) == owner
+    assert cfg.multi3 == [['no', 'yes']]
+
+
+def test_insert_unvalide_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    multi2 = StrOption('multi2', '', default_multi='yes', multi=submulti)
+    multi3 = StrOption('multi3', '', default=[['yes']], multi=submulti)
+    od = OptionDescription('od', '', [multi, multi2, multi3])
+    cfg = Config(od)
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    raises(ValueError, "cfg.multi.insert(0, 1)")
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    #
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+    raises(ValueError, "cfg.multi3[0].insert(0, 1)")
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+
+
+def test_extend_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    multi2 = StrOption('multi2', '', default_multi='yes', multi=submulti)
+    multi3 = StrOption('multi3', '', default=[['yes']], multi=submulti)
+    od = OptionDescription('od', '', [multi, multi2, multi3])
+    cfg = Config(od)
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    cfg.multi.extend([['no']])
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi == [['no']]
+    assert isinstance(cfg.multi, Multi)
+    assert isinstance(cfg.multi[0], SubMulti)
+    #
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3.extend([[]])
+    assert cfg.getowner(multi3) == owner
+    assert cfg.multi3 == [['yes'], []]
+    cfg.multi3.extend([['no']])
+    assert cfg.multi3 == [['yes'], [], ['no']]
+    del(cfg.multi3)
+    assert cfg.getowner(multi3) == owners.default
+    cfg.multi3[0].extend(['no'])
+    assert cfg.getowner(multi3) == owner
+    assert cfg.multi3 == [['yes', 'no']]
+
+
+def test_extend_unvalide_submulti():
+    multi = StrOption('multi', '', multi=submulti)
+    multi2 = StrOption('multi2', '', default_multi='yes', multi=submulti)
+    multi3 = StrOption('multi3', '', default=[['yes']], multi=submulti)
+    od = OptionDescription('od', '', [multi, multi2, multi3])
+    cfg = Config(od)
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    raises(ValueError, "cfg.multi.extend([[1]])")
+    assert cfg.multi == []
+    assert cfg.getowner(multi) == owners.default
+    #
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+    raises(ValueError, "cfg.multi3[0].extend([1])")
+    assert cfg.multi3 == [['yes']]
+    assert cfg.getowner(multi3) == owners.default
+
+
+def test_callback_submulti_str():
+    multi = StrOption('multi', '', multi=submulti, callback=return_val)
+    od = OptionDescription('od', '', [multi])
+    cfg = Config(od)
+    cfg.read_write()
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert cfg.getowner(multi) == owners.default
+    assert cfg.multi == [['val']]
+    cfg.multi.append()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi == [['val'], ['val']]
+    del(cfg.multi)
+    assert cfg.getowner(multi) == owners.default
+    cfg.multi[0].append()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi == [['val', 'val']]
+
+
+def test_callback_submulti_list():
+    multi = StrOption('multi', '', multi=submulti, callback=return_list)
+    od = OptionDescription('od', '', [multi])
+    cfg = Config(od)
+    cfg.read_write()
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert cfg.multi == [['val', 'val']]
+    assert cfg.getowner(multi) == owners.default
+    cfg.multi.append()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi == [['val', 'val'], ['val', 'val']]
+    del(cfg.multi)
+    assert cfg.getowner(multi) == owners.default
+    cfg.multi[0].append()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi == [['val', 'val', None]]
+
+
+def test_callback_submulti_list_list():
+    multi = StrOption('multi', '', multi=submulti, callback=return_list2)
+    od = OptionDescription('od', '', [multi])
+    cfg = Config(od)
+    cfg.read_write()
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert cfg.multi == [['val', 'val']]
+    assert cfg.getowner(multi) == owners.default
+    cfg.multi.append()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi == [['val', 'val'], []]
+    del(cfg.multi)
+    assert cfg.getowner(multi) == owners.default
+    cfg.multi[0].append()
+    assert cfg.getowner(multi) == owner
+    assert cfg.multi == [['val', 'val', None]]
+
+
+#FIXME multi sur une master
+
+
+def test_groups_with_master_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    assert interface1.impl_get_group_type() == groups.master
+
+
+def test_groups_with_master_in_config_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    Config(interface1)
+    assert interface1.impl_get_group_type() == groups.master
+
+
+def test_values_with_master_and_slaves_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert interface1.impl_get_group_type() == groups.master
+    assert cfg.getowner(ip_admin_eth0) == owners.default
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == []
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    assert cfg.ip_admin_eth0.ip_admin_eth0 == ["192.168.230.145"]
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == [[]]
+    assert cfg.getowner(ip_admin_eth0) == owner
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+    cfg.ip_admin_eth0.ip_admin_eth0 = ["192.168.230.145", "192.168.230.147"]
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == [[], []]
+    raises(SlaveError, 'cfg.ip_admin_eth0.netmask_admin_eth0.append(None)')
+    raises(SlaveError, 'cfg.ip_admin_eth0.netmask_admin_eth0.pop(0)')
+    cfg.ip_admin_eth0.netmask_admin_eth0[0].append('255.255.255.0')
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == [['255.255.255.0'], []]
+    cfg.ip_admin_eth0.netmask_admin_eth0[0].pop(0)
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == [[], []]
+    raises(ValueError, 'cfg.ip_admin_eth0.netmask_admin_eth0 = ["255.255.255.0", "255.255.255.0"]')
+
+
+def test_reset_values_with_master_and_slaves_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert interface1.impl_get_group_type() == groups.master
+    assert cfg.getowner(ip_admin_eth0) == owners.default
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    assert cfg.getowner(ip_admin_eth0) == owner
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+    del(cfg.ip_admin_eth0.ip_admin_eth0)
+    assert cfg.getowner(ip_admin_eth0) == owners.default
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+    assert cfg.ip_admin_eth0.ip_admin_eth0 == []
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == []
+    #
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    cfg.ip_admin_eth0.netmask_admin_eth0[0].append('255.255.255.0')
+    assert cfg.getowner(ip_admin_eth0) == owner
+    assert cfg.getowner(netmask_admin_eth0) == owner
+    del(cfg.ip_admin_eth0.ip_admin_eth0)
+    assert cfg.getowner(ip_admin_eth0) == owners.default
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+    assert cfg.ip_admin_eth0.ip_admin_eth0 == []
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == []
+
+
+def test_values_with_master_and_slaves_slave_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == []
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0']]")
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0']]
+    cfg.ip_admin_eth0.netmask_admin_eth0[0] = ['255.255.255.0']
+    cfg.ip_admin_eth0.netmask_admin_eth0[0][0] = '255.255.255.0'
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0'], ['255.255.255.0']]")
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = []")
+    del(cfg.ip_admin_eth0.netmask_admin_eth0)
+    cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0']]
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == [['255.255.255.0'], []]
+    cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0'], ['255.255.255.0']]
+    raises(SlaveError, 'cfg.ip_admin_eth0.netmask_admin_eth0.pop(1)')
+
+
+def test_values_with_master_and_slaves_master_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    cfg.ip_admin_eth0.ip_admin_eth0 = ["192.168.230.145"]
+    cfg.ip_admin_eth0.ip_admin_eth0 = ["192.168.230.145", "192.168.230.145"]
+    cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0'], ['255.255.255.0']]
+    raises(SlaveError, 'cfg.ip_admin_eth0.ip_admin_eth0 = ["192.168.230.145"]')
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == [['255.255.255.0'], ['255.255.255.0']]
+    cfg.ip_admin_eth0.ip_admin_eth0.pop(1)
+    assert cfg.ip_admin_eth0.ip_admin_eth0 == ["192.168.230.145"]
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == [['255.255.255.0']]
+    del(cfg.ip_admin_eth0.ip_admin_eth0)
+    assert cfg.ip_admin_eth0.ip_admin_eth0 == []
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == []
+
+
+def test_values_with_master_and_slaves_master_error_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    cfg.ip_admin_eth0.ip_admin_eth0 = ["192.168.230.145", "192.168.230.145"]
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0']]")
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0'], ['255.255.255.0'], ['255.255.255.0']]")
+    cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0'], ['255.255.255.0']]
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0']]")
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0 = [['255.255.255.0'], ['255.255.255.0'], ['255.255.255.0']]")
+
+
+def test_values_with_master_owner_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert cfg.getowner(ip_admin_eth0) == owners.default
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    assert cfg.getowner(ip_admin_eth0) == owner
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+    cfg.ip_admin_eth0.ip_admin_eth0.pop(0)
+    assert cfg.getowner(ip_admin_eth0) == owner
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+
+
+def test_values_with_master_disabled_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    cfg.ip_admin_eth0.ip_admin_eth0.pop(0)
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    cfg.ip_admin_eth0.netmask_admin_eth0 = [["192.168.230.145"]]
+    cfg.ip_admin_eth0.ip_admin_eth0.pop(0)
+    del(cfg.ip_admin_eth0.netmask_admin_eth0)
+    cfg.cfgimpl_get_settings()[netmask_admin_eth0].append('disabled')
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    cfg.ip_admin_eth0.ip_admin_eth0.pop(0)
+
+    #delete with value in disabled var
+    cfg.cfgimpl_get_settings()[netmask_admin_eth0].remove('disabled')
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    cfg.ip_admin_eth0.netmask_admin_eth0 = [["192.168.230.145"]]
+    cfg.cfgimpl_get_settings()[netmask_admin_eth0].append('disabled')
+    cfg.ip_admin_eth0.ip_admin_eth0.pop(0)
+
+    #append with value in disabled var
+    cfg.cfgimpl_get_settings()[netmask_admin_eth0].remove('disabled')
+    cfg.ip_admin_eth0.ip_admin_eth0.append("192.168.230.145")
+    cfg.ip_admin_eth0.netmask_admin_eth0 = [["192.168.230.145"]]
+    cfg.cfgimpl_get_settings()[netmask_admin_eth0].append('disabled')
+    cfg.ip_admin_eth0.ip_admin_eth0.append('192.168.230.43')
+
+
+def test_multi_insert_master_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    raises(SlaveError, "cfg.ip_admin_eth0.ip_admin_eth0.insert(0, 'nok')")
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0.insert(0, 'nok')")
+    cfg.ip_admin_eth0.ip_admin_eth0.append('192.168.1.1')
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0[0].insert(0, 'nok')")
+
+
+def test_multi_sort_master_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    raises(SlaveError, "cfg.ip_admin_eth0.ip_admin_eth0.sort()")
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0.sort()")
+    cfg.ip_admin_eth0.ip_admin_eth0.append('192.168.1.1')
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0[0].sort()")
+
+
+def test_multi_reverse_master_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    raises(SlaveError, "cfg.ip_admin_eth0.ip_admin_eth0.reverse()")
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0.reverse()")
+    cfg.ip_admin_eth0.ip_admin_eth0.append('192.168.1.1')
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0[0].reverse()")
+
+
+def test_multi_extend_master_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    raises(SlaveError, "cfg.ip_admin_eth0.ip_admin_eth0.extend(['ok'])")
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0.extend(['ok'])")
+    cfg.ip_admin_eth0.ip_admin_eth0.append('192.168.1.1')
+    raises(SlaveError, "cfg.ip_admin_eth0.netmask_admin_eth0[0].extend(['ok'])")
+
+
+def test_slave_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=submulti)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    assert cfg.ip_admin_eth0.ip_admin_eth0.__class__.__name__ == 'Multi'
+    assert cfg.ip_admin_eth0.netmask_admin_eth0.__class__.__name__ == 'Multi'
+    cfg.ip_admin_eth0.ip_admin_eth0.append('192.168.1.1')
+    assert cfg.ip_admin_eth0.ip_admin_eth0.__class__.__name__ == 'Multi'
+    assert cfg.ip_admin_eth0.netmask_admin_eth0.__class__.__name__ == 'Multi'
+    assert cfg.ip_admin_eth0.ip_admin_eth0[0].__class__.__name__ == 'str'
+    assert cfg.ip_admin_eth0.netmask_admin_eth0[0].__class__.__name__ == 'SubMulti'
+
+
+def test__master_is_submulti():
+    ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=submulti)
+    netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=True)
+    interface1 = OptionDescription('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
+    interface1.impl_set_group_type(groups.master)
+    maconfig = OptionDescription('toto', '', [interface1])
+    cfg = Config(maconfig)
+    cfg.read_write()
+    owner = cfg.cfgimpl_get_settings().getowner()
+    assert interface1.impl_get_group_type() == groups.master
+    assert cfg.getowner(ip_admin_eth0) == owners.default
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == []
+    cfg.ip_admin_eth0.ip_admin_eth0.append(["192.168.230.145"])
+    assert cfg.ip_admin_eth0.ip_admin_eth0 == [["192.168.230.145"]]
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == [None]
+    assert cfg.getowner(ip_admin_eth0) == owner
+    assert cfg.getowner(netmask_admin_eth0) == owners.default
+    cfg.ip_admin_eth0.ip_admin_eth0 = [["192.168.230.145"], ["192.168.230.147"]]
+    assert cfg.ip_admin_eth0.netmask_admin_eth0 == [None, None]
+    raises(SlaveError, 'cfg.ip_admin_eth0.netmask_admin_eth0.append(None)')
+    raises(SlaveError, 'cfg.ip_admin_eth0.netmask_admin_eth0.pop(0)')
+    cfg.ip_admin_eth0.ip_admin_eth0[0].append('192.168.1.1')
+    assert cfg.ip_admin_eth0.ip_admin_eth0 == [["192.168.230.145", '192.168.1.1'], ["192.168.230.147"]]
+    cfg.ip_admin_eth0.ip_admin_eth0[0].pop(0)
+    assert cfg.ip_admin_eth0.ip_admin_eth0 == [["192.168.1.1"], ["192.168.230.147"]]
+    raises(ValueError, 'cfg.ip_admin_eth0.ip_admin_eth0 = ["192.168.1.1", "192.168.1.1"]')
index 3be1c50..c17b243 100644 (file)
@@ -1,6 +1,6 @@
 from .masterslave import MasterSlaves
 from .optiondescription import OptionDescription
-from .baseoption import Option, SymLinkOption
+from .baseoption import Option, SymLinkOption, submulti
 from .option import (ChoiceOption, BoolOption, IntOption, FloatOption,
                      StrOption, UnicodeOption, IPOption, PortOption,
                      NetworkOption, NetmaskOption, BroadcastOption,
@@ -13,4 +13,4 @@ __all__ = ('MasterSlaves', 'OptionDescription', 'Option', 'SymLinkOption',
            'StrOption', 'UnicodeOption', 'IPOption', 'PortOption',
            'NetworkOption', 'NetmaskOption', 'BroadcastOption',
            'DomainnameOption', 'EmailOption', 'URLOption', 'UsernameOption',
-           'FilenameOption')
+           'FilenameOption', 'submulti')
index b3720eb..2ad51dc 100644 (file)
@@ -32,6 +32,14 @@ from tiramisu.storage import get_storages_option
 
 StorageBase = get_storages_option('base')
 
+
+class SubMulti(object):
+    pass
+
+
+submulti = SubMulti()
+
+
 name_regexp = re.compile(r'^\d+')
 forbidden_names = ('iter_all', 'iter_group', 'find', 'find_first',
                    'make_dict', 'unwrap_from_path', 'read_only',
@@ -123,7 +131,7 @@ class Base(StorageBase):
                                    "for option {1}: {2}").format(
                                        str(default_multi), name, err))
         self._multi = multi
-        if self._multi:
+        if self._multi is not False:
             if default is None:
                 default = []
             self._default_multi = default_multi
@@ -414,7 +422,7 @@ class Option(OnlyOption):
         return self._requires
 
     def _launch_consistency(self, func, option, value, context, index,
-                            all_cons_opts, warnings_only):
+                            submulti_index, all_cons_opts, warnings_only):
         """Launch consistency now
 
         :param func: function name, this name should start with _cons_
@@ -452,17 +460,24 @@ class Option(OnlyOption):
             #append value
             if not self.impl_is_multi() or option == opt:
                 all_cons_vals.append(opt_value)
+            elif self.impl_is_submulti():
+                try:
+                    all_cons_vals.append(opt_value[index][submulti_index])
+                except IndexError:
+                    #value is not already set, could be higher index
+                    #so return if no value
+                    return
             else:
-                #value is not already set, could be higher index
                 try:
                     all_cons_vals.append(opt_value[index])
                 except IndexError:
+                    #value is not already set, could be higher index
                     #so return if no value
                     return
         getattr(self, func)(all_cons_opts, all_cons_vals, warnings_only)
 
     def impl_validate(self, value, context=None, validate=True,
-                      force_index=None):
+                      force_index=None, force_submulti_index=None):
         """
         :param value: the option's value
         :param context: Config's context
@@ -470,8 +485,11 @@ class Option(OnlyOption):
         :param validate: if true enables ``self._validator`` validation
         :type validate: boolean
         :param force_index: if multi, value has to be a list
-                               not if force_index is not None
+                            not if force_index is not None
         :type force_index: integer
+        :param force_submulti_index: if submulti, value has to be a list
+                                     not if force_submulti_index is not None
+        :type force_submulti_index: integer
         """
         if not validate:
             return
@@ -496,15 +514,17 @@ class Option(OnlyOption):
                                       callback=self._validator,
                                       callback_params=validator_params)
 
-        def do_validation(_value, _index=None):
+        def do_validation(_value, _index, submulti_index):
             if _value is None:
                 return
             # option validation
             try:
                 self._validate(_value)
             except ValueError as err:
-                log.debug('do_validation: value: {0} index: {1}'.format(
-                    _value, _index), exc_info=True)
+                log.debug('do_validation: value: {0}, index: {1}, '
+                          'submulti_index: {2}'.format(_value, _index,
+                                                       submulti_index),
+                          exc_info=True)
                 raise ValueError(_('invalid value for option {0}: {1}'
                                    '').format(self.impl_getname(), err))
             error = None
@@ -514,7 +534,8 @@ class Option(OnlyOption):
                 val_validator(_value)
                 # if not context launch consistency validation
                 if context is not None:
-                    descr._valid_consistency(self, _value, context, _index)
+                    descr._valid_consistency(self, _value, context, _index,
+                                             submulti_index)
                 self._second_level_validation(_value, self._warnings_only)
             except ValueError as error:
                 log.debug(_('do_validation for {0}: error in value').format(
@@ -530,7 +551,8 @@ class Option(OnlyOption):
                 try:
                     # if context launch consistency validation
                     if context is not None:
-                        descr._valid_consistency(self, _value, context, _index)
+                        descr._valid_consistency(self, _value, context, _index,
+                                                 submulti_index)
                 except ValueError as error:
                     log.debug(_('do_validation for {0}: error in consistency').format(
                         self.impl_getname()), exc_info=True)
@@ -553,13 +575,34 @@ class Option(OnlyOption):
         if context is not None:
             descr = context.cfgimpl_get_description()
 
-        if not self._multi or force_index is not None:
-            do_validation(value, force_index)
+        if not self.impl_is_multi():
+            do_validation(value, None, None)
+        elif force_index is not None:
+            if self.impl_is_submulti() and force_submulti_index is None:
+                if not isinstance(value, list):
+                    raise ValueError(_("invalid value {0} for option {1} which"
+                                       " must be a list").format(
+                                           value, self.impl_getname()))
+                for idx, val in enumerate(value):
+                    do_validation(val, force_index, idx)
+            else:
+                do_validation(value, force_index, force_submulti_index)
         else:
             if not isinstance(value, list):
-                raise ValueError(_("invalid value {0} for option {1} which must be a list").format(value, self.impl_getname()))
-            for index, val in enumerate(value):
-                do_validation(val, index)
+                raise ValueError(_("invalid value {0} for option {1} which "
+                                   "must be a list").format(value,
+                                                            self.impl_getname()))
+            for idx, val in enumerate(value):
+                if self.impl_is_submulti() and force_submulti_index is None:
+                    if not isinstance(val, list):
+                        raise ValueError(_("invalid value {0} for option {1} "
+                                           "which must be a list of list"
+                                           "").format(value,
+                                                      self.impl_getname()))
+                    for slave_idx, slave_val in enumerate(val):
+                        do_validation(slave_val, idx, slave_idx)
+                else:
+                    do_validation(val, idx, force_submulti_index)
 
     def impl_getdefault(self):
         "accessing the default value"
@@ -615,7 +658,10 @@ class Option(OnlyOption):
     #    return value
 
     def impl_is_multi(self):
-        return self._multi
+        return self._multi is True or self._multi is submulti
+
+    def impl_is_submulti(self):
+        return self._multi is submulti
 
     def impl_add_consistency(self, func, *other_opts, **params):
         """Add consistency means that value will be validate with other_opts
@@ -647,10 +693,18 @@ class Option(OnlyOption):
         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, warnings_only)
+                    if not self.impl_is_submulti():
+                        self._launch_consistency(func, self, val, None, idx,
+                                                 None, all_cons_opts,
+                                                 warnings_only)
+                    else:
+                        for slave_idx, val in enumerate(value):
+                            self._launch_consistency(func, self, val, None,
+                                                     idx, slave_idx,
+                                                     all_cons_opts,
+                                                     warnings_only)
             else:
-                self._launch_consistency(func, self, value, None,
+                self._launch_consistency(func, self, value, None, None,
                                          None, all_cons_opts, warnings_only)
         self._add_consistency(func, all_cons_opts, params)
         self.impl_validate(self.impl_getdefault())
@@ -808,7 +862,8 @@ def validate_requires_arg(requires, name):
                                'must be an option in option {0}').format(name))
         if option.impl_is_multi():
             raise ValueError(_('malformed requirements option {0} '
-                               'must not be a multi').format(name))
+                               'must not be a multi for {1}').format(
+                                   option.impl_getname(), name))
         if expected is not None:
             try:
                 option._validate(expected)
index 7a9057f..b962a98 100644 (file)
@@ -20,7 +20,7 @@
 # the whole pypy projet is under MIT licence
 # ____________________________________________________________
 from tiramisu.i18n import _
-from tiramisu.setting import log
+from tiramisu.setting import log, undefined
 from tiramisu.error import SlaveError, ConfigError
 from .baseoption import SymLinkOption, Option
 
@@ -87,18 +87,21 @@ class MasterSlaves(object):
         pass
 
     def getitem(self, values, opt, path, validate, force_permissive,
-                force_properties, validate_properties):
+                force_properties, validate_properties, slave_path=undefined,
+                slave_value=undefined):
         if opt == self.master:
             return self._getmaster(values, opt, path, validate,
                                    force_permissive, force_properties,
-                                   validate_properties)
+                                   validate_properties, slave_path,
+                                   slave_value)
         else:
             return self._getslave(values, opt, path, validate,
                                   force_permissive, force_properties,
                                   validate_properties)
 
     def _getmaster(self, values, opt, path, validate, force_permissive,
-                   force_properties, validate_properties):
+                   force_properties, validate_properties, c_slave_path,
+                   c_slave_value):
         value = values._get_validated_value(opt, path, validate,
                                             force_permissive,
                                             force_properties,
@@ -108,12 +111,15 @@ class MasterSlaves(object):
             for slave in self.slaves:
                 try:
                     slave_path = values._get_opt_path(slave)
-                    slave_value = values._get_validated_value(slave,
-                                                              slave_path,
-                                                              False,
-                                                              False,
-                                                              None, False,
-                                                              None)  # not undefined
+                    if c_slave_path == slave_path:
+                        slave_value = c_slave_value
+                    else:
+                        slave_value = values._get_validated_value(slave,
+                                                                  slave_path,
+                                                                  False,
+                                                                  False,
+                                                                  None, False,
+                                                                  None)  # not undefined
                     slavelen = len(slave_value)
                     self.validate_slave_length(masterlen, slavelen, slave._name)
                 except ConfigError:
@@ -146,10 +152,11 @@ class MasterSlaves(object):
             self.validate_slave_length(self.get_length(values), len(value),
                                        opt._name, setitem=True)
 
-    def get_length(self, values, validate=True):
+    def get_length(self, values, validate=True, slave_path=undefined,
+                   slave_value=undefined):
         masterp = values._get_opt_path(self.master)
         return len(self.getitem(values, self.master, masterp, validate, False,
-                                None, True))
+                                None, True, slave_path, slave_value))
 
     def validate_slave_length(self, masterlen, valuelen, name, setitem=False):
         if valuelen > masterlen or (valuelen < masterlen and setitem):
@@ -183,11 +190,11 @@ class MasterSlaves(object):
                 list is greater than master: raise SlaveError
         """
         #if slave, had values until master's one
-        masterlen = self.get_length(values, validate)
+        path = values._get_opt_path(opt)
+        masterlen = self.get_length(values, validate, path, value)
         valuelen = len(value)
         if validate:
             self.validate_slave_length(masterlen, valuelen, opt._name)
-        path = values._get_opt_path(opt)
         if valuelen < masterlen:
             for num in range(0, masterlen - valuelen):
                 index = valuelen + num
index d707940..1b39012 100644 (file)
@@ -134,7 +134,8 @@ class IPOption(Option):
                  callback_params=None, validator=None, validator_params=None,
                  properties=None, private_only=False, allow_reserved=False,
                  warnings_only=False):
-        self._extra = {'_private_only': private_only, '_allow_reserved': allow_reserved}
+        self._extra = {'_private_only': private_only,
+                       '_allow_reserved': allow_reserved}
         super(IPOption, self).__init__(name, doc, default=default,
                                        default_multi=default_multi,
                                        callback=callback,
index 7a1a7c1..29c1e43 100644 (file)
@@ -21,7 +21,7 @@
 from copy import copy
 
 from tiramisu.i18n import _
-from tiramisu.setting import groups, log
+from tiramisu.setting import groups  # , log
 from .baseoption import BaseOption
 from . import MasterSlaves
 from tiramisu.error import ConfigError, ConflictError, ValueWarning
@@ -162,7 +162,7 @@ class OptionDescription(BaseOption, StorageOptionDescription):
     def impl_get_group_type(self):
         return self._group_type
 
-    def _valid_consistency(self, option, value, context, index):
+    def _valid_consistency(self, option, value, context, index, submulti_idx):
         if self._cache_consistencies is None:
             return True
         #consistencies is something like [('_cons_not_equal', (opt1, opt2))]
@@ -175,6 +175,7 @@ class OptionDescription(BaseOption, StorageOptionDescription):
                     all_cons_opts[0]._launch_consistency(func, option,
                                                          value,
                                                          context, index,
+                                                         submulti_idx,
                                                          all_cons_opts,
                                                          warnings_only)
                 except ValueError as err:
index 887c16c..fa29b07 100644 (file)
@@ -223,7 +223,7 @@ populate_owners()
 
 
 # ____________________________________________________________
-class Undefined():
+class Undefined(object):
     pass
 
 
index f86d970..f7a7f92 100644 (file)
@@ -28,8 +28,8 @@ class Base(object):
     __slots__ = ('_name', '_requires', '_properties', '_readonly',
                  '_calc_properties', '_informations',
                  '_state_readonly', '_state_requires', '_stated',
-                 '_multi', '_validator', '_validator_params', '_default',
-                 '_default_multi', '_state_callback', '_callback',
+                 '_multi', '_validator', '_validator_params',
+                 '_default', '_default_multi', '_state_callback', '_callback',
                  '_state_callback_params', '_callback_params', '_multitype',
                  '_consistencies', '_warnings_only', '_master_slaves',
                  '_state_consistencies', '_extra', '__weakref__')
index 757bcdb..b208e9e 100644 (file)
@@ -86,7 +86,13 @@ class Values(object):
                                           index=index)
             try:
                 if isinstance(value, list) and index is not undefined:
-                    return value[index]
+                    #if submulti and return a list of list, just return list
+                    if opt.impl_is_submulti():
+                        val = value[index]
+                        if isinstance(val, list):
+                            value = val
+                    else:
+                        value = value[index]
                 return value
             except IndexError:
                 pass
@@ -219,9 +225,10 @@ class Values(object):
 
     def _get_validated_value(self, opt, path, validate, force_permissive,
                              force_properties, validate_properties,
-                             index=undefined):
-        """same has getitem but don't touch the cache"""
-        #FIXME expliquer la différence entre index == undefined et index == None
+                             index=undefined, submulti_index=undefined):
+        """same has getitem but don't touch the cache
+        index is None for slave value, if value returned is not a list, just return []
+        """
         context = self._getcontext()
         setting = context.cfgimpl_get_settings()
         is_default = self._is_default_owner(opt, path,
@@ -254,13 +261,24 @@ class Values(object):
             else:
                 force_index = index
             if opt.impl_is_multi():
+                #for slave is a multi
                 if index is None and not isinstance(value, list):
                     value = []
                 if force_index is None:
                     value = Multi(value, self.context, opt, path)
+                elif opt.impl_is_submulti() and submulti_index is undefined:
+                    value = SubMulti(value, self.context, opt, path,
+                                     force_index)
+
             if validate:
+                if submulti_index is undefined:
+                    force_submulti_index = None
+                else:
+                    force_submulti_index = submulti_index
+
                 opt.impl_validate(value, context, 'validator' in setting,
-                                  force_index=force_index)
+                                  force_index=force_index,
+                                  force_submulti_index=force_submulti_index)
             #FIXME pas de test avec les metas ...
             #FIXME et les symlinkoption ...
             if is_default and 'force_store_value' in setting[opt]:
@@ -291,7 +309,7 @@ class Values(object):
         opt.impl_validate(value, context,
                           'validator' in context.cfgimpl_get_settings())
         if opt.impl_is_multi():
-            value = Multi(value, self.context, opt, path)
+            #value = Multi(value, self.context, opt, path)
             if opt.impl_is_master_slaves():
                 opt.impl_get_master_slaves().setitem(self, opt, value, path)
         self._setvalue(opt, path, value, force_permissive=force_permissive,
@@ -309,6 +327,10 @@ class Values(object):
         owner = context.cfgimpl_get_settings().getowner()
         if isinstance(value, Multi):
             value = list(value)
+            if opt.impl_is_submulti():
+                for idx, val in enumerate(value):
+                    if isinstance(val, SubMulti):
+                        value[idx] = list(val)
         self._p_.setvalue(path, value, owner)
 
     def getowner(self, opt, force_permissive=False):
@@ -471,28 +493,47 @@ class Values(object):
 # ____________________________________________________________
 # multi types
 
-
 class Multi(list):
     """multi options values container
     that support item notation for the values of multi options"""
-    __slots__ = ('opt', 'path', 'context')
+    __slots__ = ('opt', 'path', 'context', '__weakref__')
 
     def __init__(self, value, context, opt, path):
         """
         :param value: the Multi wraps a list value
         :param context: the home config that has the values
         :param opt: the option object that have this Multi value
+        :param path: path of the option
         """
-        if isinstance(value, Multi):
-            raise ValueError(_('{0} is already a Multi ').format(opt.impl_getname()))
+        if value is None:
+            value = []
+        if not opt.impl_is_submulti() and isinstance(value, Multi):
+            raise ValueError(_('{0} is already a Multi ').format(
+                opt.impl_getname()))
         self.opt = opt
         self.path = path
         if not isinstance(context, weakref.ReferenceType):
             raise ValueError('context must be a Weakref')
         self.context = context
         if not isinstance(value, list):
+            if not '_index' in self.__slots__ and opt.impl_is_submulti():
+                value = [[value]]
+            else:
+                value = [value]
+        elif value != [] and not '_index' in self.__slots__ and \
+                opt.impl_is_submulti() and not isinstance(value[0], list):
             value = [value]
         super(Multi, self).__init__(value)
+        if opt.impl_is_submulti():
+            if not '_index' in self.__slots__:
+                for idx, val in enumerate(self):
+                    if not isinstance(val, SubMulti):
+                        super(Multi, self).__setitem__(idx, SubMulti(val,
+                                                                     context,
+                                                                     opt, path,
+                                                                     idx))
+                    #FIXME weakref ??
+                    self[idx].submulti = weakref.ref(self)
 
     def _getcontext(self):
         """context could be None, we need to test it
@@ -506,24 +547,32 @@ class Multi(list):
         return context
 
     def __setitem__(self, index, value):
-        self._validate(value, index)
+        self._setitem(index, value)
+
+    def _setitem(self, index, value):
+        self._validate(value, index, True)
         #assume not checking mandatory property
         super(Multi, self).__setitem__(index, value)
         self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path,
                                                           self)
 
-    def __repr__(self, *args, **kwargs):
-        print args, kwargs
-        return super(Multi, self).__repr__(*args, **kwargs)
+    #def __repr__(self, *args, **kwargs):
+    #    print args, kwargs
+    #    return super(Multi, self).__repr__(*args, **kwargs)
 
-    def __getitem__(self, y):
-        return super(Multi, self).__getitem__(y)
+    #def __getitem__(self, y):
+    #    return super(Multi, self).__getitem__(y)
+
+    def _get_validated_value(self, index):
+        values = self._getcontext().cfgimpl_get_values()
+        return values._get_validated_value(self.opt, self.path,
+                                           True, False, None, True,
+                                           index=index)
 
     def append(self, value=undefined, force=False, setitem=True):
         """the list value can be updated (appened)
         only if the option is a master
         """
-        values = self._getcontext().cfgimpl_get_values()
         if not force:
             if self.opt.impl_is_master_slaves('slave'):
                 raise SlaveError(_("cannot append a value on a multi option {0}"
@@ -531,16 +580,17 @@ class Multi(list):
         index = self.__len__()
         if value is undefined:
             try:
-                value = values._get_validated_value(self.opt, self.path,
-                                                    True, False, None, True,
-                                                    index=index)
+                value = self._get_validated_value(index)
             except IndexError:
                 value = None
-        self._validate(value, index)
+        self._validate(value, index, True)
+        if not '_index' in self.__slots__ and self.opt.impl_is_submulti():
+            if not isinstance(value, SubMulti):
+                value = SubMulti(value, self.context, self.opt, self.path, index)
+            value.submulti = weakref.ref(self)
         super(Multi, self).append(value)
         if setitem:
-            values._setvalue(self.opt, self.path, self,
-                             validate_properties=not force)
+            self._store(force)
 
     def sort(self, cmp=None, key=None, reverse=False):
         if self.opt.impl_is_master_slaves():
@@ -553,43 +603,40 @@ class Multi(list):
             super(Multi, self).sort(key=key, reverse=reverse)
         else:
             super(Multi, self).sort(cmp=cmp, key=key, reverse=reverse)
-        self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path,
-                                                          self)
+        self._store()
 
     def reverse(self):
         if self.opt.impl_is_master_slaves():
             raise SlaveError(_("cannot reverse multi option {0} if master or "
                                "slave").format(self.opt.impl_getname()))
         super(Multi, self).reverse()
-        self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path,
-                                                          self)
+        self._store()
 
     def insert(self, index, obj):
+        #FIXME obj should be undefined
         if self.opt.impl_is_master_slaves():
             raise SlaveError(_("cannot insert multi option {0} if master or "
                                "slave").format(self.opt.impl_getname()))
+        self._validate(obj, index, True)
         super(Multi, self).insert(index, obj)
-        self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path,
-                                                          self)
+        self._store()
 
     def extend(self, iterable):
         if self.opt.impl_is_master_slaves():
             raise SlaveError(_("cannot extend multi option {0} if master or "
                                "slave").format(self.opt.impl_getname()))
+        try:
+            index = self._index
+        except:
+            index = None
+        self._validate(iterable, index)
         super(Multi, self).extend(iterable)
-        self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path,
-                                                          self)
+        self._store()
 
-    def _validate(self, value, force_index):
+    def _validate(self, value, force_index, submulti=False):
         if value is not None:
-            try:
-                self.opt.impl_validate(value, context=self._getcontext(),
-                                       force_index=force_index)
-            except ValueError as err:
-                raise ValueError(_("invalid value {0} "
-                                   "for option {1}: {2}"
-                                   "").format(str(value),
-                                              self.opt.impl_getname(), err))
+            self.opt.impl_validate(value, context=self._getcontext(),
+                                   force_index=force_index)
 
     def pop(self, index, force=False):
         """the list value can be updated (poped)
@@ -611,6 +658,53 @@ class Multi(list):
                     context.cfgimpl_get_values(), index)
         #set value without valid properties
         ret = super(Multi, self).pop(index)
-        context.cfgimpl_get_values()._setvalue(self.opt, self.path, self,
-                                               validate_properties=not force)
+        self._store(force)
         return ret
+
+    def _store(self, force=False):
+        self._getcontext().cfgimpl_get_values()._setvalue(self.opt, self.path,
+                                                          self,
+                                                          validate_properties=not force)
+
+
+class SubMulti(Multi):
+    __slots__ = ('_index', 'submulti')
+
+    def __init__(self, value, context, opt, path, index):
+        """
+        :param index: index (only for slave with submulti)
+        :type index: `int`
+        """
+        self._index = index
+        super(SubMulti, self).__init__(value, context, opt, path)
+
+    def append(self, value=undefined):
+        super(SubMulti, self).append(value, force=True)
+
+    def pop(self, index):
+        return super(SubMulti, self).pop(index, force=True)
+
+    def __setitem__(self, index, value):
+        self._setitem(index, value)
+
+    def _store(self, force=False):
+        #force is unused here
+        self._getcontext().cfgimpl_get_values()._setvalue(self.opt,
+                                                          self.path,
+                                                          self.submulti())
+
+    def _validate(self, value, force_index, submulti=False):
+        if value is not None:
+            if submulti is False:
+                super(SubMulti, self)._validate(value, force_index)
+            else:
+                self.opt.impl_validate(value, context=self._getcontext(),
+                                       force_index=self._index,
+                                       force_submulti_index=force_index)
+
+    def _get_validated_value(self, index):
+        values = self._getcontext().cfgimpl_get_values()
+        return values._get_validated_value(self.opt, self.path,
+                                           True, False, None, True,
+                                           index=index,
+                                           submulti_index=self._index)