feat: better identifiers retrieve

This commit is contained in:
egarette@silique.fr 2026-04-29 15:25:56 +02:00
parent f3a7344b58
commit 6d56e25524
7 changed files with 157 additions and 62 deletions

View file

@ -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

View file

@ -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"]
#

View file

@ -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):

View file

@ -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 = []
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
subconfig = subconfig.parent
for identifier in self._subconfig.identifiers:
if identifier is None:
continue
identifiers.append(dynconfig.option.convert_identifier_to_path(identifier))
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(

View file

@ -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 identifier is undefined:
if not identifiers:
identifier = None
else:
identifier = identifiers.pop(0)
if not identifier:
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()

View file

@ -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,8 +343,7 @@ 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:
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,
@ -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(

View file

@ -227,6 +227,12 @@ class Option(BaseOption):
default = getattr(self, "_default", undefined)
if default is undefined:
if is_multi:
if self.impl_is_follower():
if submulti:
default = getattr(self, "_default_multi", [])
else:
default = getattr(self, "_default_multi", None)
else:
default = []
else:
default = None