From f7aad40ee731e37801b6a9db33f5f8a158a9321d Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Wed, 4 Mar 2026 08:38:39 +0100 Subject: [PATCH] fix: better support for identifier calculation --- src/rougail/annotator/variable.py | 4 +++ src/rougail/convert/convert.py | 21 ++++++++++--- src/rougail/convert/object_model.py | 10 +++--- src/rougail/utils.py | 6 +++- .../makedict/after.json | 31 +++++++++++++++++++ .../makedict/base.json | 16 ++++++++++ .../makedict/before.json | 31 +++++++++++++++++++ .../makedict/mandatory.json | 1 + .../makedict/read_write.json | 16 ++++++++++ .../tiramisu/base.py | 19 ++++++++++++ .../tiramisu/no_namespace.py | 18 +++++++++++ 11 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/after.json create mode 100644 tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/base.json create mode 100644 tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/before.json create mode 100644 tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/mandatory.json create mode 100644 tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/read_write.json create mode 100644 tests/dictionaries/60_5family_dynamic_calc_identifier_multi/tiramisu/base.py create mode 100644 tests/dictionaries/60_5family_dynamic_calc_identifier_multi/tiramisu/no_namespace.py diff --git a/src/rougail/annotator/variable.py b/src/rougail/annotator/variable.py index 8fef48ca5..25c73f2ad 100644 --- a/src/rougail/annotator/variable.py +++ b/src/rougail/annotator/variable.py @@ -248,10 +248,14 @@ class Annotator(Walk): # pylint: disable=R0903 ).format(variable.path) raise DictConsistencyError(msg, 75, variable.xmlfiles) self._convert_variable_multi(calculated_variable) + identifier_is_a_calculation = False + if isinstance(variable.default, Calculation): + identifier_is_a_calculation = isinstance(variable.default.identifier, Calculation) variable.multi = calc_multi_for_type_variable( variable, calculated_variable_path, calculated_variable, + identifier_is_a_calculation, self.objectspace, )[1] if ( diff --git a/src/rougail/convert/convert.py b/src/rougail/convert/convert.py index 0b6866db7..f0cf3741d 100644 --- a/src/rougail/convert/convert.py +++ b/src/rougail/convert/convert.py @@ -248,7 +248,7 @@ class ParserVariable: set(hint) - {"name", "path", "xmlfiles"} | {"redefine", "exists"} ) self.variable_calculations = self.search_calculation( # pylint: disable=W0201 - hint + hint, variable=True, ) self.is_init = True @@ -969,6 +969,9 @@ class ParserVariable: return value["type"] in CALCULATION_TYPES # auto set type typ = set(CALCULATION_TYPES) & set(value) + # XXX variable could have identifier + if typ == {"variable", "identifier"}: + typ = {"variable"} # XXX variable is also set to information if typ == {"variable", "information"}: typ = {"information"} @@ -1002,7 +1005,12 @@ class ParserVariable: # if attribute == "default" and "identifier" in calculation_object: identifier = calculation_object["identifier"] - if isinstance(identifier, dict): + if isinstance(identifier, dict) and self.is_calculation( + "identifier", + identifier, + self.variable_calculations, + inside_list, + ): self.set_calculation(calculation_object, "identifier", identifier, path, family_is_dynamic, xmlfiles, inside_list=inside_list, index=index) if "params" in calculation_object: if not isinstance(calculation_object["params"], dict): @@ -1091,10 +1099,15 @@ class RougailConvert(ParserVariable): def search_calculation( self, hint: dict, + variable: bool=False, ) -> Tuple[List[Any], List[Any]]: """attribute is calculated if typing is like: Union[Calculation, xxx]""" - inside_list = [] - outside_list = [] + if variable: + inside_list = ["identifier"] + outside_list = ["identifier"] + else: + inside_list = [] + outside_list = [] for key, value in hint.items(): if "Union" in value.__class__.__name__ and ( Calculation in value.__args__ or VariableCalculation in value.__args__ diff --git a/src/rougail/convert/object_model.py b/src/rougail/convert/object_model.py index e4e697b8c..e1c511f31 100644 --- a/src/rougail/convert/object_model.py +++ b/src/rougail/convert/object_model.py @@ -575,10 +575,9 @@ class _VariableCalculation(Calculation): params["__default_value"] = self.default_values if self.allow_none: params["allow_none"] = True - if not identifier_is_a_calculation: - self.check_multi( - objectspace, path, variable_in_calculation_path, variable_in_calculation, - ) + self.check_multi( + objectspace, path, variable_in_calculation_path, variable_in_calculation, identifier_is_a_calculation, + ) if path in objectspace.followers: multi = objectspace.multis[path] == "submulti" else: @@ -588,7 +587,7 @@ class _VariableCalculation(Calculation): return params def check_multi( - self, objectspace, path, variable_in_calculation_path, variable_in_calculation + self, objectspace, path, variable_in_calculation_path, variable_in_calculation, identifier_is_a_calculation, ): local_variable = objectspace.paths[path] local_variable_multi, variable_in_calculation_multi = ( @@ -596,6 +595,7 @@ class _VariableCalculation(Calculation): local_variable, variable_in_calculation_path, variable_in_calculation, + identifier_is_a_calculation, objectspace, ) ) diff --git a/src/rougail/utils.py b/src/rougail/utils.py index 007492462..6e287b8f1 100644 --- a/src/rougail/utils.py +++ b/src/rougail/utils.py @@ -157,6 +157,7 @@ def calc_multi_for_type_variable( local_variable: "Variable", variable_in_calculation_path: str, variable_in_calculation: "Variable", + identifier_is_a_calculation: bool, objectspace: "RougailConvert", ) -> Union[bool, str]: variable_in_calculation_multi = variable_in_calculation.multi @@ -192,7 +193,10 @@ def calc_multi_for_type_variable( common_variable_path = variable_in_calculation_path if common_path: common_variable_path = common_variable_path[len(common_path) + 1 :] - count_identifiers = common_variable_path.count("{{ identifier }}") + if identifier_is_a_calculation: + count_identifiers = 0 + else: + count_identifiers = common_variable_path.count("{{ identifier }}") if count_identifiers == 1: if variable_in_calculation_multi is False: variable_in_calculation_multi = True diff --git a/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/after.json b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/after.json new file mode 100644 index 000000000..7aed314c2 --- /dev/null +++ b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/after.json @@ -0,0 +1,31 @@ +{ + "rougail.var1": { + "owner": "default", + "value": [ + "val1", + "val2" + ] + }, + "rougail.var2": { + "owner": "default", + "value": "val1" + }, + "rougail.dynval1.var": { + "owner": "default", + "value": [ + "val1" + ] + }, + "rougail.dynval2.var": { + "owner": "default", + "value": [ + "val2" + ] + }, + "rougail.var3": { + "owner": "default", + "value": [ + "val1" + ] + } +} diff --git a/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/base.json b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/base.json new file mode 100644 index 000000000..895253b9c --- /dev/null +++ b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/base.json @@ -0,0 +1,16 @@ +{ + "rougail.var1": [ + "val1", + "val2" + ], + "rougail.var2": "val1", + "rougail.dynval1.var": [ + "val1" + ], + "rougail.dynval2.var": [ + "val2" + ], + "rougail.var3": [ + "val1" + ] +} diff --git a/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/before.json b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/before.json new file mode 100644 index 000000000..7aed314c2 --- /dev/null +++ b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/before.json @@ -0,0 +1,31 @@ +{ + "rougail.var1": { + "owner": "default", + "value": [ + "val1", + "val2" + ] + }, + "rougail.var2": { + "owner": "default", + "value": "val1" + }, + "rougail.dynval1.var": { + "owner": "default", + "value": [ + "val1" + ] + }, + "rougail.dynval2.var": { + "owner": "default", + "value": [ + "val2" + ] + }, + "rougail.var3": { + "owner": "default", + "value": [ + "val1" + ] + } +} diff --git a/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/mandatory.json b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/mandatory.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/mandatory.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/read_write.json b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/read_write.json new file mode 100644 index 000000000..895253b9c --- /dev/null +++ b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/makedict/read_write.json @@ -0,0 +1,16 @@ +{ + "rougail.var1": [ + "val1", + "val2" + ], + "rougail.var2": "val1", + "rougail.dynval1.var": [ + "val1" + ], + "rougail.dynval2.var": [ + "val2" + ], + "rougail.var3": [ + "val1" + ] +} diff --git a/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/tiramisu/base.py b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/tiramisu/base.py new file mode 100644 index 000000000..61164374e --- /dev/null +++ b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/tiramisu/base.py @@ -0,0 +1,19 @@ +from tiramisu import * +from tiramisu.setting import ALLOWED_LEADER_PROPERTIES +from re import compile as re_compile +from rougail.tiramisu import func, dict_env, load_functions, ConvertDynOptionDescription +load_functions('../rougail-tests/funcs/test.py') +try: + groups.namespace +except: + groups.addgroup('namespace') +ALLOWED_LEADER_PROPERTIES.add("basic") +ALLOWED_LEADER_PROPERTIES.add("standard") +ALLOWED_LEADER_PROPERTIES.add("advanced") +option_2 = StrOption(name="var1", doc="A suffix variable", multi=True, default=["val1", "val2"], default_multi="val1", properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier_multi/rougail/00-base.yml'], 'type': 'string'}) +option_3 = StrOption(name="var2", doc="A suffix variable2", default="val1", properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier_multi/rougail/00-base.yml'], 'type': 'string'}) +option_5 = StrOption(name="var", doc="A dynamic variable", multi=True, default=[Calculation(func['calc_value'], Params((ParamIdentifier())))], default_multi=Calculation(func['calc_value'], Params((ParamIdentifier()))), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier_multi/rougail/00-base.yml'], 'type': 'string'}) +optiondescription_4 = ConvertDynOptionDescription(name="dyn{{ identifier }}", doc="dyn{{ identifier }}", identifiers=Calculation(func['calc_value'], Params((ParamOption(option_2)))), children=[option_5], properties=frozenset({"standard"}), informations={'dynamic_variable': 'rougail.var1', 'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier_multi/rougail/00-base.yml']}) +option_6 = StrOption(name="var3", doc="A variable calculated", multi=True, default=Calculation(func['calc_value'], Params((ParamDynOption(option_5, Calculation(func['calc_value'], Params((ParamOption(option_3)), kwargs={'__internal_multi': ParamValue(True)})))), kwargs={'__internal_multi': ParamValue(True)})), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier_multi/rougail/00-base.yml'], 'type': 'string'}) +optiondescription_1 = OptionDescription(name="rougail", doc="Rougail", group_type=groups.namespace, children=[option_2, option_3, optiondescription_4, option_6], properties=frozenset({"standard"})) +option_0 = OptionDescription(name="baseoption", doc="baseoption", children=[optiondescription_1]) diff --git a/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/tiramisu/no_namespace.py b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/tiramisu/no_namespace.py new file mode 100644 index 000000000..3938ecc42 --- /dev/null +++ b/tests/dictionaries/60_5family_dynamic_calc_identifier_multi/tiramisu/no_namespace.py @@ -0,0 +1,18 @@ +from tiramisu import * +from tiramisu.setting import ALLOWED_LEADER_PROPERTIES +from re import compile as re_compile +from rougail.tiramisu import func, dict_env, load_functions, ConvertDynOptionDescription +load_functions('../rougail-tests/funcs/test.py') +try: + groups.namespace +except: + groups.addgroup('namespace') +ALLOWED_LEADER_PROPERTIES.add("basic") +ALLOWED_LEADER_PROPERTIES.add("standard") +ALLOWED_LEADER_PROPERTIES.add("advanced") +option_1 = StrOption(name="var1", doc="A suffix variable", multi=True, default=["val1", "val2"], default_multi="val1", properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier_multi/rougail/00-base.yml'], 'type': 'string'}) +option_2 = StrOption(name="var2", doc="A suffix variable2", default="val1", properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier_multi/rougail/00-base.yml'], 'type': 'string'}) +option_4 = StrOption(name="var", doc="A dynamic variable", multi=True, default=[Calculation(func['calc_value'], Params((ParamIdentifier())))], default_multi=Calculation(func['calc_value'], Params((ParamIdentifier()))), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier_multi/rougail/00-base.yml'], 'type': 'string'}) +optiondescription_3 = ConvertDynOptionDescription(name="dyn{{ identifier }}", doc="dyn{{ identifier }}", identifiers=Calculation(func['calc_value'], Params((ParamOption(option_1)))), children=[option_4], properties=frozenset({"standard"}), informations={'dynamic_variable': 'var1', 'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier_multi/rougail/00-base.yml']}) +option_5 = StrOption(name="var3", doc="A variable calculated", multi=True, default=Calculation(func['calc_value'], Params((ParamDynOption(option_4, Calculation(func['calc_value'], Params((ParamOption(option_2)), kwargs={'__internal_multi': ParamValue(True)})))), kwargs={'__internal_multi': ParamValue(True)})), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_5family_dynamic_calc_identifier_multi/rougail/00-base.yml'], 'type': 'string'}) +option_0 = OptionDescription(name="baseoption", doc="baseoption", children=[option_1, option_2, optiondescription_3, option_5])