Compare commits

...

5 commits

10 changed files with 141 additions and 39 deletions

View file

@ -4,7 +4,7 @@ requires = ["flit_core >=3.8.0,<4"]
[project] [project]
name = "tiramisu" name = "tiramisu"
version = "5.2.0a26" version = "5.2.0a27"
authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}] authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}]
readme = "README.md" readme = "README.md"
description = "an options controller tool" description = "an options controller tool"

View file

@ -3130,3 +3130,54 @@ def test_dynoption_duplicate_5():
cfg.value.get() cfg.value.get()
with pytest.raises(ConflictError): with pytest.raises(ConflictError):
cfg.option('od.od_val').value.get() cfg.option('od.od_val').value.get()
def test_dyn_disabled():
var1 = StrOption("var1", "var1", default=["val1", "val2"], multi=True)
var = StrOption("var", "var", properties=frozenset({"disabled"}))
dyn = DynOptionDescription("dyn", "dyn", identifiers=Calculation(calc_value, Params((ParamOption(var1)))), children=[var])
var2 = StrOption(name="var2", doc="A variable calculated", default=Calculation(calc_value, Params((ParamDynOption(var, ["val1"])))))
od = OptionDescription('od', '', [var1, dyn, var2])
cfg = Config(od)
cfg.property.read_write()
with pytest.raises(ConfigError):
cfg.value.get()
def test_dyn_param_calculation():
var1 = StrOption("var1", "var1", default=["val1", "val2"], multi=True)
var = StrOption("var", "var", default=Calculation(calc_value, Params(ParamIdentifier())))
dyn = DynOptionDescription("dyn", "dyn", identifiers=Calculation(calc_value, Params((ParamOption(var1)))), children=[var])
var2 = StrOption(name="var2", doc="A variable calculated", default=Calculation(calc_value, Params((ParamDynOption(var, Calculation(calc_value, Params(ParamValue(["val1"]))))))))
od = OptionDescription('od', '', [var1, dyn, var2])
cfg = Config(od)
cfg.property.read_write()
assert parse_od_get(cfg.value.get()) == {'var1': ['val1', 'val2'], 'dynval1.var': 'val1', 'dynval2.var': 'val2', 'var2': 'val1'}
def test_dyn_param_calculation_var():
identifier_var = StrOption("identifier_var", "identifier_var", default=["val1"], multi=True)
var1 = StrOption("var1", "var1", default=["val1", "val2"], multi=True)
var = StrOption("var", "var", default=Calculation(calc_value, Params(ParamIdentifier())))
dyn = DynOptionDescription("dyn", "dyn", identifiers=Calculation(calc_value, Params((ParamOption(var1)))), children=[var])
var2 = StrOption(name="var2", doc="A variable calculated", default=Calculation(calc_value, Params((ParamDynOption(var, Calculation(calc_value, Params(ParamOption(identifier_var))))))))
od = OptionDescription('od', '', [identifier_var, var1, dyn, var2])
cfg = Config(od)
cfg.property.read_write()
assert parse_od_get(cfg.value.get()) == {'identifier_var': ['val1'], 'var1': ['val1', 'val2'], 'dynval1.var': 'val1', 'dynval2.var': 'val2', 'var2': 'val1'}
cfg.option('identifier_var').value.set(['val2'])
assert parse_od_get(cfg.value.get()) == {'identifier_var': ['val2'], 'var1': ['val1', 'val2'], 'dynval1.var': 'val1', 'dynval2.var': 'val2', 'var2': 'val2'}
def test_dyn_param_calculation_var_not_multi():
identifier_var = StrOption("identifier_var", "identifier_var", default="val1")
var1 = StrOption("var1", "var1", default=["val1", "val2"], multi=True)
var = StrOption("var", "var", default=Calculation(calc_value, Params(ParamIdentifier())))
dyn = DynOptionDescription("dyn", "dyn", identifiers=Calculation(calc_value, Params((ParamOption(var1)))), children=[var])
var2 = StrOption(name="var2", doc="A variable calculated", default=Calculation(calc_value, Params((ParamDynOption(var, Calculation(calc_value, Params(ParamOption(identifier_var))))))))
od = OptionDescription('od', '', [identifier_var, var1, dyn, var2])
cfg = Config(od)
cfg.property.read_write()
assert parse_od_get(cfg.value.get()) == {'identifier_var': 'val1', 'var1': ['val1', 'val2'], 'dynval1.var': 'val1', 'dynval2.var': 'val2', 'var2': 'val1'}
cfg.option('identifier_var').value.set('val2')
assert parse_od_get(cfg.value.get()) == {'identifier_var': 'val2', 'var1': ['val1', 'val2'], 'dynval1.var': 'val1', 'dynval2.var': 'val2', 'var2': 'val2'}

View file

@ -1094,6 +1094,10 @@ def test_follower_properties():
cfg.option('ip_admin_eth0.netmask_admin_eth0').property.add('newproperty1') cfg.option('ip_admin_eth0.netmask_admin_eth0').property.add('newproperty1')
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).property.get() == ('aproperty', 'newproperty', 'newproperty1') cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).property.get() == ('aproperty', 'newproperty', 'newproperty1')
cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).property.get() == ('aproperty', 'newproperty1') cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).property.get() == ('aproperty', 'newproperty1')
#
with pytest.raises(ConfigError):
cfg.option('ip_admin_eth0.netmask_admin_eth0').property.get()
cfg.option('ip_admin_eth0.netmask_admin_eth0').property.get(uncalculated=True) == ('aproperty', 'newproperty1')
# assert not list_sessions() # assert not list_sessions()

View file

@ -208,7 +208,9 @@ def test_mandatory_multi_none():
cfg.option('str3').value.get() cfg.option('str3').value.get()
except PropertiesOptionError as err: except PropertiesOptionError as err:
prop = err.proptype prop = err.proptype
assert 'mandatory' in prop assert 'empty' in prop
assert cfg.option("str3").value.mandatory()
assert cfg.option("str3").value.mandatory(return_type=True) == "empty"
cfg.property.read_write() cfg.property.read_write()
cfg.option('str3').value.set(['yes', None]) cfg.option('str3').value.set(['yes', None])
assert cfg.option('str3').owner.get() == 'user' assert cfg.option('str3').owner.get() == 'user'
@ -218,7 +220,7 @@ def test_mandatory_multi_none():
cfg.option('str3').value.get() cfg.option('str3').value.get()
except PropertiesOptionError as err: except PropertiesOptionError as err:
prop = err.proptype prop = err.proptype
assert 'mandatory' in prop assert 'empty' in prop
# assert not list_sessions() # assert not list_sessions()
@ -244,7 +246,7 @@ def test_mandatory_multi_empty():
cfg.option('str3').value.get() cfg.option('str3').value.get()
except PropertiesOptionError as err: except PropertiesOptionError as err:
prop = err.proptype prop = err.proptype
assert 'mandatory' in prop assert 'empty' in prop
# #
cfg.property.read_write() cfg.property.read_write()
cfg.option('str3').value.set(['yes', '']) cfg.option('str3').value.set(['yes', ''])
@ -255,7 +257,7 @@ def test_mandatory_multi_empty():
cfg.option('str3').value.get() cfg.option('str3').value.get()
except PropertiesOptionError as err: except PropertiesOptionError as err:
prop = err.proptype prop = err.proptype
assert 'mandatory' in prop assert 'empty' in prop
# assert not list_sessions() # assert not list_sessions()
@ -332,10 +334,26 @@ def test_mandatory_warnings_ro():
prop = err.proptype prop = err.proptype
assert 'mandatory' in prop assert 'mandatory' in prop
compare(cfg.value.mandatory(), ['str', 'str1', 'unicode2', 'str3']) compare(cfg.value.mandatory(), ['str', 'str1', 'unicode2', 'str3'])
assert cfg.option('str').value.mandatory()
assert cfg.option('str1').value.mandatory()
assert cfg.option('unicode2').value.mandatory()
assert cfg.option('str3').value.mandatory()
assert cfg.option('str').value.mandatory(return_type=True) == 'mandatory'
assert cfg.option('str1').value.mandatory(return_type=True) == 'mandatory'
assert cfg.option('unicode2').value.mandatory(return_type=True) == 'mandatory'
assert cfg.option('str3').value.mandatory(return_type=True) == 'mandatory'
cfg.property.read_write() cfg.property.read_write()
cfg.option('str').value.set('a') cfg.option('str').value.set('a')
cfg.property.read_only() cfg.property.read_only()
compare(cfg.value.mandatory(), ['str1', 'unicode2', 'str3']) compare(cfg.value.mandatory(), ['str1', 'unicode2', 'str3'])
assert not cfg.option('str').value.mandatory()
assert cfg.option('str1').value.mandatory()
assert cfg.option('unicode2').value.mandatory()
assert cfg.option('str3').value.mandatory()
assert cfg.option('str').value.mandatory(return_type=True) is False
assert cfg.option('str1').value.mandatory(return_type=True) == 'mandatory'
assert cfg.option('unicode2').value.mandatory(return_type=True) == 'mandatory'
assert cfg.option('str3').value.mandatory(return_type=True) == 'mandatory'
# assert not list_sessions() # assert not list_sessions()

View file

@ -8,7 +8,7 @@ import warnings
from tiramisu.setting import groups, owners from tiramisu.setting import groups, owners
from tiramisu import StrOption, IntOption, OptionDescription, submulti, Leadership, Config, \ from tiramisu import StrOption, IntOption, OptionDescription, submulti, Leadership, Config, \
MetaConfig, Params, ParamOption, Calculation MetaConfig, Params, ParamOption, Calculation
from tiramisu.error import LeadershipError, PropertiesOptionError from tiramisu.error import LeadershipError, PropertiesOptionError, ValueOptionError
def return_val(val=None): def return_val(val=None):
@ -317,10 +317,8 @@ def test_values_with_leader_and_followers_submulti_mandatory():
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get() cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get()
# #
cfg.property.read_write() cfg.property.read_write()
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.set(["255.255.255.0", '', "255.255.255.0"]) with pytest.raises(ValueOptionError):
cfg.property.read_only() cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.set(["255.255.255.0", "255.255.255.0"])
with pytest.raises(PropertiesOptionError):
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get()
# assert not list_sessions() # assert not list_sessions()
@ -382,7 +380,6 @@ def test_values_with_leader_and_followers_follower_submulti():
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.set(['255.255.255.0']) cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.set(['255.255.255.0'])
cfg.option('ip_admin_eth0.ip_admin_eth0').value.set(['192.168.230.145']) cfg.option('ip_admin_eth0.ip_admin_eth0').value.set(['192.168.230.145'])
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.set(['255.255.255.0']) cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.set(['255.255.255.0'])
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.set(['255.255.255.0', '255.255.255.0'])
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.reset() cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.reset()
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.set(['255.255.255.0']) cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.set(['255.255.255.0'])
cfg.option('ip_admin_eth0.ip_admin_eth0').value.set(['192.168.230.145', '192.168.230.145']) cfg.option('ip_admin_eth0.ip_admin_eth0').value.set(['192.168.230.145', '192.168.230.145'])
@ -501,7 +498,7 @@ def test_callback_submulti():
def test_callback_submulti_follower(): def test_callback_submulti_follower():
multi = StrOption('multi', '', multi=True) multi = StrOption('multi', '', multi=True)
multi2 = StrOption('multi2', '', Calculation(return_list), multi=submulti) multi2 = StrOption('multi2', '', Calculation(return_list), multi=submulti, properties=('notunique',))
od = Leadership('multi', '', [multi, multi2]) od = Leadership('multi', '', [multi, multi2])
od1 = OptionDescription('multi', '', [od]) od1 = OptionDescription('multi', '', [od])
cfg = Config(od1) cfg = Config(od1)

View file

@ -1 +1 @@
__version__ = "5.2.0a26" __version__ = "5.2.0a27"

View file

@ -218,15 +218,16 @@ def option_type(typ):
): ):
# default is "without_index" # default is "without_index"
if ( if (
"with_index" not in types self._index is not None
and "with_index" not in types
and "with_or_without_index" not in types and "with_or_without_index" not in types
and self._index is not None and "with_index_or_uncalculated" not in types
): ):
msg = _("please do not specify index ({0}.{1})").format( msg = _("please do not specify index ({0}.{1})").format(
self.__class__.__name__, func.__name__ self.__class__.__name__, func.__name__
) )
raise ConfigError(msg) raise ConfigError(msg)
if self._index is None and "with_index" in types: if self._index is None and ("with_index" in types or ("with_index_or_uncalculated" in types and kwargs.get("uncalculated", False) is False)):
msg = _( msg = _(
"please specify index with a follower option ({0}.{1})" "please specify index with a follower option ({0}.{1})"
).format(self.__class__.__name__, func.__name__) ).format(self.__class__.__name__, func.__name__)
@ -788,7 +789,7 @@ class TiramisuOptionProperty(CommonTiramisuOption):
_validate_properties = False _validate_properties = False
@option_type( @option_type(
["option", "optiondescription", "with_index", "symlink", "allow_dynoption"] ["option", "optiondescription", "with_index_or_uncalculated", "symlink", "allow_dynoption"]
) )
def get( def get(
self, self,
@ -1142,7 +1143,7 @@ class TiramisuOptionValue(CommonTiramisuOption, _TiramisuODGet):
"""Length for a leadership""" """Length for a leadership"""
return self._subconfig.parent.get_length_leadership() return self._subconfig.parent.get_length_leadership()
def mandatory(self): def mandatory(self, *, return_type=False):
"""Return path of options with mandatory property without any value""" """Return path of options with mandatory property without any value"""
subconfig = self._subconfig subconfig = self._subconfig
ori_config_bag = self._subconfig.config_bag ori_config_bag = self._subconfig.config_bag
@ -1152,6 +1153,10 @@ class TiramisuOptionValue(CommonTiramisuOption, _TiramisuODGet):
self._subconfig.config_bag = config_bag self._subconfig.config_bag = config_bag
try: try:
if subconfig.option.impl_is_optiondescription(): if subconfig.option.impl_is_optiondescription():
if return_type:
raise ConfigError(
_("return_type is not valid for a optiondescription")
)
options = [] options = []
for subconfig in config_bag.context.walk( for subconfig in config_bag.context.walk(
self._subconfig, self._subconfig,
@ -1172,6 +1177,8 @@ class TiramisuOptionValue(CommonTiramisuOption, _TiramisuODGet):
self._subconfig, only_mandatory=True self._subconfig, only_mandatory=True
) )
except PropertiesOptionError as err: except PropertiesOptionError as err:
if return_type:
return err.proptype[0]
return err.proptype == ["mandatory"] or err.proptype == ["empty"] return err.proptype == ["mandatory"] or err.proptype == ["empty"]
self._subconfig.config_bag = ori_config_bag self._subconfig.config_bag = ori_config_bag
return False return False

View file

@ -155,9 +155,9 @@ class ParamDynOption(ParamOption):
notraisepropertyerror, notraisepropertyerror,
raisepropertyerror, raisepropertyerror,
) )
if not isinstance(identifiers, list): if not isinstance(identifiers, (list, Calculation)):
raise Exception( raise Exception(
_("identifiers in ParamDynOption must be a list, not {0}").format( _("identifiers in ParamDynOption must be a list or a calculation, not {0}").format(
identifiers identifiers
) )
) )
@ -167,6 +167,8 @@ class ParamDynOption(ParamOption):
optional optional
) )
) )
if isinstance(identifiers, Calculation):
option.value_dependency(identifiers, type_="identifier")
self.identifiers = identifiers self.identifiers = identifiers
self.optional = optional self.optional = optional
@ -462,7 +464,7 @@ def manager_callback(
or param.raisepropertyerror or param.raisepropertyerror
): ):
raise err from err raise err from err
raise ConfigError(err, subconfig=subconfig) raise ConfigError(str(err), subconfig=subconfig) from err
except ValueError as err: except ValueError as err:
display_name = subconfig.option.impl_get_display_name( display_name = subconfig.option.impl_get_display_name(
subconfig, with_quote=True subconfig, with_quote=True
@ -675,7 +677,15 @@ def manager_callback(
with_index = False with_index = False
if callbk_option.issubdyn(): if callbk_option.issubdyn():
if isinstance(param, ParamDynOption): if isinstance(param, ParamDynOption):
identifiers = param.identifiers.copy() if isinstance(param.identifiers, Calculation):
identifiers = get_calculated_value(
subconfig,
param.identifiers,
)[0]
if not isinstance(identifiers, list):
identifiers = [identifiers]
else:
identifiers = param.identifiers.copy()
paths = callbk_option.impl_getpath().split(".") paths = callbk_option.impl_getpath().split(".")
parents = [config_bag.context.get_root(config_bag)] parents = [config_bag.context.get_root(config_bag)]
subconfigs_is_a_list = False subconfigs_is_a_list = False
@ -751,21 +761,28 @@ def manager_callback(
) )
) )
else: else:
new_parents.append( try:
parent.get_child( new_parents.append(
doption, parent.get_child(
None, doption,
True, None,
name=name, True,
name=name,
)
) )
) except PropertiesOptionError as err:
# raise PropertiesOptionError (which is catched) because must not add value None in carry_out_calculation
if (
param.notraisepropertyerror
or param.raisepropertyerror
):
raise err from err
raise ConfigError(str(err), subconfig=subconfig) from err
parents = new_parents parents = new_parents
if subconfigs_is_a_list: if subconfigs_is_a_list:
subconfigs = parents subconfigs = parents
else: else:
subconfigs = parents[0] subconfigs = parents[0]
else: else:
search_option = param.option search_option = param.option
subconfigs = subconfig.get_common_child( subconfigs = subconfig.get_common_child(

View file

@ -73,7 +73,8 @@ class Leadership(OptionDescription):
if __debug__: if __debug__:
self._check_default_value(child) self._check_default_value(child)
# remove empty property for follower # remove empty property for follower
child._properties = frozenset(child._properties - {"empty", "unique"}) if not child.impl_is_submulti():
child._properties = frozenset(child._properties - {"empty", "unique"})
followers.append(child) followers.append(child)
child._add_dependency(self, "leadership") child._add_dependency(self, "leadership")
child._leadership = weakref.ref(self) child._leadership = weakref.ref(self)

View file

@ -268,8 +268,8 @@ class Values:
force_allow_empty_list: bool, force_allow_empty_list: bool,
) -> bool: ) -> bool:
"""convenience method to know if an option is empty""" """convenience method to know if an option is empty"""
index = subconfig.index
option = subconfig.option option = subconfig.option
index = subconfig.index
if index is None and option.impl_is_submulti(): if index is None and option.impl_is_submulti():
# index is not set # index is not set
isempty = True isempty = True
@ -277,9 +277,16 @@ class Values:
isempty = self._isempty_multi(val, force_allow_empty_list) isempty = self._isempty_multi(val, force_allow_empty_list)
if isempty: if isempty:
break break
elif ( elif option.impl_is_submulti():
index is None or (index is not None and option.impl_is_submulti()) if index is None:
) and option.impl_is_multi(): isempty = False
for v in value:
if self._isempty_multi(v, force_allow_empty_list):
isempty = True
break
else:
isempty = self._isempty_multi(value, force_allow_empty_list)
elif index is None and option.impl_is_multi():
# it's a single list # it's a single list
isempty = self._isempty_multi(value, force_allow_empty_list) isempty = self._isempty_multi(value, force_allow_empty_list)
else: else:
@ -293,9 +300,9 @@ class Values:
) -> bool: ) -> bool:
if not isinstance(value, list): if not isinstance(value, list):
return False return False
return ( if not force_allow_empty_list:
(not force_allow_empty_list and value == []) or None in value or "" in value return value == []
) return None in value or "" in value
# ______________________________________________________________________ # ______________________________________________________________________
# set value # set value