feat: property.get(uncalculated=True) is now possible for an dynoptiondescription without identifiers

This commit is contained in:
egarette@silique.fr 2025-10-15 09:11:33 +02:00
parent e8921795d3
commit e0f16b14c7
5 changed files with 69 additions and 14 deletions

View file

@ -13,7 +13,7 @@ from tiramisu import BoolOption, StrOption, ChoiceOption, IPOption, \
Config, \ Config, \
Params, ParamOption, ParamValue, ParamIdentifier, ParamSelfOption, ParamDynOption, ParamIndex, ParamSelfInformation, ParamInformation, \ Params, ParamOption, ParamValue, ParamIdentifier, ParamSelfOption, ParamDynOption, ParamIndex, ParamSelfInformation, ParamInformation, \
Calculation, calc_value 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: def display_name(kls, subconfig, with_quote=False) -> str:
@ -317,6 +317,20 @@ def test_prop_dyndescription():
# assert not list_sessions() # 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(): def test_prop_dyndescription_force_store_value():
st = StrOption('st', '', properties=('force_store_value',)) st = StrOption('st', '', properties=('force_store_value',))
dod = DynOptionDescription('dod', '', [st], identifiers=Calculation(return_list)) dod = DynOptionDescription('dod', '', [st], identifiers=Calculation(return_list))
@ -451,6 +465,30 @@ def test_callback_dyndescription_outside_optional():
# assert not list_sessions() # 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(): def test_callback_dyndescription_subdyn():
lst = StrOption('lst', '', ['val1', 'val2'], multi=True) lst = StrOption('lst', '', ['val1', 'val2'], multi=True)
st = StrOption('st', '', 'val1') st = StrOption('st', '', 'val1')

View file

@ -26,6 +26,7 @@ from .error import (
LeadershipError, LeadershipError,
ValueErrorWarning, ValueErrorWarning,
PropertiesOptionError, PropertiesOptionError,
AttributeOptionError,
) )
from .i18n import _ from .i18n import _
from .setting import ( from .setting import (
@ -123,7 +124,6 @@ class TiramisuHelp:
class CommonTiramisu(TiramisuHelp): class CommonTiramisu(TiramisuHelp):
_validate_properties = True _validate_properties = True
_allow_dynoption = False
def _set_subconfig(self) -> None: def _set_subconfig(self) -> None:
if not self._subconfig: if not self._subconfig:
@ -133,7 +133,7 @@ class CommonTiramisu(TiramisuHelp):
self._path, self._path,
self._index, self._index,
validate_properties=False, validate_properties=False,
allow_dynoption=self._allow_dynoption, allow_dynoption=True,
) )
except AssertionError as err: except AssertionError as err:
raise ConfigError(str(err)) raise ConfigError(str(err))
@ -149,8 +149,6 @@ def option_type(typ):
@wraps(func) @wraps(func)
def wrapped(*args, **kwargs): def wrapped(*args, **kwargs):
self = args[0] self = args[0]
if isinstance(typ, list) and "allow_dynoption" in typ:
self._allow_dynoption = True
config_bag = self._config_bag config_bag = self._config_bag
if self._config_bag.context.impl_type == "group" and "group" in types: if self._config_bag.context.impl_type == "group" and "group" in types:
options_bag = [ options_bag = [
@ -164,6 +162,9 @@ def option_type(typ):
kwargs["is_group"] = True kwargs["is_group"] = True
return func(self, options_bag, *args[1:], **kwargs) return func(self, options_bag, *args[1:], **kwargs)
self._set_subconfig() 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 option = self._subconfig.option
error_type = None error_type = None
if "dynamic" in types: if "dynamic" in types:
@ -668,7 +669,7 @@ class TiramisuOptionProperty(CommonTiramisuOption):
_validate_properties = False _validate_properties = False
@option_type(["option", "optiondescription", "with_index", "symlink"]) @option_type(["option", "optiondescription", "with_index", "symlink", "allow_dynoption"])
def get( def get(
self, self,
*, *,
@ -677,6 +678,8 @@ class TiramisuOptionProperty(CommonTiramisuOption):
uncalculated: bool = False, uncalculated: bool = False,
): ):
"""Get properties for an option""" """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() settings = self._config_bag.context.get_settings()
if not only_raises: if not only_raises:
return settings.getproperties( return settings.getproperties(
@ -797,9 +800,8 @@ class TiramisuOptionInformation(CommonTiramisuOption):
"""Manage option's informations""" """Manage option's informations"""
_validate_properties = False _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( def get(
self, self,
name: str, name: str,
@ -812,7 +814,7 @@ class TiramisuOptionInformation(CommonTiramisuOption):
default, default,
) )
@option_type(["option", "optiondescription"]) @option_type(["option", "optiondescription", "allow_dynoption"])
def set(self, key: str, value: Any) -> None: def set(self, key: str, value: Any) -> None:
"""Set information""" """Set information"""
self._config_bag.context.get_values().set_information( self._config_bag.context.get_values().set_information(
@ -821,7 +823,7 @@ class TiramisuOptionInformation(CommonTiramisuOption):
value, value,
) )
@option_type(["option", "optiondescription"]) @option_type(["option", "optiondescription", "allow_dynoption"])
def remove( def remove(
self, self,
key: str, key: str,
@ -832,7 +834,7 @@ class TiramisuOptionInformation(CommonTiramisuOption):
path=self._path, 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: def list(self) -> list:
"""List information's keys""" """List information's keys"""
lst1 = set(self._subconfig.option._list_information()) lst1 = set(self._subconfig.option._list_information())
@ -1106,7 +1108,6 @@ class TiramisuOption(
self._path = path self._path = path
self._index = index self._index = index
self._config_bag = config_bag self._config_bag = config_bag
self._allow_dynoption = allow_dynoption
self._subconfig = subconfig self._subconfig = subconfig
if not self._registers: if not self._registers:
_registers(self._registers, "TiramisuOption") _registers(self._registers, "TiramisuOption")

View file

@ -25,7 +25,7 @@ from copy import copy, deepcopy
from typing import Optional, List, Any, Union from typing import Optional, List, Any, Union
from os.path import commonprefix 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 .option import DynOptionDescription, Leadership, Option
from .setting import ConfigBag, Settings, undefined, groups from .setting import ConfigBag, Settings, undefined, groups
from .value import Values, owners from .value import Values, owners
@ -237,6 +237,7 @@ class SubConfig:
"apply_requires", "apply_requires",
"transitive_properties", "transitive_properties",
"is_dynamic", "is_dynamic",
"is_dynamic_without_identifiers",
"identifiers", "identifiers",
"_length", "_length",
) )
@ -254,6 +255,7 @@ class SubConfig:
# for python 3.9 properties: Union[list[str], undefined] = undefined, # for python 3.9 properties: Union[list[str], undefined] = undefined,
properties=undefined, properties=undefined,
validate_properties: bool = True, validate_properties: bool = True,
check_dynamic_without_identifiers: bool = True,
) -> None: ) -> None:
self.index = index self.index = index
self.identifiers = identifiers self.identifiers = identifiers
@ -269,10 +271,17 @@ class SubConfig:
) )
self.apply_requires = not is_follower or index is not None self.apply_requires = not is_follower or index is not None
self.true_path = true_path 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 = 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: else:
self.is_dynamic = False self.is_dynamic = False
self.is_dynamic_without_identifiers = False
self._properties = properties self._properties = properties
if validate_properties: if validate_properties:
if self.path and self._properties is undefined: if self.path and self._properties is undefined:
@ -433,6 +442,7 @@ class SubConfig:
check_index: bool = True, check_index: bool = True,
config_bag: ConfigBag = None, config_bag: ConfigBag = None,
true_path: Optional[str] = None, true_path: Optional[str] = None,
check_dynamic_without_identifiers: bool = True,
) -> "SubConfig": ) -> "SubConfig":
# pylint: disable=too-many-branches,too-many-locals,too-many-arguments # pylint: disable=too-many-branches,too-many-locals,too-many-arguments
if config_bag is None: if config_bag is None:
@ -461,6 +471,7 @@ class SubConfig:
properties=properties, properties=properties,
validate_properties=validate_properties, validate_properties=validate_properties,
true_path=true_path, true_path=true_path,
check_dynamic_without_identifiers=check_dynamic_without_identifiers,
) )
if check_index and index is not None: if check_index and index is not None:
if option.impl_is_optiondescription() or not option.impl_is_follower(): if option.impl_is_optiondescription() or not option.impl_is_follower():
@ -512,6 +523,7 @@ class SubConfig:
search_option: "BaseOption", search_option: "BaseOption",
true_path: Optional[str] = None, true_path: Optional[str] = None,
validate_properties: bool = True, validate_properties: bool = True,
check_dynamic_without_identifiers: bool = True,
): ):
current_option_path = self.option.impl_getpath() current_option_path = self.option.impl_getpath()
search_option_path = search_option.impl_getpath() search_option_path = search_option.impl_getpath()
@ -571,6 +583,7 @@ class SubConfig:
None, None,
validate_properties, validate_properties,
true_path=true_path, true_path=true_path,
check_dynamic_without_identifiers=check_dynamic_without_identifiers,
) )
) )
parents = new_parents parents = new_parents
@ -581,6 +594,7 @@ class SubConfig:
search_option, search_option,
index, index,
validate_properties, validate_properties,
check_dynamic_without_identifiers=check_dynamic_without_identifiers,
) )
) )
if subconfigs_is_a_list: if subconfigs_is_a_list:

View file

@ -115,6 +115,7 @@ class DynOptionDescription(OptionDescription):
None, None,
False, False,
properties=None, properties=None,
check_dynamic_without_identifiers=False,
) )
identifiers = self._identifiers identifiers = self._identifiers
if isinstance(identifiers, list): if isinstance(identifiers, list):

View file

@ -395,6 +395,7 @@ class Values:
woption(), woption(),
true_path=subconfig.path, true_path=subconfig.path,
validate_properties=False, validate_properties=False,
check_dynamic_without_identifiers=False,
) )
if not isinstance(options, list): if not isinstance(options, list):
options = [options] options = [options]