diff --git a/.cz.toml b/.cz.toml deleted file mode 100644 index 64fb53b..0000000 --- a/.cz.toml +++ /dev/null @@ -1,6 +0,0 @@ -[tool.commitizen] -name = "cz_conventional_commits" -tag_format = "$version" -version_scheme = "semver" -version = "4.1.0" -update_changelog_on_bump = true diff --git a/tests/test_dyn_optiondescription.py b/tests/test_dyn_optiondescription.py index bb2a61e..f774036 100644 --- a/tests/test_dyn_optiondescription.py +++ b/tests/test_dyn_optiondescription.py @@ -508,8 +508,8 @@ def test_dyndescription_subdyn(): assert set(cfg.option('od.dod2val1.dodval1.st').property.get()) == {'validator'} assert set(cfg.option('od.dod2val1.dodval1.st').property.get(uncalculated=True)) == {'validator'} assert set(cfg.option('od.dod2.dod.st').property.get(uncalculated=True)) == {'validator'} - with pytest.raises(AttributeOptionError): - cfg.option('od.dod2.dodval1.st').property.get(uncalculated=True) +# with pytest.raises(AttributeOptionError): + assert set(cfg.option('od.dod2.dodval1.st').property.get(uncalculated=True)) == {'validator'} assert cfg.option('od.dod2val1.dod.st').property.get(uncalculated=True) == {'validator'} # with pytest.raises(AttributeOptionError): @@ -540,11 +540,13 @@ def test_dyndescription_subdyn(): assert cfg.option('od.dod2.dod').group_type() == "default" assert cfg.option('od.dod2val1.dodval1').group_type() == "default" # - with pytest.raises(AttributeOptionError): - cfg.option('od.dod2.dod').identifiers() + assert cfg.option('od.dod2.dod').identifiers() == [['val1', 'val1'], ['val1', 'val2'], ['val2', 'val1'], ['val2', 'val2']] assert cfg.option('od.dod2.dod').identifiers(only_self=True) == ['val1', 'val2'] assert cfg.option('od.dod2val1.dodval1').identifiers() == ['val1', 'val1'] assert cfg.option('od.dod2val1.dodval1').identifiers(only_self=True) == ['val1', 'val2'] + assert cfg.option('od.dod2val1.dodval2').identifiers() == ['val1', 'val2'] + assert cfg.option('od.dod2val1.dod').identifiers() == [['val1', 'val1'], ['val1', 'val2']] + assert cfg.option('od.dod2.dodval1.st').identifiers() == [['val1', 'val1'], ['val2', 'val1']] def test_callback_dyndescription_subdyn(): @@ -562,6 +564,37 @@ def test_callback_dyndescription_subdyn(): assert parse_od_get(cfg.value.get()) == {'od.dod2val1.dodval1.st': 'val1', 'od.dod2val1.dodval3.st': 'val1', 'od.dod2val1.out': ['val1', 'val1'], 'od.dod2val3.dodval1.st': 'val1', 'od.dod2val3.dodval3.st': 'val1', 'od.dod2val3.out': ['val1', 'val1'], 'lst': ['val1', 'val3']} cfg.option('lst').value.set(["val1", "val3", None]) assert parse_od_get(cfg.value.get()) == {'od.dod2val1.dodval1.st': 'val1', 'od.dod2val1.dodval3.st': 'val1', 'od.dod2val1.out': ['val1', 'val1'], 'od.dod2val3.dodval1.st': 'val1', 'od.dod2val3.dodval3.st': 'val1', 'od.dod2val3.out': ['val1', 'val1'], 'lst': ['val1', 'val3', None]} + assert cfg.option('od.dod2.dod.st').identifiers() == [['val1', 'val1'], ['val1', 'val3'], ['val3', 'val1'], ['val3', 'val3']] + assert cfg.option('od.dod2.dod').identifiers(only_self=True) == ['val1', 'val3'] + assert cfg.option('od.dod2val1.dod.st').identifiers() == [['val1', 'val1'], ['val1', 'val3']] + assert cfg.option('od.dod2val1.dodval1.st').identifiers() == ['val1', 'val1'] + assert cfg.option('od.dod2val1.dodval1').identifiers(only_self=True) == ['val1', 'val3'] + + +def test_callback_dyndescription_subdyn2(): + lst1 = StrOption('lst1', '', ['val1', 'val2'], multi=True) + lst2 = StrOption('lst2', '', ['val3', 'val4'], multi=True) + st = StrOption('st', '', 'val1') + dod = DynOptionDescription('dod', '', [st], identifiers=Calculation(return_list, Params(ParamOption(lst1)))) + out = StrOption('out', '', Calculation(return_dynval, Params(ParamDynOption(st, [None, 'val1']))), multi=True, properties=('notunique',)) + dod2 = DynOptionDescription('dod2', '', [dod, out], identifiers=Calculation(return_list, Params(ParamOption(lst2)))) + od = OptionDescription('od', '', [dod2]) + od2 = OptionDescription('od', '', [od, lst1, lst2]) + cfg = Config(od2) + cfg.property.read_write() + # + assert parse_od_get(cfg.value.get()) == {'od.dod2val3.dodval1.st': 'val1', 'od.dod2val3.dodval2.st': 'val1', 'od.dod2val3.out': ['val1', 'val1'], 'od.dod2val4.dodval1.st': 'val1', 'od.dod2val4.dodval2.st': 'val1', 'od.dod2val4.out': ['val1', 'val1'], 'lst1': ['val1', 'val2'], 'lst2': ['val3', 'val4']} + # + cfg.option('lst1').value.set(["val1", "val3"]) + assert parse_od_get(cfg.value.get()) == {'od.dod2val3.dodval1.st': 'val1', 'od.dod2val3.dodval3.st': 'val1', 'od.dod2val3.out': ['val1', 'val1'], 'od.dod2val4.dodval1.st': 'val1', 'od.dod2val4.dodval3.st': 'val1', 'od.dod2val4.out': ['val1', 'val1'], 'lst1': ['val1', 'val3'], 'lst2': ['val3', 'val4']} + # + cfg.option('lst1').value.set(["val1", "val3", None]) + assert parse_od_get(cfg.value.get()) == {'od.dod2val3.dodval1.st': 'val1', 'od.dod2val3.dodval3.st': 'val1', 'od.dod2val3.out': ['val1', 'val1'], 'od.dod2val4.dodval1.st': 'val1', 'od.dod2val4.dodval3.st': 'val1', 'od.dod2val4.out': ['val1', 'val1'], 'lst1': ['val1', 'val3', None], 'lst2': ['val3', 'val4']} + assert cfg.option('od.dod2.dod.st').identifiers() == [['val3', 'val1'], ['val3', 'val3'], ['val4', 'val1'], ['val4', 'val3']] + assert cfg.option('od.dod2.dod').identifiers(only_self=True) == ['val1', 'val3'] + assert cfg.option('od.dod2val3.dod.st').identifiers() == [['val3', 'val1'], ['val3', 'val3']] + assert cfg.option('od.dod2val3.dodval1.st').identifiers() == ['val3', 'val1'] + assert cfg.option('od.dod2val3.dodval1').identifiers(only_self=True) == ['val1', 'val3'] def test_callback_list_dyndescription(): @@ -2220,6 +2253,8 @@ def test_leadership_dyndescription_convert(): assert cfg.option('od.stval2.st1.st1').owner.isdefault() assert cfg.option('od.stval2.st1.st1').identifiers() == ["val.2"] assert cfg.option('od.stval2.st1.st1').identifiers(convert=True) == ["val2"] + assert cfg.option('od.st.st1.st1').identifiers() == [["val.1"], ["val.2"]] + assert cfg.option('od.st.st1.st1').identifiers(convert=True) == [["val1"], ["val2"]] assert cfg.option('od.stval2').identifiers(only_self=True) == ["val.1", "val.2"] assert cfg.option('od.stval2').identifiers(only_self=True, convert=True) == ["val1", "val2"] # diff --git a/tests/test_requires.py b/tests/test_requires.py index 16b1772..6aab523 100644 --- a/tests/test_requires.py +++ b/tests/test_requires.py @@ -1186,7 +1186,6 @@ def test_leadership_requires_leadership(config_type): # cfg.option('activate').value.set(False) if config_type != 'tiramisu-api': - # FIXME with pytest.raises(PropertiesOptionError): cfg.option('ip_admin_eth0.ip_admin_eth0').value.get() with pytest.raises(PropertiesOptionError): diff --git a/tiramisu/api.py b/tiramisu/api.py index 19ca5fb..27b38cb 100644 --- a/tiramisu/api.py +++ b/tiramisu/api.py @@ -135,11 +135,13 @@ class CommonTiramisu(TiramisuHelp): config_bag=self._config_bag, parent=subconfig.parent, identifiers=subconfig.identifiers, + identifier=None, true_path=subconfig.true_path, properties=subconfig.properties, validate_properties=False, check_dynamic_without_identifiers=False, ) + self._subconfig.is_self_dynamic_without_identifiers = subconfig.is_self_dynamic_without_identifiers else: self._subconfig._length = None if not self._subconfig: @@ -148,18 +150,14 @@ class CommonTiramisu(TiramisuHelp): self._config_bag, self._path, self._index, - validate_properties=False, + validate_properties=self._validate_properties, allow_dynoption=True, ) except AssertionError as err: raise ConfigError(str(err)) -def option_type(typ): - if not isinstance(typ, list): - types = [typ] - else: - types = typ +def option_type(types): def wrapper(func): @wraps(func) @@ -179,7 +177,7 @@ def option_type(typ): return func(self, options_bag, *args[1:], **kwargs) self._set_subconfig() if ( - not isinstance(typ, list) or "allow_dynoption" not in typ + "allow_dynoption" not in types and not ("dynoption_or_uncalculated" in types and kwargs.get("uncalculated", False) is True) ) and self._subconfig.is_dynamic_without_identifiers: raise AttributeOptionError(self._subconfig.path, "option-dynamic") @@ -595,43 +593,88 @@ class _TiramisuOptionOptionDescription: ) return ret + def has_identifiers(self): + return self._subconfig.is_dynamic_without_identifiers + @option_type(["dynamic", "with_or_without_index", "allow_dynoption"]) def identifiers( self, + *, only_self: bool = False, uncalculated: bool = False, convert: bool = False, ): """Get identifiers for dynamic option""" subconfig = self._subconfig - if not only_self: - if self._subconfig.is_dynamic_without_identifiers and not uncalculated: - raise AttributeOptionError(self._subconfig.path, "option-dynamic") - if not convert: - return self._subconfig.identifiers - identifiers = [] - dynconfig = None - while not dynconfig: - if subconfig.option.impl_is_optiondescription() and subconfig.option.impl_is_dynoptiondescription(): - dynconfig = subconfig - subconfig = subconfig.parent - for identifier in self._subconfig.identifiers: - if identifier is None: - continue - identifiers.append(dynconfig.option.convert_identifier_to_path(identifier)) - return identifiers + if only_self: + func = self._identifiers_only_self + elif not subconfig.is_dynamic_without_identifiers: + func = self._identifiers_all + else: + func = self._identifiers_all_no_identifiers + return func(subconfig, uncalculated, convert) + + def _identifiers_all(self, subconfig, uncalculated, convert): + dynconfig = None + _subconfig = subconfig + while not dynconfig: + if _subconfig.option.impl_is_optiondescription() and _subconfig.option.impl_is_dynoptiondescription(): + dynconfig = _subconfig + else: + _subconfig = _subconfig.parent + identifiers = [] + for identifier in _subconfig.identifiers: + if convert: + identifier = dynconfig.option.convert_identifier_to_path(identifier) + identifiers.append(identifier) + return identifiers + + def _identifiers_all_no_identifiers(self, subconfig, uncalculated, convert): + """dyn{{ identifier }}.var{{ identifier }}.var + => dyn["val1", "val2"] + => var["val3", "val4"] + returns [["val1", "val3"], ["val1", "val4"], ["val2", "val3"], ["val2", "val4"]] + """ + identifiers = [] + while True: + if subconfig.option.impl_is_optiondescription() and subconfig.option.impl_is_dynoptiondescription(): + if not subconfig.is_self_dynamic_without_identifiers: + new_identifiers = [subconfig.identifiers[-1]] + else: + new_identifiers = subconfig.option.get_identifiers(subconfig.parent, uncalculated=uncalculated, convert=convert) + if isinstance(new_identifiers, Calculation): + if identifiers: + identifiers = [[new_identifiers] + old_identifiers for old_identifiers in identifiers] + else: + identifiers = [new_identifiers] + elif identifiers: + identifiers = [[identifier] + old_identifiers for identifier in new_identifiers for old_identifiers in identifiers] + else: + identifiers = [[identifier] for identifier in new_identifiers] + subconfig = subconfig.parent + if subconfig is None: + break + return identifiers + + def _identifiers_only_self(self, subconfig, uncalculated, convert): if ( - not self._subconfig.option.impl_is_optiondescription() - or not self._subconfig.option.impl_is_dynoptiondescription() + not subconfig.option.impl_is_optiondescription() + or not subconfig.option.impl_is_dynoptiondescription() ): raise ConfigError( _( "the option {0} is not a dynamic option, cannot get identifiers with only_self parameter to True" - ).format(self._subconfig.path), + ).format(subconfig.path), subconfig=subconfig, ) - return self._subconfig.option.get_identifiers( - self._subconfig.parent, + dynconfig = None + _subconfig = subconfig + while not dynconfig: + if _subconfig.option.impl_is_optiondescription() and _subconfig.option.impl_is_dynoptiondescription(): + dynconfig = _subconfig + _subconfig = _subconfig.parent + return dynconfig.option.get_identifiers( + dynconfig.parent, uncalculated=uncalculated, convert=convert, ) @@ -1010,7 +1053,7 @@ class TiramisuOptionValue(CommonTiramisuOption, _TiramisuODGet): _validate_properties = True - @option_type(["option", "symlink", "with_index", "optiondescription"]) + @option_type(["option", "symlink", "with_index", "optiondescription", "dynoption_or_uncalculated"]) def get( self, *, @@ -1074,7 +1117,7 @@ class TiramisuOptionValue(CommonTiramisuOption, _TiramisuODGet): values.reset(self._subconfig) @option_type( - ["option", "with_or_without_index", "symlink", "dont_validate_property"] + ["option", "with_or_without_index", "symlink", "dont_validate_property", "dynoption_or_uncalculated"] ) def default( self, @@ -1113,7 +1156,7 @@ class TiramisuOptionValue(CommonTiramisuOption, _TiramisuODGet): return False return True - @option_type(["choice", "with_index", "allow_dynoption"]) + @option_type(["choice", "with_index_or_uncalculated", "allow_dynoption"]) def list( self, *, @@ -1127,7 +1170,7 @@ class TiramisuOptionValue(CommonTiramisuOption, _TiramisuODGet): uncalculated, ) - @option_type("leader") + @option_type(["leader"]) def pop( self, index: int, @@ -1350,7 +1393,7 @@ class TiramisuOption( remotable=remotable, ) - @option_type("optiondescription") + @option_type(["optiondescription"]) def dict( self, clearable: str = "all", @@ -1363,7 +1406,7 @@ class TiramisuOption( self._load_dict(clearable, remotable) return self._tiramisu_dict.todict(form) - @option_type("optiondescription") + @option_type(["optiondescription"]) def updates( self, body: List, @@ -1445,17 +1488,20 @@ class TiramisuContextValue(TiramisuConfig, _TiramisuODGet): only_mandatory=True, ): if id(subconfig.config_bag) != id(config_bag): + old_is_dynamic_without_identifiers = subconfig.is_self_dynamic_without_identifiers subconfig = subconfig.__class__(option=subconfig.option, index=subconfig.index, path=subconfig.path, config_bag=config_bag, parent=subconfig.parent, identifiers=subconfig.identifiers, + identifier=None, true_path=subconfig.true_path, properties=subconfig.properties, validate_properties=False, check_dynamic_without_identifiers=False, ) + subconfig.is_self_dynamic_without_identifiers = old_is_dynamic_without_identifiers else: subconfig._length = None options.append( diff --git a/tiramisu/autolib.py b/tiramisu/autolib.py index 6b6e67d..96074d8 100644 --- a/tiramisu/autolib.py +++ b/tiramisu/autolib.py @@ -691,6 +691,7 @@ def manager_callback( subconfigs_is_a_list = False for name in paths: new_parents = [] + identifier = undefined for parent in parents: doption = parent.option.get_child( name, @@ -699,11 +700,12 @@ def manager_callback( allow_dynoption=True, ) if doption.impl_is_dynoptiondescription(): - if not identifiers: - identifier = None - else: - identifier = identifiers.pop(0) - if not identifier: + if identifier is undefined: + if not identifiers: + identifier = None + else: + identifier = identifiers.pop(0) + if identifier is None: subconfigs_is_a_list = True new_parents.extend( parent.dyn_to_subconfig( @@ -879,17 +881,20 @@ def carry_out_calculation( kwargs = {} config_bag = config_bag.copy() config_bag.set_permissive() + old_is_dynamic_without_identifiers = subconfig.is_dynamic_without_identifiers subconfig = subconfig.__class__(option=subconfig.option, index=subconfig.index, path=subconfig.path, config_bag=config_bag, parent=subconfig.parent, identifiers=subconfig.identifiers, + identifier=None, true_path=subconfig.true_path, properties=subconfig.properties, validate_properties=False, check_dynamic_without_identifiers=False, ) + subconfig.is_dynamic_without_identifiers = old_is_dynamic_without_identifiers if callback_params: for key, param in chain( fake_items(callback_params.args), callback_params.kwargs.items() diff --git a/tiramisu/config.py b/tiramisu/config.py index f9612d6..7554f85 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -275,6 +275,7 @@ class SubConfig: "transitive_properties", "is_dynamic", "is_dynamic_without_identifiers", + "is_self_dynamic_without_identifiers", "identifiers", "_length", ) @@ -287,6 +288,7 @@ class SubConfig: config_bag: ConfigBag, parent: Optional["SubConfig"], identifiers: Optional[list[str]], + identifier: Optional[str], *, true_path: Optional[str] = None, # for python 3.9 properties: Union[list[str], undefined] = undefined, @@ -308,6 +310,7 @@ class SubConfig: ) self.apply_requires = not is_follower or index is not None self.true_path = true_path + self.is_self_dynamic_without_identifiers = identifier is None if self.option.impl_is_dynoptiondescription(): self.is_dynamic = True self.is_dynamic_without_identifiers = identifiers is None or ( @@ -321,7 +324,8 @@ class SubConfig: and self.is_dynamic_without_identifiers != parent.is_dynamic_without_identifiers ): - raise AttributeOptionError(true_path, "option-dynamic") + self.is_dynamic_without_identifiers = True + # raise AttributeOptionError(true_path, "option-dynamic") elif parent: self.is_dynamic = parent.is_dynamic self.is_dynamic_without_identifiers = parent.is_dynamic_without_identifiers @@ -339,15 +343,14 @@ class SubConfig: self.config_bag.context.get_settings().validate_properties(self) self._properties = undefined self.config_bag.context.get_settings().validate_properties(self) - if self.apply_requires and self.option.impl_is_optiondescription(): - if self.path and self.properties is not None: - settings = config_bag.context.get_settings() - self.transitive_properties = settings.calc_transitive_properties( - self, - self.properties, - ) - else: - self.transitive_properties = frozenset() + if validate_properties and self.apply_requires and self.option.impl_is_optiondescription() and self.path and self.properties is not None: + settings = config_bag.context.get_settings() + self.transitive_properties = settings.calc_transitive_properties( + self, + self.properties, + ) + else: + self.transitive_properties = frozenset() @property def properties(self): @@ -515,6 +518,7 @@ class SubConfig: config_bag, self, identifiers, + identifier, properties=properties, validate_properties=validate_properties, true_path=true_path, @@ -560,8 +564,10 @@ class SubConfig: cconfig_bag, self, self.identifiers, + None, validate_properties=False, ) + subconfig.is_self_dynamic_without_identifiers = self.is_self_dynamic_without_identifiers #FIXME #self._length = len(cconfig_bag.context.get_value(subconfig)) length = len(cconfig_bag.context.get_value(subconfig)) @@ -664,16 +670,19 @@ class SubConfig: def change_context(self, context) -> "SubConfig": config_bag = self.config_bag.copy() config_bag.context = context - return SubConfig( + subconfig = SubConfig( self.option, self.index, self.path, config_bag, self.parent, self.identifiers, + None, true_path=self.true_path, validate_properties=False, ) + subconfig.is_self_dynamic_without_identifiers = self.is_self_dynamic_without_identifiers + return subconfig class _Config(CCache): @@ -761,6 +770,7 @@ class _Config(CCache): config_bag, None, None, + None, ) def get_sub_config( diff --git a/tiramisu/option/option.py b/tiramisu/option/option.py index 36e5947..3dbd8df 100644 --- a/tiramisu/option/option.py +++ b/tiramisu/option/option.py @@ -227,7 +227,13 @@ class Option(BaseOption): default = getattr(self, "_default", undefined) if default is undefined: if is_multi: - default = [] + if self.impl_is_follower(): + if submulti: + default = getattr(self, "_default_multi", []) + else: + default = getattr(self, "_default_multi", None) + else: + default = [] else: default = None elif is_multi and isinstance(default, tuple):