diff --git a/test/test_option_callback.py b/test/test_option_callback.py index 843ae30..53b896f 100644 --- a/test/test_option_callback.py +++ b/test/test_option_callback.py @@ -8,7 +8,7 @@ from tiramisu.config import KernelConfig from tiramisu.setting import groups, owners from tiramisu import ChoiceOption, BoolOption, IntOption, FloatOption, \ StrOption, OptionDescription, SymLinkOption, IPOption, NetmaskOption, Leadership, \ - undefined, Params, ParamOption, ParamValue, ParamContext + undefined, Params, ParamOption, ParamValue, ParamContext, calc_value from tiramisu.api import TIRAMISU_VERSION from tiramisu.error import PropertiesOptionError, ConflictError, LeadershipError, ConfigError from tiramisu.i18n import _ @@ -1194,3 +1194,100 @@ def test_callback_raise(): api.option('od2.opt2').value.get() except ConfigError as err: assert '"Option 2"' in str(err) + + +def test_calc_value_simple(): + val1 = StrOption('val1', '', 'val1') + val2 = StrOption('val2', '', callback=calc_value, callback_params=Params(ParamOption(val1))) + od = OptionDescription('root', '', [val1, val2]) + cfg = Config(od) + assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val1'} + + +def test_calc_value_multi(): + val1 = StrOption('val1', "", 'val1') + val2 = StrOption('val2', "", 'val2') + val3 = StrOption('val3', "", multi=True, callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2)), multi=ParamValue(True))) + od = OptionDescription('root', '', [val1, val2, val3]) + cfg = Config(od) + assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val2', 'val3': ['val1', 'val2']} + + +def test_calc_value_disabled(): + val1 = StrOption('val1', '', 'val1') + val2 = StrOption('val2', '', callback=calc_value, callback_params=Params(ParamOption(val1, True), default=ParamValue('default_value'))) + od = OptionDescription('root', '', [val1, val2]) + cfg = Config(od) + cfg.property.read_write() + assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val1'} + cfg.option('val1').property.add('disabled') + assert cfg.value.dict() == {'val2': 'default_value'} + + +def test_calc_value_condition(): + boolean = BoolOption('boolean', '', True) + val1 = StrOption('val1', '', 'val1') + val2 = StrOption('val2', '', callback=calc_value, callback_params=Params(ParamOption(val1, True), + default=ParamValue('default_value'), + condition=ParamOption(boolean), + expected=ParamValue(True))) + od = OptionDescription('root', '', [boolean, val1, val2]) + cfg = Config(od) + cfg.property.read_write() + assert cfg.value.dict() == {'boolean': True, 'val1': 'val1', 'val2': 'val1'} + cfg.option('boolean').value.set(False) + assert cfg.value.dict() == {'boolean': False, 'val1': 'val1', 'val2': 'default_value'} + + +def test_calc_value_allow_none(): + from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + val1 = StrOption('val1', "", 'val1') + val2 = StrOption('val2', "") + val3 = StrOption('val3', "", multi=True, callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2)), multi=ParamValue(True), allow_none=ParamValue(True))) + od = OptionDescription('root', '', [val1, val2, val3]) + cfg = Config(od) + assert cfg.value.dict() == {'val1': 'val1', 'val2': None, 'val3': ['val1', None]} + + +def test_calc_value_remove_duplicate(): + from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + val1 = StrOption('val1', "", 'val1') + val2 = StrOption('val2', "", 'val1') + val3 = StrOption('val3', "", multi=True, callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2)), multi=ParamValue(True), remove_duplicate_value=ParamValue(True))) + od = OptionDescription('root', '', [val1, val2, val3]) + cfg = Config(od) + assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val1', 'val3': ['val1']} + + +def test_calc_value_join(): + from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + val1 = StrOption('val1', "", 'val1') + val2 = StrOption('val2', "", 'val2') + val3 = StrOption('val3', "", callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2)), join=ParamValue('.'))) + od = OptionDescription('root', '', [val1, val2, val3]) + cfg = Config(od) + assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val2', 'val3': 'val1.val2'} + + +def test_calc_value_min(): + from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + val1 = StrOption('val1', "", 'val1') + val2 = StrOption('val2', "", 'val2') + val3 = StrOption('val3', "", 'val3') + val4 = StrOption('val4', "", callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2), ParamOption(val3, True)), join=ParamValue('.'), min_args_len=ParamValue(3))) + od = OptionDescription('root', '', [val1, val2, val3, val4]) + cfg = Config(od) + cfg.property.read_write() + assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val2', 'val3': 'val3', 'val4': 'val1.val2.val3'} + cfg.option('val3').property.add('disabled') + assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val2', 'val4': ''} + + +def test_calc_value_add(): + from tiramisu import calc_value, IntOption, OptionDescription, Config, Params, ParamOption, ParamValue + val1 = IntOption('val1', "", 1) + val2 = IntOption('val2', "", 2) + val3 = IntOption('val3', "", callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2)), operator=ParamValue('add'))) + od = OptionDescription('root', '', [val1, val2, val3]) + cfg = Config(od) + assert cfg.value.dict() == {'val1': 1, 'val2': 2, 'val3': 3} diff --git a/tiramisu/__init__.py b/tiramisu/__init__.py index 4826832..db7fad8 100644 --- a/tiramisu/__init__.py +++ b/tiramisu/__init__.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from .function import Params, ParamOption, ParamValue, ParamContext, \ - tiramisu_copy + tiramisu_copy, calc_value from .option import * from .error import APIError from .api import Config, MetaConfig, GroupConfig, MixConfig @@ -37,7 +37,8 @@ allfuncs = ['Params', 'Storage', 'list_sessions', 'delete_session', - 'tiramisu_copy'] + 'tiramisu_copy', + 'calc_value'] allfuncs.extend(all_options) del(all_options) __all__ = tuple(allfuncs) diff --git a/tiramisu/function.py b/tiramisu/function.py index 46bf1c0..94837a1 100644 --- a/tiramisu/function.py +++ b/tiramisu/function.py @@ -12,6 +12,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +from typing import Any, List, Optional +from operator import add, mul, sub, truediv +from .setting import undefined from .i18n import _ @@ -79,3 +82,235 @@ class ParamIndex(Param): def tiramisu_copy(val): # pragma: no cover return val + + +def calc_value(*args: List[Any], + multi: bool=False, + default: Any=undefined, + condition: Any=undefined, + expected: Any=undefined, + condition_operator: str='AND', + allow_none: bool=False, + remove_duplicate_value: bool=False, + join: Optional[str]=None, + min_args_len: Optional[int]=None, + operator: Optional[str]=None, + **kwargs) -> Any: + """calculate value + :param multi: value returns must be a list of value + :param default: default value if condition is not matched or if args is empty + if there is more than one default value, set default_0, default_1, ... + :param condition: test if condition is equal to expected value + if there is more than one condition, set condition_0, condition_1, ... + :param expected: value expected for all conditions + if expected value is different between condition, set expected_0, expected_1, ... + :param condition_operator: OR or AND operator for condition + :param allow_none: if False, do not return list in None is present in list + :param remove_duplicate_value: if True, remote duplicated value + :param join: join all args with specified characters + :param min_args_len: if number of arguments is smaller than this value, return default value + :param operator: operator + + examples: + * you want to copy value from an option to an other option: + >>> from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption + >>> val1 = StrOption('val1', '', 'val1') + >>> val2 = StrOption('val2', '', callback=calc_value, callback_params=Params(ParamOption(val1))) + >>> od = OptionDescription('root', '', [val1, val2]) + >>> cfg = Config(od) + >>> cfg.value.dict() + {'val1': 'val1', 'val2': 'val1'} + + * you want to copy values from two options in one multi option + >>> from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + >>> val1 = StrOption('val1', "", 'val1') + >>> val2 = StrOption('val2', "", 'val2') + >>> val3 = StrOption('val3', "", multi=True, callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2)), multi=ParamValue(True))) + >>> od = OptionDescription('root', '', [val1, val2, val3]) + >>> cfg = Config(od) + >>> cfg.value.dict() + {'val1': 'val1', 'val2': 'val2', 'val3': ['val1', 'val2']} + + * you want to copy a value from an option is it not disabled, otherwise set 'default_value' + >>> from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + >>> val1 = StrOption('val1', '', 'val1') + >>> val2 = StrOption('val2', '', callback=calc_value, callback_params=Params(ParamOption(val1, True), default=ParamValue('default_value'))) + >>> od = OptionDescription('root', '', [val1, val2]) + >>> cfg = Config(od) + >>> cfg.property.read_write() + >>> cfg.value.dict() + {'val1': 'val1', 'val2': 'val1'} + >>> cfg.option('val1').property.add('disabled') + >>> cfg.value.dict() + {'val2': 'default_value'} + + * you want to copy value from an option is an other is True, otherwise set 'default_value' + >>> from tiramisu import calc_value, BoolOption, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + >>> boolean = BoolOption('boolean', '', True) + >>> val1 = StrOption('val1', '', 'val1') + >>> val2 = StrOption('val2', '', callback=calc_value, callback_params=Params(ParamOption(val1, True), + ... default=ParamValue('default_value'), + ... condition=ParamOption(boolean), + ... expected=ParamValue(True))) + >>> od = OptionDescription('root', '', [boolean, val1, val2]) + >>> cfg = Config(od) + >>> cfg.property.read_write() + >>> cfg.value.dict() + {'boolean': True, 'val1': 'val1', 'val2': 'val1'} + >>> cfg.option('boolean').value.set(False) + >>> cfg.value.dict() + {'boolean': False, 'val1': 'val1', 'val2': 'default_value'} + + * you want to copy option even if None is present + >>> from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + >>> val1 = StrOption('val1', "", 'val1') + >>> val2 = StrOption('val2', "") + >>> val3 = StrOption('val3', "", multi=True, callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2)), multi=ParamValue(True), allow_none=ParamValue(True))) + >>> od = OptionDescription('root', '', [val1, val2, val3]) + >>> cfg = Config(od) + >>> cfg.value.dict() + {'val1': 'val1', 'val2': None, 'val3': ['val1', None]} + + * you want uniq value + >>> from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + >>> val1 = StrOption('val1', "", 'val1') + >>> val2 = StrOption('val2', "", 'val1') + >>> val3 = StrOption('val3', "", multi=True, callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2)), multi=ParamValue(True), remove_duplicate_value=ParamValue(True))) + >>> od = OptionDescription('root', '', [val1, val2, val3]) + >>> cfg = Config(od) + >>> cfg.value.dict() + {'val1': 'val1', 'val2': 'val1', 'val3': ['val1']} + + + * you want to join two values with '.' + >>> from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + >>> val1 = StrOption('val1', "", 'val1') + >>> val2 = StrOption('val2', "", 'val2') + >>> val3 = StrOption('val3', "", callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2)), join=ParamValue('.'))) + >>> od = OptionDescription('root', '', [val1, val2, val3]) + >>> cfg = Config(od) + >>> cfg.value.dict() + {'val1': 'val1', 'val2': 'val2', 'val3': 'val1.val2'} + + * you want join three values, only if almost three values are set + >>> from tiramisu import calc_value, StrOption, OptionDescription, Config, Params, ParamOption, ParamValue + >>> val1 = StrOption('val1', "", 'val1') + >>> val2 = StrOption('val2', "", 'val2') + >>> val3 = StrOption('val3', "", 'val3') + >>> val4 = StrOption('val4', "", callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2), ParamOption(val3, True)), join=ParamValue('.'), min_args_len=ParamValue(3))) + >>> od = OptionDescription('root', '', [val1, val2, val3, val4]) + >>> cfg = Config(od) + >>> cfg.property.read_write() + >>> cfg.value.dict() + {'val1': 'val1', 'val2': 'val2', 'val3': 'val3', 'val4': 'val1.val2.val3'} + >>> cfg.option('val3').property.add('disabled') + >>> cfg.value.dict() + {'val1': 'val1', 'val2': 'val2', 'val4': ''} + + * you want to add all values + >>> from tiramisu import calc_value, IntOption, OptionDescription, Config, Params, ParamOption, ParamValue + >>> val1 = IntOption('val1', "", 1) + >>> val2 = IntOption('val2', "", 2) + >>> val3 = IntOption('val3', "", callback=calc_value, callback_params=Params((ParamOption(val1), ParamOption(val2)), operator=ParamValue('add'))) + >>> od = OptionDescription('root', '', [val1, val2, val3]) + >>> cfg = Config(od) + >>> cfg.value.dict() + {'val1': 1, 'val2': 2, 'val3': 3} + + """ + def value_from_kwargs(value: Any, pattern: str, to_dict: bool=False) -> Any: + # if value attribute exist return it's value + # otherwise pattern_0, pattern_1, ... + # otherwise undefined + if value is not undefined: + if to_dict == 'all': + returns = {0: value} + else: + returns = value + else: + kwargs_matches = {} + len_pattern = len(pattern) + for key in kwargs.keys(): + if key.startswith(pattern): + index = int(key[len_pattern:]) + kwargs_matches[index] = kwargs[key] + if not kwargs_matches: + return undefined + keys = sorted(kwargs_matches) + if to_dict: + returns = {} + else: + returns = [] + for key in keys: + if to_dict: + returns[key] = kwargs_matches[key] + else: + returns.append(kwargs_matches[key]) + return returns + + def is_condition_matches(): + calculated_conditions = value_from_kwargs(condition, 'condition_', to_dict='all') + if condition is not undefined: + is_matches = None + calculated_expected = value_from_kwargs(expected, 'expected_', to_dict=True) + for idx, calculated_condition in calculated_conditions.items(): + if isinstance(calculated_expected, dict): + current_matches = calculated_condition == calculated_expected[idx] + else: + current_matches = calculated_condition == calculated_expected + if is_matches is None: + is_matches = current_matches + elif condition_operator == 'AND': + is_matches = is_matches and current_matches + elif condition_operator == 'OR': + is_matches = is_matches or current_matches + else: + raise ValueError(_('unexpected {} condition_operator in calc_value').format(condition_operator)) + else: + is_matches = True + return is_matches + + def get_value(): + if not is_condition_matches(): + # force to default + value = [] + else: + value = list(args) + if min_args_len and not len(value) >= min_args_len: + value = [] + if value == []: + # default value + new_default = value_from_kwargs(default, 'default_') + if new_default is not undefined: + if not isinstance(new_default, list): + value = [new_default] + else: + value = new_default + return value + + value = get_value() + if not multi: + if join is not None: + value = join.join(value) + elif value and operator: + new_value = value[0] + op = {'mul': mul, + 'add': add, + 'div': truediv, + 'sub': sub}[operator] + for val in value[1:]: + new_value = op(new_value, val) + value = new_value + elif value == []: + value = None + else: + value = value[0] + elif None in value and not allow_none: + value = [] + elif remove_duplicate_value: + new_value = [] + for val in value: + if val not in new_value: + new_value.append(val) + value = new_value + return value