From f7bd6e3a471b957ebe829c601a0ef805221bd47e Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 11 Apr 2020 13:13:35 +0200 Subject: [PATCH] add support in calculation when option is in a dynoptiondescription --- tests/test_dyn_optiondescription.py | 58 +++++++++++- tests/test_metaconfig.py | 21 +++++ tiramisu/__init__.py | 5 +- tiramisu/api.py | 24 ++++- tiramisu/autolib.py | 113 +++++++++++++++++------- tiramisu/config.py | 35 +++++++- tiramisu/option/baseoption.py | 26 ++++-- tiramisu/option/dynoptiondescription.py | 10 ++- tiramisu/option/option.py | 21 +++-- tiramisu/option/syndynoption.py | 4 +- 10 files changed, 252 insertions(+), 65 deletions(-) diff --git a/tests/test_dyn_optiondescription.py b/tests/test_dyn_optiondescription.py index 66865c1..481f7dc 100644 --- a/tests/test_dyn_optiondescription.py +++ b/tests/test_dyn_optiondescription.py @@ -9,7 +9,9 @@ from tiramisu import BoolOption, StrOption, ChoiceOption, IPOption, \ StrOption, PortOption, BroadcastOption, DomainnameOption, \ EmailOption, URLOption, UsernameOption, FilenameOption, SymLinkOption, \ OptionDescription, DynOptionDescription, SynDynOption, submulti, Leadership, \ - Config, Params, ParamOption, ParamValue, ParamSuffix, ParamSelfOption, ParamIndex, Calculation, calc_value, \ + Config, \ + Params, ParamOption, ParamValue, ParamSuffix, ParamSelfOption, ParamDynOption, ParamIndex, \ + Calculation, calc_value, \ delete_session from tiramisu.error import PropertiesOptionError, ConfigError, ConflictError from tiramisu.storage import list_sessions @@ -145,8 +147,8 @@ async def test_getdoc_dyndescription(): assert await cfg.option('od.dodval2.stval2').option.name() == 'stval2' assert await cfg.option('od.dodval1').option.name() == 'dodval1' assert await cfg.option('od.dodval2').option.name() == 'dodval2' - assert await cfg.option('od.dodval1.stval1').option.doc() == 'doc1' - assert await cfg.option('od.dodval2.stval2').option.doc() == 'doc1' + assert await cfg.option('od.dodval1.stval1').option.doc() == 'doc1val1' + assert await cfg.option('od.dodval2.stval2').option.doc() == 'doc1val2' assert await cfg.option('od.dodval1').option.doc() == 'doc2val1' assert await cfg.option('od.dodval2').option.doc() == 'doc2val2' assert not await list_sessions() @@ -289,6 +291,56 @@ async def test_callback_dyndescription(): assert not await list_sessions() +@pytest.mark.asyncio +async def test_callback_dyndescription_outside_wrong_param(): + lst = StrOption('lst', '', ['val1', 'val2'], multi=True) + st = StrOption('st', '', Calculation(return_dynval)) + dod = DynOptionDescription('dod', '', [st], suffixes=Calculation(return_list, Params(ParamOption(lst)))) + out = StrOption('out', '', Calculation(return_dynval, Params(ParamOption(st)))) + od = OptionDescription('od', '', [dod, out]) + od2 = OptionDescription('od', '', [od, lst]) + async with await Config(od2) as cfg: + with pytest.raises(ConfigError): + await cfg.value.dict() + assert not await list_sessions() + + +@pytest.mark.asyncio +async def test_callback_dyndescription_outside1(): + lst = StrOption('lst', '', ['val1', 'val2'], multi=True) + st = StrOption('st', '', Calculation(return_dynval)) + dod = DynOptionDescription('dod', '', [st], suffixes=Calculation(return_list, Params(ParamOption(lst)))) + out = StrOption('out', '', Calculation(return_dynval, Params(ParamDynOption(st, 'val1', dod)))) + od = OptionDescription('od', '', [dod, out]) + od2 = OptionDescription('od', '', [od, lst]) + async with await Config(od2) as cfg: + assert await cfg.value.dict() == {'od.dodval1.stval1': 'val', 'od.dodval2.stval2': 'val', 'od.out': 'val', 'lst': ['val1', 'val2']} + await cfg.option('od.dodval1.stval1').value.set('val1') + await cfg.option('od.dodval2.stval2').value.set('val2') + assert await cfg.value.dict() == {'od.dodval1.stval1': 'val1', 'od.dodval2.stval2': 'val2', 'od.out': 'val1', 'lst': ['val1', 'val2']} + await cfg.option('lst').value.set(['val2']) + with pytest.raises(ConfigError): + await cfg.value.dict() + await cfg.option('lst').value.set(['val1']) + assert await cfg.value.dict() == {'od.dodval1.stval1': 'val1', 'od.out': 'val1', 'lst': ['val1']} + assert not await list_sessions() + + +@pytest.mark.asyncio +async def test_callback_dyndescription_outside2(): + lst = StrOption('lst', '', ['val1', 'val2'], multi=True) + out = StrOption('out', '') + st = StrOption('st', '', Calculation(return_dynval, Params(ParamOption(out)))) + dod = DynOptionDescription('dod', '', [st], suffixes=Calculation(return_list, Params(ParamOption(lst)))) + od = OptionDescription('od', '', [dod, out]) + od2 = OptionDescription('od', '', [od, lst]) + async with await Config(od2) as cfg: + assert await cfg.value.dict() == {'od.dodval1.stval1': None, 'od.dodval2.stval2': None, 'od.out': None, 'lst': ['val1', 'val2']} + await cfg.option('od.out').value.set('val1') + assert await cfg.value.dict() == {'od.dodval1.stval1': 'val1', 'od.dodval2.stval2': 'val1', 'od.out': 'val1', 'lst': ['val1', 'val2']} + assert not await list_sessions() + + @pytest.mark.asyncio async def test_callback_list_dyndescription(): st = StrOption('st', '', Calculation(return_list2, Params(ParamSuffix())), multi=True, properties=('notunique',)) diff --git a/tests/test_metaconfig.py b/tests/test_metaconfig.py index ceb54f7..cf216f6 100644 --- a/tests/test_metaconfig.py +++ b/tests/test_metaconfig.py @@ -358,6 +358,27 @@ async def test_meta_new_config_wrong_name(): await delete_sessions(meta) +@pytest.mark.asyncio +async def test_meta_load_config(): + od = make_description() + meta = await MetaConfig(['name1', 'name2'], optiondescription=od) + assert len(list(await meta.config.list())) == 2 + await meta.config.load('name1') + assert len(list(await meta.config.list())) == 3 + await delete_sessions(meta) + + +@pytest.mark.asyncio +async def test_meta_load_config_wrong_name(): + od = make_description() + meta = await MetaConfig(['name1', 'name2'], optiondescription=od) + assert len(list(await meta.config.list())) == 2 + with pytest.raises(ConfigError): + await meta.config.load('name3') + assert len(list(await meta.config.list())) == 2 + await delete_sessions(meta) + + @pytest.mark.asyncio async def test_meta_meta_set(): meta = await make_metaconfig(double=True) diff --git a/tiramisu/__init__.py b/tiramisu/__init__.py index 5156885..793e890 100644 --- a/tiramisu/__init__.py +++ b/tiramisu/__init__.py @@ -17,8 +17,8 @@ from .function import calc_value, calc_value_property_help, valid_ip_netmask, \ valid_network_netmask, valid_in_network, valid_broadcast, \ valid_not_equal -from .autolib import Calculation, Params, ParamOption, ParamSelfOption, ParamValue, \ - ParamIndex, ParamSuffix +from .autolib import Calculation, Params, ParamOption, ParamDynOption, ParamSelfOption, \ + ParamValue, ParamIndex, ParamSuffix from .option import * from .error import APIError from .api import Config, MetaConfig, GroupConfig, MixConfig @@ -31,6 +31,7 @@ from .storage import default_storage, Storage, list_sessions, \ allfuncs = ['Calculation', 'Params', 'ParamOption', + 'ParamDynOption', 'ParamSelfOption', 'ParamValue', 'ParamIndex', diff --git a/tiramisu/api.py b/tiramisu/api.py index 366d689..2399e1b 100644 --- a/tiramisu/api.py +++ b/tiramisu/api.py @@ -1533,8 +1533,7 @@ class _TiramisuContextMixConfig(_TiramisuContextGroupConfig, _TiramisuContextCon async def new(self, session_id, storage=None, - type='config', - new=None): + type='config'): """Create and add a new config""" config = self._config_bag.context if storage is None: @@ -1545,7 +1544,26 @@ class _TiramisuContextMixConfig(_TiramisuContextGroupConfig, _TiramisuContextCon session_id=session_id, storage=storage, type_=type, - new=new) + ) + return await self._return_config(new_config, + storage) + + async def load(self, + session_id, + storage=None, + type='config', + ): + """Create and add a new config""" + config = self._config_bag.context + if storage is None: + storage = config._storage + storage_obj = await storage.get() + async with storage_obj.Connection() as connection: + new_config = await config.load_config(connection, + session_id=session_id, + storage=storage, + type_=type, + ) return await self._return_config(new_config, storage) diff --git a/tiramisu/autolib.py b/tiramisu/autolib.py index 9361518..d4ea3df 100644 --- a/tiramisu/autolib.py +++ b/tiramisu/autolib.py @@ -81,6 +81,26 @@ class ParamOption(Param): self.raisepropertyerror = raisepropertyerror +class ParamDynOption(ParamOption): + __slots__ = ('suffix', + ) + def __init__(self, + option: 'Option', + suffix: str, + dynoptiondescription: 'DynOptionDescription', + notraisepropertyerror: bool=False, + raisepropertyerror: bool=False, + todict: bool=False, + ) -> None: + super().__init__(option, + notraisepropertyerror, + raisepropertyerror, + todict, + ) + self.suffix = suffix + self.dynoptiondescription = dynoptiondescription + + class ParamSelfOption(Param): __slots__ = ('todict', 'whole') def __init__(self, @@ -214,7 +234,10 @@ async def manager_callback(callbk: Union[ParamOption, ParamValue], value = value[apply_index] return value - async def get_value(callbk, option_bag, path): + async def get_value(callbk, + option_bag, + path, + ): try: # get value value = await config_bag.context.getattr(path, @@ -227,6 +250,10 @@ async def manager_callback(callbk: Union[ParamOption, ParamValue], ', {}').format(option.impl_get_display_name(), err), err) except ValueError as err: raise ValueError(_('the option "{0}" is used in a calculation but is invalid ({1})').format(option_bag.option.impl_get_display_name(), err)) + except AttributeError as err: + raise ConfigError(_('impossible to calculate "{0}", {1}').format(option_bag.option.impl_get_display_name(), + err, + )) return value async def get_option_bag(config_bag, @@ -272,41 +299,59 @@ async def manager_callback(callbk: Union[ParamOption, ParamValue], return {'name': option.impl_get_display_name(), 'value': value} - # it's ParamOption - callbk_option = callbk.option - if callbk_option.issubdyn(): - callbk_option = callbk_option.to_dynoption(option.rootpath, - option.impl_getsuffix(), - callbk_option.getsubdyn()) - if leadership_must_have_index and callbk_option.impl_get_leadership() and index is None: - raise Break() - if config_bag is undefined: - return undefined - if index is not None and callbk_option.impl_get_leadership() and \ - callbk_option.impl_get_leadership().in_same_group(option): - if not callbk_option.impl_is_follower(): - # leader - index_ = None - with_index = True + if isinstance(callbk, ParamOption): + callbk_option = callbk.option + if callbk_option.issubdyn(): + if isinstance(callbk, ParamDynOption): + subdyn = callbk.dynoptiondescription + rootpath = subdyn.impl_getpath() + callbk.suffix + suffix = callbk.suffix + else: + if not option.impl_is_dynsymlinkoption(): + msg = 'option "{}" is not dynamic in callback of the option "{}"' + raise ConfigError(_(msg).format(callbk_option.impl_get_display_name(), + option.impl_get_display_name(), + )) + rootpath = option.rootpath + suffix = option.impl_getsuffix() + subdyn = callbk_option.getsubdyn() + callbk_option = callbk_option.to_dynoption(rootpath, + suffix, + subdyn) + if leadership_must_have_index and callbk_option.impl_get_leadership() and index is None: + raise Break() + if config_bag is undefined: + return undefined + if index is not None and callbk_option.impl_get_leadership() and \ + callbk_option.impl_get_leadership().in_same_group(option): + if not callbk_option.impl_is_follower(): + # leader + index_ = None + with_index = True + else: + # follower + index_ = index + with_index = False else: - # follower - index_ = index + index_ = None with_index = False - else: - index_ = None - with_index = False - path = callbk_option.impl_getpath() - option_bag = await get_option_bag(config_bag, - callbk_option, - index_, - False) - value = await get_value(callbk, option_bag, path) - if with_index: - value = value[index] - if not callbk.todict: - return value - return {'name': callbk_option.impl_get_display_name(), - 'value': value} + path = callbk_option.impl_getpath() + option_bag = await get_option_bag(config_bag, + callbk_option, + index_, + False) + value = await get_value(callbk, + option_bag, + path, + ) + if with_index: + value = value[index] + if not callbk.todict: + return value + return {'name': callbk_option.impl_get_display_name(), + 'value': value} + raise ConfigError(_('unknown callback type {} in option {}').format(callbk, + option.impl_get_display_name())) async def carry_out_calculation(option, diff --git a/tiramisu/config.py b/tiramisu/config.py index 8a8ed0d..d33d3cd 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -124,6 +124,18 @@ class SubConfig: await self.reset_one_option_cache(desc, resetted_opts, doption_bag) + async for coption in self.cfgimpl_get_description().get_children_recursively(None, + None, + option_bag.config_bag): + coption_bag = OptionBag() + coption_bag.set_option(coption, + option_bag.index, + option_bag.config_bag) + coption_bag.properties = await self.cfgimpl_get_settings().getproperties(coption_bag) + await self.reset_one_option_cache(option, + resetted_opts, + coption_bag, + ) elif option.issubdyn(): # it's an option in dynoptiondescription, remove cache for all generated option dynopt = option.getsubdyn() @@ -1176,13 +1188,28 @@ class KernelMixConfig(KernelGroupConfig): session_id, type_='config', storage=None, - new=None, ): - if new is None: - new = session_id not in await list_sessions() - if new and session_id in [child.impl_getname() for child in self._impl_children]: + if session_id in [child.impl_getname() for child in self._impl_children]: raise ConflictError(_('config name must be uniq in ' 'groupconfig for {0}').format(session_id)) + return await self.load_config(connection, + session_id, + type_, + storage, + new=True, + ) + + async def load_config(self, + connection, + session_id, + type_='config', + storage=None, + new=False, + ): + if not new: + if session_id not in [child.impl_getname() for child in self._impl_children]: + raise ConfigError(_('cannot find existing config with session_id to "{}"').format(session_id)) + assert type_ in ('config', 'metaconfig', 'mixconfig'), _('unknown type {}').format(type_) if type_ == 'config': config = await KernelConfig(self._impl_descr, diff --git a/tiramisu/option/baseoption.py b/tiramisu/option/baseoption.py index 902a60d..8440b2f 100644 --- a/tiramisu/option/baseoption.py +++ b/tiramisu/option/baseoption.py @@ -270,20 +270,36 @@ class BaseOption(Base): return self.impl_get_callback()[0] is not None def _impl_get_display_name(self, - dyn_name: Base=None) -> str: + dyn_name: Base=None, + suffix: str=None, + ) -> str: name = self.impl_get_information('doc', None) if name is None or name == '': if dyn_name is not None: name = dyn_name else: name = self.impl_getname() + elif suffix: + name += suffix return name - def impl_get_display_name(self, - dyn_name: Base=None) -> str: + def _get_display_name(self, + dyn_name, + suffix, + ): if hasattr(self, '_display_name_function'): - return self._display_name_function(self, dyn_name) - return self._impl_get_display_name(dyn_name) + return self._display_name_function(self, + dyn_name, + suffix, + ) + return self._impl_get_display_name(dyn_name, + suffix, + ) + + def impl_get_display_name(self) -> str: + return self._get_display_name(None, + None, + ) def reset_cache(self, path: str, diff --git a/tiramisu/option/dynoptiondescription.py b/tiramisu/option/dynoptiondescription.py index 324b031..7b04750 100644 --- a/tiramisu/option/dynoptiondescription.py +++ b/tiramisu/option/dynoptiondescription.py @@ -20,6 +20,8 @@ # ____________________________________________________________ import re from typing import List, Callable +from itertools import chain +from ..autolib import ParamOption from ..i18n import _ @@ -60,8 +62,12 @@ class DynOptionDescription(OptionDescription): 'dynoptiondescription')) child._setsubdyn(self) # add suffixes - if __debug__ and isinstance(suffixes, Calculation): - self._suffixes = suffixes + if __debug__ and not isinstance(suffixes, Calculation): + raise ConfigError(_('suffixes in dynoptiondescription has to be a calculation')) + for param in chain(suffixes.params.args, suffixes.params.kwargs.values()): + if isinstance(param, ParamOption): + param.option._add_dependency(self) + self._suffixes = suffixes def convert_suffix_to_path(self, suffix): diff --git a/tiramisu/option/option.py b/tiramisu/option/option.py index d28092f..7b42d6e 100644 --- a/tiramisu/option/option.py +++ b/tiramisu/option/option.py @@ -93,17 +93,16 @@ class Option(BaseOption): doc, properties=properties, is_multi=is_multi) - if __debug__: - if validators is not None: - if not isinstance(validators, list): - raise ValueError(_('validators must be a list of Calculation for "{}"').format(name)) - for validator in validators: - if not isinstance(validator, Calculation): - raise ValueError(_('validators must be a Calculation for "{}"').format(name)) - for param in chain(validator.params.args, validator.params.kwargs.values()): - if isinstance(param, ParamOption): - param.option._add_dependency(self) - self._has_dependency = True + if validators is not None: + if __debug__ and not isinstance(validators, list): + raise ValueError(_('validators must be a list of Calculation for "{}"').format(name)) + for validator in validators: + if __debug__ and not isinstance(validator, Calculation): + raise ValueError(_('validators must be a Calculation for "{}"').format(name)) + for param in chain(validator.params.args, validator.params.kwargs.values()): + if isinstance(param, ParamOption): + param.option._add_dependency(self) + self._has_dependency = True self._validators = tuple(validators) if extra is not None and extra != {}: diff --git a/tiramisu/option/syndynoption.py b/tiramisu/option/syndynoption.py index 2a5073a..37204a4 100644 --- a/tiramisu/option/syndynoption.py +++ b/tiramisu/option/syndynoption.py @@ -59,7 +59,9 @@ class SynDynOption: return self.opt.impl_getname() + self.suffix def impl_get_display_name(self) -> str: - return self.opt.impl_get_display_name(dyn_name=self.impl_getname()) + self.suffix + return self.opt._get_display_name(dyn_name=self.impl_getname(), + suffix=self.suffix, + ) def impl_getsuffix(self) -> str: return self.suffix