From 050979f6d3e3b67760f857ec4fd1ad5b71016ae1 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Wed, 26 Nov 2025 09:22:42 +0100 Subject: [PATCH] feat: ParamSelfOption accept dynamic=False --- tests/test_dyn_optiondescription.py | 32 +++++++- tests/test_option_validator.py | 2 +- tiramisu/autolib.py | 73 ++++++++++++----- tiramisu/config.py | 118 +++++++++++++++++----------- tiramisu/option/baseoption.py | 16 ++-- tiramisu/option/choiceoption.py | 4 +- 6 files changed, 167 insertions(+), 78 deletions(-) diff --git a/tests/test_dyn_optiondescription.py b/tests/test_dyn_optiondescription.py index 5ab8240..a8ec735 100644 --- a/tests/test_dyn_optiondescription.py +++ b/tests/test_dyn_optiondescription.py @@ -39,12 +39,18 @@ def return_true(value, param=None, identifier=None): raise ValueError('no value') -def return_no_dyn(value, identifier): +def return_no_dyn(value): if value in [['val', 'val'], ['yes', 'yes']]: return raise ValueError('no value') +def return_no_dyn_properties(value, identifier): + idx = int(identifier) + if idx and not value[idx - 1]: + return 'disabled' + + def return_dynval(value='val', identifier=None): return value @@ -1138,7 +1144,7 @@ def test_validator_param_self_option(): out = StrOption('out', '', 'val') val1 = StrOption('val1', '', ['val1', 'val2'], multi=True) st_in = StrOption('st_in', '', Calculation(return_dynval, Params(ParamOption(out)))) - st = StrOption('st', '', Calculation(return_dynval, Params(ParamOption(st_in))), validators=[Calculation(return_no_dyn, Params((ParamSelfOption(dynamic=False), ParamIdentifier())))]) + st = StrOption('st', '', Calculation(return_dynval, Params(ParamOption(st_in))), validators=[Calculation(return_no_dyn, Params((ParamSelfOption(dynamic=False),)))]) dod = DynOptionDescription('dod', '', [st_in, st], identifiers=Calculation(return_list)) od = OptionDescription('od', '', [dod, val1, out]) od2 = OptionDescription('od', '', [od]) @@ -1149,6 +1155,28 @@ def test_validator_param_self_option(): cfg.option('od.out').value.set('yes') +def test_properties_param_self_option(): + out = StrOption('out', '', 'val') + val1 = StrOption('val1', '', ["0", "1", "2"], multi=True) + disabled_property = Calculation(return_no_dyn_properties, Params((ParamSelfOption(dynamic=False), ParamIdentifier()))) + st = StrOption('st', '', None, properties=(disabled_property,)) + dod = DynOptionDescription('dod', '', [st], identifiers=Calculation(return_list, Params(ParamOption(val1)))) + od = OptionDescription('od', '', [dod, val1, out]) + od2 = OptionDescription('od', '', [od]) + cfg = Config(od2) + cfg.property.read_write() + assert cfg.option('od.dod0.st').value.get() is None + with pytest.raises(PropertiesOptionError): + cfg.option('od.dod1.st').value.get() + with pytest.raises(PropertiesOptionError): + cfg.option('od.dod2.st').value.get() + cfg.option('od.dod0.st').value.set('val') + assert cfg.option('od.dod0.st').value.get() == 'val' + assert cfg.option('od.dod1.st').value.get() is None + with pytest.raises(PropertiesOptionError): + cfg.option('od.dod2.st').value.get() + + def test_makedict_dyndescription_context(): val1 = StrOption('val1', '', ['val1', 'val2'], multi=True) st = StrOption('st', '') diff --git a/tests/test_option_validator.py b/tests/test_option_validator.py index 25ebb12..b974806 100644 --- a/tests/test_option_validator.py +++ b/tests/test_option_validator.py @@ -6,7 +6,7 @@ import pytest from tiramisu import BoolOption, StrOption, IPOption, NetmaskOption, NetworkOption, BroadcastOption, \ IntOption, OptionDescription, Leadership, Config, Params, ParamValue, ParamOption, \ - ParamSelfOption, ParamIndex, ParamInformation, ParamSelfInformation, ParamSelfOption, Calculation, \ + ParamSelfOption, ParamIndex, ParamInformation, ParamSelfInformation, Calculation, \ valid_ip_netmask, valid_network_netmask, \ valid_in_network, valid_broadcast, valid_not_equal from tiramisu.setting import groups diff --git a/tiramisu/autolib.py b/tiramisu/autolib.py index 82f279c..1ef4bdf 100644 --- a/tiramisu/autolib.py +++ b/tiramisu/autolib.py @@ -172,15 +172,17 @@ class ParamDynOption(ParamOption): class ParamSelfOption(Param): - __slots__ = "whole" + __slots__ = ("whole", "dynamic") def __init__( self, whole: bool = undefined, + dynamic: bool = True, ) -> None: """whole: send all value for a multi, not only indexed value""" if whole is not undefined: self.whole = whole + self.dynamic = dynamic class ParamValue(Param): @@ -613,18 +615,45 @@ def manager_callback( return subconfig.identifiers[param.identifier_index] if isinstance(param, ParamSelfOption): - value = calc_self( - param, - index, - orig_value, - config_bag, - ) - if callback.__name__ not in FUNCTION_WAITING_FOR_DICT: - return value - return { - "name": option.impl_get_display_name(subconfig), - "value": value, - } + search_option = subconfig.option + if subconfig.option.issubdyn() and not param.dynamic: + subconfigs = subconfig.parent.parent.get_common_child( + search_option, + true_path=subconfig.path, + validate_properties=False, + ) + values = [] + properties = config_bag.context.get_settings().getproperties( + subconfig, + uncalculated=True, + ) - {'validator'} + for subconfig_ in subconfigs: + if subconfig.path == subconfig_.path: + values.append(orig_value) + else: + subconfig_.properties = properties + values.append(get_value( + config_bag, + subconfig_, + param, + True, + )) + if callback.__name__ not in FUNCTION_WAITING_FOR_DICT: + return values + return {"name": search_option.impl_get_display_name(subconfig), "value": values} + else: + value = calc_self( + param, + index, + orig_value, + config_bag, + ) + if callback.__name__ not in FUNCTION_WAITING_FOR_DICT: + return value + return { + "name": option.impl_get_display_name(subconfig), + "value": value, + } if isinstance(param, ParamOption): callbk_option = param.option @@ -762,13 +791,15 @@ def manager_callback( ] values = None for subconfig in subconfigs: - callbk_option = subconfig.option - value = get_value( - config_bag, - subconfig, - param, - False, - ) + if isinstance(subconfig, PropertiesOptionError): + value = subconfig + else: + value = get_value( + config_bag, + subconfig, + param, + False, + ) if with_index: value = value[index] if values is not None: @@ -777,6 +808,8 @@ def manager_callback( value = values if callback.__name__ not in FUNCTION_WAITING_FOR_DICT: return value + # FIXME the last one? + callbk_option = subconfig.option return {"name": callbk_option.impl_get_display_name(subconfig), "value": value} diff --git a/tiramisu/config.py b/tiramisu/config.py index b484fe9..0965832 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -42,9 +42,15 @@ from . import autolib def get_common_path(path1, path2): + if None in (path1, path2): + return None common_path = commonprefix([path1, path2]) - if common_path in [path1, path2]: - return common_path + all_paths = [path1, path2] + if common_path in all_paths: + # od.st is not the common_path of od.st_in + all_paths.remove(common_path) + if all_paths[0].startswith(common_path + '.'): + return common_path if common_path.endswith("."): return common_path[:-1] elif "." in common_path: @@ -88,45 +94,52 @@ class CCache: subconfig, resetted_opts, is_default, + *, + force=False, ): """reset cache for one option""" - if subconfig.path in resetted_opts: + if not force and subconfig.path in resetted_opts: return resetted_opts.append(subconfig.path) config_bag = subconfig.config_bag -# if is_default and config_bag.context.get_owner(subconfig) != owners.default: -# return - for is_default, woption in subconfig.option.get_dependencies(subconfig.option): - option = woption() - if option.issubdyn(): - # it's an option in dynoptiondescription, remove cache for all generated option - self.reset_cache_dyn_option( - subconfig, - option, - resetted_opts, - is_default, - ) - elif option.impl_is_dynoptiondescription(): - self.reset_cache_dyn_optiondescription( - option, - config_bag, - resetted_opts, - is_default, - ) - else: - option_subconfig = self.get_sub_config( - config_bag, - option.impl_getpath(), - None, - properties=None, - validate_properties=False, - ) - self.reset_one_option_cache( - option_subconfig, - resetted_opts, - is_default, - ) - del option + if not force: + # if is_default and config_bag.context.get_owner(subconfig) != owners.default: + # return + for is_default, woption in subconfig.option.get_dependencies(subconfig.option): + option = woption() + if option.issubdyn(): + # it's an option in dynoptiondescription, remove cache for all generated option + if option.impl_getpath() == subconfig.option.impl_getpath(): + force = True + subconfig = subconfig.parent.parent + self.reset_cache_dyn_option( + subconfig, + option, + resetted_opts, + is_default, + force, + ) + elif option.impl_is_dynoptiondescription(): + self.reset_cache_dyn_optiondescription( + option, + config_bag, + resetted_opts, + is_default, + ) + else: + option_subconfig = self.get_sub_config( + config_bag, + option.impl_getpath(), + None, + properties=None, + validate_properties=False, + ) + self.reset_one_option_cache( + option_subconfig, + resetted_opts, + is_default, + ) + del option subconfig.option.reset_cache( subconfig.path, config_bag, @@ -182,14 +195,18 @@ class CCache: def get_dynamic_from_dyn_option(self, subconfig, option): config_bag = subconfig.config_bag sub_paths = option.impl_getpath() - current_paths = subconfig.path.split(".") - current_paths_max_index = len(current_paths) - 1 + if not subconfig.path: + current_paths = [] + current_paths_max_index = 0 + else: + current_paths = subconfig.path.split(".") + current_paths_max_index = len(current_paths) - 1 current_subconfigs = [] parent = subconfig while True: current_subconfigs.insert(0, parent) parent = parent.parent - if parent.path is None: + if not parent or parent.path is None: break currents = [self.get_root(config_bag)] for idx, sub_path in enumerate(sub_paths.split(".")): @@ -234,12 +251,14 @@ class CCache: option, resetted_opts, is_default, + force, ): for dyn_option_subconfig in self.get_dynamic_from_dyn_option(subconfig, option): self.reset_one_option_cache( dyn_option_subconfig, resetted_opts, is_default, + force=force, ) @@ -569,10 +588,17 @@ class SubConfig: parents = [self.parent] else: if common_path: - parent = self.parent common_parent_number = common_path.count(".") + 1 - for idx in range(current_option_path.count(".") - common_parent_number): - parent = parent.parent + parent_count = current_option_path.count(".") - common_parent_number + if parent_count >= 0: + parent = self.parent + for idx in range(parent_count): + parent = parent.parent + elif parent_count == 0: + parent = self.parent + else: + # so -1 + parent = self parents = [parent] else: common_parent_number = 0 @@ -617,14 +643,16 @@ class SubConfig: parents = new_parents subconfigs = [] for parent in parents: - subconfigs.append( - parent.get_child( + try: + ret = parent.get_child( search_option, index, validate_properties, check_dynamic_without_identifiers=check_dynamic_without_identifiers, ) - ) + except PropertiesOptionError as err: + ret = err + subconfigs.append(ret) if subconfigs_is_a_list: return subconfigs return subconfigs[0] diff --git a/tiramisu/option/baseoption.py b/tiramisu/option/baseoption.py index 70065ff..b8df7de 100644 --- a/tiramisu/option/baseoption.py +++ b/tiramisu/option/baseoption.py @@ -27,7 +27,7 @@ from itertools import chain from ..i18n import _ from ..setting import undefined -from ..autolib import Calculation, ParamOption, ParamInformation, ParamSelfInformation +from ..autolib import Calculation, ParamOption, ParamSelfOption, ParamInformation, ParamSelfInformation STATIC_TUPLE = frozenset() @@ -104,9 +104,7 @@ class Base: "Calculation" ).format(type(prop), name) ) - for param in chain(prop.params.args, prop.params.kwargs.values()): - if isinstance(param, ParamOption): - param.option._add_dependency(self, "property") + self.value_dependency(prop, type_="property") if properties: _setattr(self, "_properties", properties) self.set_informations(informations) @@ -395,16 +393,20 @@ class BaseOption(Base): self, value: Any, is_identifier: bool = False, + type_: str = 'default' ) -> Any: """parse dependancy to add dependencies""" for param in chain(value.params.args, value.params.kwargs.values()): if isinstance(param, ParamOption): # pylint: disable=protected-access if is_identifier: - type_ = "identifier" + _type_ = "identifier" else: - type_ = "default" - param.option._add_dependency(self, type_, is_identifier=is_identifier) + _type_ = type_ + param.option._add_dependency(self, _type_, is_identifier=is_identifier) + self._has_dependency = True + elif isinstance(param, ParamSelfOption) and not param.dynamic: + self._add_dependency(self, "self") self._has_dependency = True elif isinstance(param, ParamInformation): dest = self diff --git a/tiramisu/option/choiceoption.py b/tiramisu/option/choiceoption.py index eff5249..5965225 100644 --- a/tiramisu/option/choiceoption.py +++ b/tiramisu/option/choiceoption.py @@ -45,9 +45,7 @@ class ChoiceOption(Option): :param values: is a list of values the option can possibly take """ if isinstance(values, Calculation): - for param in chain(values.params.args, values.params.kwargs.values()): - if isinstance(param, ParamOption): - param.option._add_dependency(self, "choice") + self.value_dependency(values, "choice") elif not isinstance(values, tuple): raise TypeError( _("values must be a tuple or a calculation for {0}").format(name)