From e0f16b14c7c802dc9ed4fdab0756bc619331f7b2 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Wed, 15 Oct 2025 09:11:33 +0200 Subject: [PATCH] feat: property.get(uncalculated=True) is now possible for an dynoptiondescription without identifiers --- tests/test_dyn_optiondescription.py | 40 ++++++++++++++++++++++++- tiramisu/api.py | 23 +++++++------- tiramisu/config.py | 18 +++++++++-- tiramisu/option/dynoptiondescription.py | 1 + tiramisu/value.py | 1 + 5 files changed, 69 insertions(+), 14 deletions(-) diff --git a/tests/test_dyn_optiondescription.py b/tests/test_dyn_optiondescription.py index 8ac04fe..fc4f540 100644 --- a/tests/test_dyn_optiondescription.py +++ b/tests/test_dyn_optiondescription.py @@ -13,7 +13,7 @@ from tiramisu import BoolOption, StrOption, ChoiceOption, IPOption, \ Config, \ Params, ParamOption, ParamValue, ParamIdentifier, ParamSelfOption, ParamDynOption, ParamIndex, ParamSelfInformation, ParamInformation, \ Calculation, calc_value -from tiramisu.error import PropertiesOptionError, ConfigError, ConflictError, ValueOptionError +from tiramisu.error import PropertiesOptionError, ConfigError, ConflictError, ValueOptionError, AttributeOptionError def display_name(kls, subconfig, with_quote=False) -> str: @@ -317,6 +317,20 @@ def test_prop_dyndescription(): # assert not list_sessions() +def test_prop_dyndescription_uncalculated(): + st = StrOption('st', '', properties=('test',)) + od = OptionDescription('od', '', [st], properties=('test_od',)) + dod = DynOptionDescription('dod', '', [od], identifiers=Calculation(return_list)) + od2 = OptionDescription('od', '', [dod]) + cfg = Config(od2) + assert set(cfg.option('dod.od').property.get(uncalculated=True)) == {'test_od'} + assert set(cfg.option('dod.od.st').property.get(uncalculated=True)) == {'test', 'validator'} + with pytest.raises(AttributeOptionError): + set(cfg.option('dod.od').property.get()) + with pytest.raises(AttributeOptionError): + set(cfg.option('dod.od.st').property.get()) + + def test_prop_dyndescription_force_store_value(): st = StrOption('st', '', properties=('force_store_value',)) dod = DynOptionDescription('dod', '', [st], identifiers=Calculation(return_list)) @@ -451,6 +465,30 @@ def test_callback_dyndescription_outside_optional(): # assert not list_sessions() +def test_dyndescription_subdyn(): + lst = StrOption('lst', '', ['val1', 'val2'], multi=True) + st = StrOption('st', '', 'val1') + dod = DynOptionDescription('dod', '', [st], identifiers=Calculation(return_list)) + out = StrOption('out', '', Calculation(return_dynval, Params(ParamDynOption(st, ['val1', None]))), multi=True, properties=('notunique',)) + dod2 = DynOptionDescription('dod2', '', [dod, out], identifiers=Calculation(return_list)) + od = OptionDescription('od', '', [dod2]) + od2 = OptionDescription('od', '', [od, lst]) + cfg = Config(od2) + cfg.property.read_write() + assert cfg.option('od.dod2val1.dodval1.st').value.get() == 'val1' + with pytest.raises(AttributeOptionError): + cfg.option('od.dod2.dodval1.st').value.get() + with pytest.raises(AttributeOptionError): + cfg.option('od.dod2val1.dod.st').value.get() + 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): + cfg.option('od.dod2val1.dod.st').property.get(uncalculated=True) + + def test_callback_dyndescription_subdyn(): lst = StrOption('lst', '', ['val1', 'val2'], multi=True) st = StrOption('st', '', 'val1') diff --git a/tiramisu/api.py b/tiramisu/api.py index c078e01..751466f 100644 --- a/tiramisu/api.py +++ b/tiramisu/api.py @@ -26,6 +26,7 @@ from .error import ( LeadershipError, ValueErrorWarning, PropertiesOptionError, + AttributeOptionError, ) from .i18n import _ from .setting import ( @@ -123,7 +124,6 @@ class TiramisuHelp: class CommonTiramisu(TiramisuHelp): _validate_properties = True - _allow_dynoption = False def _set_subconfig(self) -> None: if not self._subconfig: @@ -133,7 +133,7 @@ class CommonTiramisu(TiramisuHelp): self._path, self._index, validate_properties=False, - allow_dynoption=self._allow_dynoption, + allow_dynoption=True, ) except AssertionError as err: raise ConfigError(str(err)) @@ -149,8 +149,6 @@ def option_type(typ): @wraps(func) def wrapped(*args, **kwargs): self = args[0] - if isinstance(typ, list) and "allow_dynoption" in typ: - self._allow_dynoption = True config_bag = self._config_bag if self._config_bag.context.impl_type == "group" and "group" in types: options_bag = [ @@ -164,6 +162,9 @@ def option_type(typ): kwargs["is_group"] = True return func(self, options_bag, *args[1:], **kwargs) self._set_subconfig() + if (not isinstance(typ, list) or "allow_dynoption" not in typ) and self._subconfig.is_dynamic_without_identifiers: + raise AttributeOptionError(self._subconfig.path, "option-dynamic") + option = self._subconfig.option error_type = None if "dynamic" in types: @@ -668,7 +669,7 @@ class TiramisuOptionProperty(CommonTiramisuOption): _validate_properties = False - @option_type(["option", "optiondescription", "with_index", "symlink"]) + @option_type(["option", "optiondescription", "with_index", "symlink", "allow_dynoption"]) def get( self, *, @@ -677,6 +678,8 @@ class TiramisuOptionProperty(CommonTiramisuOption): uncalculated: bool = False, ): """Get properties for an option""" + if self._subconfig.is_dynamic_without_identifiers and not uncalculated: + raise AttributeOptionError(self._subconfig.path, "option-dynamic") settings = self._config_bag.context.get_settings() if not only_raises: return settings.getproperties( @@ -797,9 +800,8 @@ class TiramisuOptionInformation(CommonTiramisuOption): """Manage option's informations""" _validate_properties = False - _allow_dynoption = True - @option_type(["option", "optiondescription", "with_or_without_index", "symlink"]) + @option_type(["option", "optiondescription", "with_or_without_index", "symlink", "allow_dynoption"]) def get( self, name: str, @@ -812,7 +814,7 @@ class TiramisuOptionInformation(CommonTiramisuOption): default, ) - @option_type(["option", "optiondescription"]) + @option_type(["option", "optiondescription", "allow_dynoption"]) def set(self, key: str, value: Any) -> None: """Set information""" self._config_bag.context.get_values().set_information( @@ -821,7 +823,7 @@ class TiramisuOptionInformation(CommonTiramisuOption): value, ) - @option_type(["option", "optiondescription"]) + @option_type(["option", "optiondescription", "allow_dynoption"]) def remove( self, key: str, @@ -832,7 +834,7 @@ class TiramisuOptionInformation(CommonTiramisuOption): path=self._path, ) - @option_type(["option", "optiondescription", "with_or_without_index", "symlink"]) + @option_type(["option", "optiondescription", "with_or_without_index", "symlink", "allow_dynoption"]) def list(self) -> list: """List information's keys""" lst1 = set(self._subconfig.option._list_information()) @@ -1106,7 +1108,6 @@ class TiramisuOption( self._path = path self._index = index self._config_bag = config_bag - self._allow_dynoption = allow_dynoption self._subconfig = subconfig if not self._registers: _registers(self._registers, "TiramisuOption") diff --git a/tiramisu/config.py b/tiramisu/config.py index d8fec1f..05b9344 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -25,7 +25,7 @@ from copy import copy, deepcopy from typing import Optional, List, Any, Union from os.path import commonprefix -from .error import PropertiesOptionError, ConfigError, ConflictError, LeadershipError +from .error import PropertiesOptionError, ConfigError, ConflictError, LeadershipError, AttributeOptionError from .option import DynOptionDescription, Leadership, Option from .setting import ConfigBag, Settings, undefined, groups from .value import Values, owners @@ -237,6 +237,7 @@ class SubConfig: "apply_requires", "transitive_properties", "is_dynamic", + "is_dynamic_without_identifiers", "identifiers", "_length", ) @@ -254,6 +255,7 @@ class SubConfig: # for python 3.9 properties: Union[list[str], undefined] = undefined, properties=undefined, validate_properties: bool = True, + check_dynamic_without_identifiers: bool = True, ) -> None: self.index = index self.identifiers = identifiers @@ -269,10 +271,17 @@ class SubConfig: ) self.apply_requires = not is_follower or index is not None self.true_path = true_path - if parent and parent.is_dynamic or self.option.impl_is_dynoptiondescription(): + if self.option.impl_is_dynoptiondescription(): self.is_dynamic = True + self.is_dynamic_without_identifiers = identifiers is None or (parent and identifiers == parent.identifiers) + if check_dynamic_without_identifiers and parent and parent.is_dynamic and self.is_dynamic_without_identifiers != parent.is_dynamic_without_identifiers: + raise AttributeOptionError(true_path, "option-dynamic") + elif parent: + self.is_dynamic = parent.is_dynamic + self.is_dynamic_without_identifiers = parent.is_dynamic_without_identifiers else: self.is_dynamic = False + self.is_dynamic_without_identifiers = False self._properties = properties if validate_properties: if self.path and self._properties is undefined: @@ -433,6 +442,7 @@ class SubConfig: check_index: bool = True, config_bag: ConfigBag = None, true_path: Optional[str] = None, + check_dynamic_without_identifiers: bool = True, ) -> "SubConfig": # pylint: disable=too-many-branches,too-many-locals,too-many-arguments if config_bag is None: @@ -461,6 +471,7 @@ class SubConfig: properties=properties, validate_properties=validate_properties, true_path=true_path, + check_dynamic_without_identifiers=check_dynamic_without_identifiers, ) if check_index and index is not None: if option.impl_is_optiondescription() or not option.impl_is_follower(): @@ -512,6 +523,7 @@ class SubConfig: search_option: "BaseOption", true_path: Optional[str] = None, validate_properties: bool = True, + check_dynamic_without_identifiers: bool = True, ): current_option_path = self.option.impl_getpath() search_option_path = search_option.impl_getpath() @@ -571,6 +583,7 @@ class SubConfig: None, validate_properties, true_path=true_path, + check_dynamic_without_identifiers=check_dynamic_without_identifiers, ) ) parents = new_parents @@ -581,6 +594,7 @@ class SubConfig: search_option, index, validate_properties, + check_dynamic_without_identifiers=check_dynamic_without_identifiers, ) ) if subconfigs_is_a_list: diff --git a/tiramisu/option/dynoptiondescription.py b/tiramisu/option/dynoptiondescription.py index cf74527..5dc8beb 100644 --- a/tiramisu/option/dynoptiondescription.py +++ b/tiramisu/option/dynoptiondescription.py @@ -115,6 +115,7 @@ class DynOptionDescription(OptionDescription): None, False, properties=None, + check_dynamic_without_identifiers=False, ) identifiers = self._identifiers if isinstance(identifiers, list): diff --git a/tiramisu/value.py b/tiramisu/value.py index 752571e..aaa3462 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -395,6 +395,7 @@ class Values: woption(), true_path=subconfig.path, validate_properties=False, + check_dynamic_without_identifiers=False, ) if not isinstance(options, list): options = [options]