From a3b2699289acd97173a1adcb57b3bf8f3d17e990 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Wed, 18 Jun 2025 07:36:49 +0300 Subject: [PATCH] feat: separate rougail and rougail-base --- pyproject.toml | 46 +- rougail-base-pyproject.toml | 52 + rougail-pyproject.toml | 35 + src/rougail/__init__.py | 58 +- src/rougail/annotator/family.py | 2 +- src/rougail/annotator/property.py | 2 +- src/rougail/annotator/value.py | 2 +- src/rougail/annotator/variable.py | 2 +- src/rougail/{config.py => config/__init__.py} | 15 +- src/rougail/convert.py | 1166 ----------------- src/rougail/convert/__init__.py | 63 + src/rougail/{ => convert}/object_model.py | 8 +- src/rougail/{ => convert}/path.py | 4 +- .../{ => convert}/tiramisureflector.py | 8 +- src/rougail/error.py | 1 + src/rougail/structural_directory/__init__.py | 4 +- src/rougail/user_datas.py | 4 +- .../tiramisu/base.py | 17 + .../tiramisu/no_namespace.py | 12 + 19 files changed, 215 insertions(+), 1286 deletions(-) create mode 100644 rougail-base-pyproject.toml create mode 100644 rougail-pyproject.toml rename src/rougail/{config.py => config/__init__.py} (97%) delete mode 100644 src/rougail/convert.py create mode 100644 src/rougail/convert/__init__.py rename src/rougail/{ => convert}/object_model.py (99%) rename src/rougail/{ => convert}/path.py (99%) rename src/rougail/{ => convert}/tiramisureflector.py (99%) create mode 100644 tests/dictionaries/00_2default_calculated_variable_description_multi_line/tiramisu/base.py create mode 100644 tests/dictionaries/00_2default_calculated_variable_description_multi_line/tiramisu/no_namespace.py diff --git a/pyproject.toml b/pyproject.toml index 1b9ebb78b..2db49ba28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,47 +1,6 @@ -[build-system] -build-backend = "flit_core.buildapi" -requires = ["flit_core >=3.8.0,<4"] - [project] name = "rougail" version = "1.2.0a26" -authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}] -readme = "README.md" -description = "A consistency handling system that was initially designed in the configuration management" -requires-python = ">=3.8" -license = {file = "LICENSE"} -classifiers = [ - "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", - "Natural Language :: English", - "Natural Language :: French", - -] -dependencies = [ - "ruamel.yaml ~= 0.18.6", - "pydantic ~= 2.9.2", - "jinja2 ~= 3.1.4", - "tiramisu >=5.0,<6" -] - -[project.optional-dependencies] -dev = [ - "pylint ~= 3.0.3", - "pytest ~= 8.2.2", - "lxml ~= 5.2.2" -] - -[project.urls] -Home = "https://forge.cloud.silique.fr/stove/rougail" [tool.commitizen] name = "cz_conventional_commits" @@ -50,7 +9,10 @@ version_scheme = "pep440" version_provider = "pep621" version_files = [ "src/rougail/__version__.py", - "pyproject.toml:version" + "rougail-pyproject.toml:version", + "rougail-pyproject.toml:rougail-base == ", + "rougail-base-pyproject.toml:version", ] update_changelog_on_bump = true changelog_merge_prerelease = true + diff --git a/rougail-base-pyproject.toml b/rougail-base-pyproject.toml new file mode 100644 index 000000000..8e500cf4a --- /dev/null +++ b/rougail-base-pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +build-backend = "flit_core.buildapi" +requires = ["flit_core >=3.8.0,<4"] + +[project] +name = "rougail-base" +version = "1.2.0a26" +authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}] +readme = "README.md" +description = "A consistency handling system that was initially designed in the configuration management" +requires-python = ">=3.10" +license = {file = "LICENSE"} +classifiers = [ + "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + "Natural Language :: English", + "Natural Language :: French", + +] +dependencies = [ + "jinja2 ~= 3.1.4", + "tiramisu ~= 5.0" +] + +[project.optional-dependencies] +dev = [ + "pylint ~= 3.0.3", + "pytest ~= 8.2.2", +] + +[tool.flit.module] +name = "rougail" + +[tool.flit.sdist] +exclude = [ + "src/rougail/annotator", + "src/rougail/config", + "src/rougail/convert", + "src/rougail/structural_commandline", + "src/rougail/structural_directory", + "src/rougail/update", + ] + +[project.urls] +Home = "https://forge.cloud.silique.fr/stove/rougail" diff --git a/rougail-pyproject.toml b/rougail-pyproject.toml new file mode 100644 index 000000000..856c2de11 --- /dev/null +++ b/rougail-pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +build-backend = "flit_core.buildapi" +requires = ["flit_core >=3.8.0,<4"] + +[project] +name = "rougail" +version = "1.2.0a26" +authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}] +description = "A consistency handling system that was initially designed in the configuration management" +classifiers = [ + "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "Programming Language :: Python", + "Operating System :: OS Independent", + "Natural Language :: English", + "Natural Language :: French", + +] +dependencies = [ + "ruamel.yaml ~= 0.18.6", + "pydantic ~= 2.9.2", + "rougail-base == 1.2.0a26", +] + +[tool.flit.sdist] +exclude = [ + "src/rougail/error.py", + "src/rougail/i18n.py", + "src/rougail/tiramisu.py", + "src/rougail/user_datas.py", + "src/rougail/utils.py", + "src/rougail/__version__.py", + ] + +[project.urls] +Home = "https://forge.cloud.silique.fr/stove/rougail" diff --git a/src/rougail/__init__.py b/src/rougail/__init__.py index 60ba4d432..265b5936d 100644 --- a/src/rougail/__init__.py +++ b/src/rougail/__init__.py @@ -24,56 +24,10 @@ details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . """ - -from tiramisu import Config -from warnings import warn - -from .convert import RougailConvert -from .config import RougailConfig -from .utils import normalize_family -from .object_model import CONVERT_OPTION -from .user_datas import UserDatas -from .tiramisu import tiramisu_display_name from .__version__ import __version__ - - -class Rougail(UserDatas): - """Main Rougail object""" - - def __init__( - self, - rougailconfig=None, - ) -> None: - if rougailconfig is None: - rougailconfig = RougailConfig - self.rougailconfig = rougailconfig - self.converted = RougailConvert(self.rougailconfig) - self.config = None - - def run(self): - """Get Tiramisu Config""" - if not self.config: - tiram_obj = self.converted.save() - optiondescription = {} - custom_types = { - custom.__name__: custom - for custom in self.rougailconfig["custom_types"].values() - } - exec(tiram_obj, custom_types, optiondescription) # pylint: disable=W0122 - self.config = Config( - optiondescription["option_0"], - display_name=tiramisu_display_name, - ) - self.config.property.read_write() - return self.config - - def get_config(self): - warn( - "get_config is deprecated, use run instead", - DeprecationWarning, - stacklevel=2, - ) - return self.run() - - -__all__ = ("Rougail", "RougailConfig") +try: + from .convert import Rougail + from .config import RougailConfig + __all__ = ("Rougail", "RougailConfig", "__version__") +except ModuleNotFoundError as err: + __all__ = ("__version__",) diff --git a/src/rougail/annotator/family.py b/src/rougail/annotator/family.py index 7c1c8db4a..ca6427e11 100644 --- a/src/rougail/annotator/family.py +++ b/src/rougail/annotator/family.py @@ -29,7 +29,7 @@ from typing import Optional from rougail.i18n import _ from rougail.error import DictConsistencyError from rougail.annotator.variable import Walk -from rougail.object_model import VariableCalculation +from rougail.convert.object_model import VariableCalculation class Mode: # pylint: disable=R0903 diff --git a/src/rougail/annotator/property.py b/src/rougail/annotator/property.py index af883c60b..cb834b6e5 100644 --- a/src/rougail/annotator/property.py +++ b/src/rougail/annotator/property.py @@ -29,7 +29,7 @@ from typing import Union from rougail.i18n import _ from rougail.error import DictConsistencyError from rougail.annotator.variable import Walk -from rougail.object_model import Calculation +from rougail.convert.object_model import Calculation PROPERTIES = ( diff --git a/src/rougail/annotator/value.py b/src/rougail/annotator/value.py index b61c4b202..2c7f82443 100644 --- a/src/rougail/annotator/value.py +++ b/src/rougail/annotator/value.py @@ -29,7 +29,7 @@ from rougail.annotator.variable import Walk from rougail.i18n import _ from rougail.error import DictConsistencyError -from rougail.object_model import Calculation +from rougail.convert.object_model import Calculation class Annotator(Walk): # pylint: disable=R0903 diff --git a/src/rougail/annotator/variable.py b/src/rougail/annotator/variable.py index 55d4a5d89..eea7912e3 100644 --- a/src/rougail/annotator/variable.py +++ b/src/rougail/annotator/variable.py @@ -29,7 +29,7 @@ from tiramisu.error import display_list from rougail.i18n import _ from rougail.utils import calc_multi_for_type_variable from rougail.error import DictConsistencyError -from rougail.object_model import Calculation, VariableCalculation +from rougail.convert.object_model import Calculation, VariableCalculation class Walk: diff --git a/src/rougail/config.py b/src/rougail/config/__init__.py similarity index 97% rename from src/rougail/config.py rename to src/rougail/config/__init__.py index f77989abc..fad6c80b5 100644 --- a/src/rougail/config.py +++ b/src/rougail/config/__init__.py @@ -26,17 +26,15 @@ You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . """ -from sys import version_info - from pathlib import Path from tiramisu import Config from ruamel.yaml import YAML -from .utils import _, load_modules, normalize_family -from .convert import RougailConvert -from .object_model import get_convert_option_types +from ..utils import _, load_modules +from ..tiramisu import normalize_family +from ..convert import RougailConvert +from ..convert.object_model import get_convert_option_types -if version_info.major == 3 and version_info.minor: - import rougail.structural_commandline.object_model +#import rougail.structural_commandline.object_model RENAMED = { "dictionaries_dir": "main_dictionaries", @@ -53,7 +51,7 @@ def get_sub_modules(): global SUBMODULES if SUBMODULES is None: SUBMODULES = {} - for submodule in Path(__file__).parent.iterdir(): + for submodule in Path(__file__).parent.parent.iterdir(): if submodule.name.startswith("_") or not submodule.is_dir(): continue config_file = submodule / "config.py" @@ -181,6 +179,7 @@ class _RougailConfig: yield f"{option.path()}: {option.value.get()}" def __repr__(self): + print(self.config) self.config.property.read_write() try: values = "\n".join(self.parse(self.config)) diff --git a/src/rougail/convert.py b/src/rougail/convert.py deleted file mode 100644 index 2d0509fbe..000000000 --- a/src/rougail/convert.py +++ /dev/null @@ -1,1166 +0,0 @@ -"""Takes a bunch of Rougail YAML dispatched in differents folders -as an input and outputs a Tiramisu's file. - -Created by: -EOLE (http://eole.orion.education.fr) -Copyright (C) 2005-2018 - -Forked by: -Cadoles (http://www.cadoles.com) -Copyright (C) 2019-2021 - -Silique (https://www.silique.fr) -Copyright (C) 2022-2025 - -This program is free software: you can redistribute it and/or modify it -under the terms of the GNU Lesser General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -details. - -You should have received a copy of the GNU Lesser General Public License -along with this program. If not, see . -""" - -import logging -from pathlib import Path -from typing import ( - Any, - Dict, - Iterator, - List, - Literal, - Optional, - Tuple, - Union, - get_type_hints, -) - -from ruamel.yaml.comments import CommentedMap -from pydantic import ValidationError - -from warnings import warn - -from tiramisu.error import display_list - -from .annotator import SpaceAnnotator -from .i18n import _ -from .object_model import CONVERT_OPTION # Choice, -from .object_model import ( - PROPERTY_ATTRIBUTE, - CALCULATION_TYPES, - CALCULATION_PROPERTY_TYPES, - PARAM_TYPES, - AnyParam, - Calculation, - Dynamic, - Family, - SymLink, - Variable, - VariableCalculation, -) -from .tiramisureflector import TiramisuReflector -from .utils import load_modules -from .path import Paths -from .error import DictConsistencyError - -property_types = Union[Literal[True], Calculation] -properties_types = Dict[str, property_types] - - -class Property: - def __init__(self) -> None: - self._properties: Dict[str, properties_types] = {} - - def add( - self, - path: str, - property_: str, - value: property_types, - ) -> None: - self._properties.setdefault(path, {})[property_] = value - - def get(self, path: str) -> None: - return self._properties.setdefault(path, {}) - - def remove(self, path: str, property_: str) -> None: - del self._properties[path][property_] - - def __getitem__( - self, - path: str, - ) -> properties_types: - return self._properties.get(path, {}) - - def __contains__( - self, - path: str, - ) -> bool: - return path in self._properties - - -information_types = Dict[str, Union[str, int, float, bool]] - - -class Informations: - def __init__(self) -> None: - self._data: Dict[str, information_types] = {} - - def add( - self, - path: str, - key: str, - data: Any, - ) -> None: - if path not in self._data: - self._data[path] = {} - if key in self._data[path]: - raise Exception(f'an information "{key}" is already present in "{path}"') - self._data[path][key] = data - - def get( - self, - path: str, - ) -> information_types: - return self._data.get(path, {}) - - -class ParserVariable: - def __init__(self, rougailconfig): - self.rougailconfig = rougailconfig - self.load_config() - self.paths = Paths(None) - self.families = [] - self.variables = [] - self.parents = {".": []} - self.index = 0 - self.reflector_names = {} - self.leaders = [] - self.followers = [] - self.dynamics_variable = {} - self.multis = {} - self.default_multi = {} - self.jinja = {} - # - self.convert_options = list(CONVERT_OPTION) - self.convert_options.extend(self.custom_types) - # - self.exclude_imports = [] - self.informations = Informations() - self.properties = Property() - # self.choices = Appendable() - self.has_dyn_option = False - self.is_init = False - super().__init__() - - def load_config(self) -> None: - rougailconfig = self.rougailconfig - self.suffix = rougailconfig["suffix"] - self.default_dictionary_format_version = rougailconfig[ - "default_dictionary_format_version" - ] - self.custom_types = rougailconfig["custom_types"] - self.functions_files = rougailconfig["functions_files"] - self.modes_level = rougailconfig["modes_level"] - if self.modes_level: - self.default_variable_mode = rougailconfig["default_variable_mode"] - self.default_family_mode = rougailconfig["default_family_mode"] - self.extra_annotators = rougailconfig["extra_annotators"] - self.base_option_name = rougailconfig["base_option_name"] - self.export_with_import = rougailconfig["export_with_import"] - self.internal_functions = rougailconfig["internal_functions"] - self.force_optional = rougailconfig["force_optional"] - self.add_extra_options = rougailconfig[ - "structural_commandline.add_extra_options" - ] - self.structurals = rougailconfig["step.structural"] - try: - self.user_datas = rougailconfig["step.user_data"] - except: - self.user_datas = [] - try: - self.output = rougailconfig["step.output"] - except: - self.output = None - self.tiramisu_cache = rougailconfig["tiramisu_cache"] - self.load_unexist_redefine = rougailconfig["load_unexist_redefine"] - self.secret_pattern = rougailconfig["secret_manager.pattern"] - # change default initkwargs in CONVERT_OPTION - if hasattr(rougailconfig, "config"): - for sub_od in rougailconfig.config.option("default_params"): - for option in sub_od: - if option.owner.isdefault(): - continue - convert_option = CONVERT_OPTION[sub_od.name()] - if "initkwargs" not in convert_option: - convert_option["initkwargs"] = {} - convert_option["initkwargs"][option.name()] = option.value.get() - - def init(self): - if self.is_init: - return - variable = Variable - family = Family - root = Path(__file__).parent - self.walker = None - for structural_name in self.structurals: - structural = f"structural_{structural_name}" - module_path = root / structural / "__init__.py" - if not module_path.is_file(): - continue - module = load_modules(f"rougail.{structural}", str(module_path)) - if "Variable" in module.__all__: - variable = type( - variable.__name__ + "_" + structural, - (variable, module.Variable), - {}, - ) - if "Family" in module.__all__: - family = type( - family.__name__ + "_" + structural, (family, module.Family), {} - ) - if not self.walker and "Walker" in module.__all__: - self.walker = module.Walker - self.variable = variable - self.family = family - self.dynamic = type(Dynamic.__name__, (Dynamic, family), {}) - hint = get_type_hints(self.dynamic) - # FIXME: only for format 1.0 - self.family_types = hint["type"].__args__ # pylint: disable=W0201 - self.family_attrs = frozenset( # pylint: disable=W0201 - set(hint) - {"name", "path", "xmlfiles"} | {"redefine", "exists"} - ) - self.family_calculations = self.search_calculation( # pylint: disable=W0201 - hint - ) - # - hint = get_type_hints(self.variable) - self.variable_types = ( - self.convert_options - ) # hint["type"].__args__ # pylint: disable=W0201 - # - self.variable_attrs = frozenset( # pylint: disable=W0201 - set(hint) - {"name", "path", "xmlfiles"} | {"redefine", "exists"} - ) - self.variable_calculations = self.search_calculation( # pylint: disable=W0201 - hint - ) - self.is_init = True - - ############################################################################################### - # determine if the object is a family or a variable - ############################################################################################### - def is_family_or_variable( - self, - path: str, - obj: dict, - family_is_leadership: bool, - filename: str, - ) -> Literal["variable", "family"]: - """Check object to determine if it's a variable or a family""" - # it's already has a variable or a family - if path in self.paths: - if path in self.families: - return "family" - return "variable" - # it's: "my_variable:" - if not obj: - return "variable" - # check type attributes - obj_type = self.get_family_or_variable_type(obj) - if obj_type: - if obj_type in self.family_types: - return "family" - if obj_type in self.variable_types: - return "variable" - msg = _("unknown type {0} for {1}").format(obj_type, path) - raise DictConsistencyError(msg, 43, [filename]) - # in a leadership there is only variable - if family_is_leadership: - return "variable" - # all attributes are in variable object - # and values in attributes are not dict is not Calculation - if isinstance(obj, dict): - extra_keys = set(obj) - self.variable_attrs - if not extra_keys: - for key, value in obj.items(): - if ( - isinstance(value, dict) - and key != "params" - and not self.is_calculation( - key, - value, - self.variable_calculations, - False, - ) - ): - break - else: - return "variable" - else: - if self.version == "1.0": - msg = f'Invalid value for the variable "{path}": "{obj}"' - raise DictConsistencyError(msg, 102, [filename]) - return "variable" - return "family" - - def get_family_or_variable_type( - self, - obj: dict, - ) -> Optional[str]: - """Check 'type' attributes""" - if not isinstance(obj, dict): - return None - if "_type" in obj: - # only family has _type attributs - return obj["_type"] - if "type" in obj and isinstance(obj["type"], str): - return obj["type"] - return None - - ############################################################################################### - # create, update or delete family or variable object - ############################################################################################### - def family_or_variable( - self, - filename: str, - name: str, - subpath: str, - obj: dict, - comment: Optional[str], - *, - first_variable: bool = False, - family_is_leadership: bool = False, - family_is_dynamic: bool = False, - parent_dynamic: Optional[str] = None, - ) -> None: - if name.startswith("_"): - msg = f'the variable or family name "{name}" is incorrect, it must not starts with "_" character' - raise DictConsistencyError(msg, 16, [filename]) - if not subpath: - path = name - else: - path = f"{subpath}.{name}" - if self.version == "0.1" and not isinstance(obj, dict) and obj is not None: - msg = f'the variable "{path}" has a wrong type "{type(obj)}"' - raise DictConsistencyError(msg, 17, [filename]) - typ = self.is_family_or_variable( - path, - obj, - family_is_leadership, - filename, - ) - logging.info("family_or_variable: %s is a %s", path, typ) - if typ == "family": - parser = self.parse_family - else: - parser = self.parse_variable - parser( - filename, - name, - path, - obj, - comment=comment, - first_variable=first_variable, - family_is_leadership=family_is_leadership, - family_is_dynamic=family_is_dynamic, - parent_dynamic=parent_dynamic, - ) - - def parse_family( - self, - filename: str, - name: str, - path: str, - obj: Optional[Dict[str, Any]], - *, - comment: Optional[str] = None, - first_variable: bool = False, - family_is_leadership: bool = False, - family_is_dynamic: bool = False, - parent_dynamic: Optional[str] = None, - ) -> None: - """Parse a family""" - if obj is None: - obj = {} - family_obj = {} - subfamily_obj = {} - if self.version != "1.0": - exists = obj.pop("exists", None) - else: - exists = None - force_to_attrs = list(self.list_attributes(obj, filename)) - for key, value in obj.items(): - if key in force_to_attrs: - if key.startswith("_"): - key = key[1:] - family_obj[key] = value - else: - subfamily_obj[key] = value - if self.version != "1.0" and not family_obj and comment: - family_obj["description"] = comment - - if path in self.paths: - # it's just for modify subfamily or subvariable, do not redefine - if family_obj: - if exists in [None, True] and not obj.pop("redefine", False): - msg = _('family "{0}" define multiple time').format(path) - raise DictConsistencyError( - msg, 32, self.paths[path].xmlfiles + [filename] - ) - # convert to Calculation objects - self.parse_parameters( - path, - obj, - filename, - family_is_dynamic, - typ="family", - ) - if self.load_unexist_redefine or exists in [None, True]: - self.paths.add( - path, - self.paths[path].model_copy(update=obj), - family_is_dynamic, - parent_dynamic, - force=True, - ) - self.paths[path].xmlfiles.append(filename) - force_not_first = True - if self.paths[path].type == "dynamic": - family_is_dynamic = True - parent_dynamic = path - else: - redefine = family_obj.pop("redefine", None) - if not self.load_unexist_redefine and exists is None and redefine: - raise Exception( - f'cannot redefine the inexisting family "{path}" in {filename}' - ) - if not self.load_unexist_redefine and exists is True: - return - extra_attrs = set(family_obj) - self.family_attrs - if extra_attrs: - raise Exception(f"extra attrs ... {extra_attrs}") - obj_type = self.get_family_or_variable_type(family_obj) - if obj_type is None: - # auto set type - if "_dynamic" in family_obj: - dynamic = family_obj["_dynamic"] - elif "dynamic" in family_obj: - dynamic = family_obj["dynamic"] - else: - dynamic = None - if isinstance(dynamic, (list, dict)): - family_obj["type"] = obj_type = "dynamic" - if obj_type == "dynamic": - family_is_dynamic = True - if "{{ identifier }}" not in name: - if "{{ suffix }}" in name: - name = name.replace("{{ suffix }}", "{{ identifier }}") - path = path.replace("{{ suffix }}", "{{ identifier }}") - elif "variable" in family_obj: - name += "{{ identifier }}" - path += "{{ identifier }}" - else: - msg = f'dynamic family name must have "{{{{ identifier }}}}" in his name for "{path}"' - raise DictConsistencyError(msg, 13, [filename]) - parent_dynamic = path - self.add_family( - path, - name, - family_obj, - filename, - family_is_dynamic, - parent_dynamic, - ) - force_not_first = False - if self.paths[path].type == "leadership": - family_is_leadership = True - for idx, key in enumerate(subfamily_obj): - value = subfamily_obj[key] - first_variable = not force_not_first and idx == 0 - comment = self.get_comment(key, obj) - self.family_or_variable( - filename, - key, - path, - value, - comment, - first_variable=first_variable, - family_is_leadership=family_is_leadership, - family_is_dynamic=family_is_dynamic, - parent_dynamic=parent_dynamic, - ) - - def list_attributes( - self, - obj: Dict[str, Any], - filename: str, - ) -> Iterator[str]: - """List attributes""" - force_to_variable = [] - for key, value in obj.items(): - if not isinstance(key, str): - raise DictConsistencyError( - f"a key is not in string format: {key}", - 103, - [filename], - ) - if key in force_to_variable: - continue - if key.startswith("_"): - # if key starts with _, it's an attribute - yield key - # if same key without _ exists, it's a variable! - true_key = key[1:] - if true_key in obj: - force_to_variable.append(true_key) - continue - if isinstance(value, dict) and not self.is_calculation( - key, - value, - self.family_calculations, - False, - ): - # it's a dict, so a new variables! - continue - # 'variable' for compatibility to format 1.0 - if ( - key == "variable" - and obj.get("type") != "dynamic" - and obj.get("_type") != "dynamic" - ): - continue - if key in self.family_attrs: - yield key - - def add_family( - self, - path: str, - name: str, - family: dict, - filename: str, - family_is_dynamic: bool, - parent_dynamic: str, - ) -> None: - """Add a new family""" - family["path"] = path - family["namespace"] = self.namespace - family["version"] = self.version - family["xmlfiles"] = [filename] - obj_type = self.get_family_or_variable_type(family) - if obj_type == "dynamic": - family_obj = self.dynamic - if self.version == "1.0": - if "variable" not in family: - raise DictConsistencyError( - f'dynamic family must have "variable" attribute for "{path}"', - 101, - family["xmlfiles"], - ) - if "dynamic" in family: - raise DictConsistencyError( - 'variable and dynamic cannot be set together in the dynamic family "{path}"', - 100, - family["xmlfiles"], - ) - family["dynamic"] = { - "type": "variable", - "variable": family["variable"], - "propertyerror": False, - "allow_none": True, - } - del family["variable"] - # FIXME only for 1.0 - if "variable" in family: - family["dynamic"] = { - "type": "variable", - "variable": family["variable"], - "propertyerror": False, - "allow_none": True, - } - del family["variable"] - if self.version != "1.0": - warning = f'"variable" attribute in dynamic family "{ path }" is depreciated in {filename}' - warn(warning) - if "variable" in family: - raise Exception( - f'dynamic family must not have "variable" attribute for "{family["path"]}" in {family["xmlfiles"]}' - ) - else: - family_obj = self.family - # convert to Calculation objects - self.parse_parameters( - path, - family, - filename, - family_is_dynamic, - typ="family", - ) - try: - self.paths.add( - path, - family_obj(name=name, **family), - family_is_dynamic, - parent_dynamic, - ) - except ValidationError as err: - raise Exception(f'invalid family "{path}" in "{filename}": {err}') from err - self.set_name( - self.paths[path], - "optiondescription_", - ) - if "." not in path: - parent = "." - else: - parent = path.rsplit(".", 1)[0] - self.parents[parent].append(path) - self.parents[path] = [] - self.families.append(path) - - def parse_variable( - self, - filename: str, - name: str, - path: str, - obj: Optional[Dict[str, Any]], - *, - comment: Optional[str] = None, - first_variable: bool = False, - family_is_leadership: bool = False, - family_is_dynamic: bool = False, - parent_dynamic: Optional[str] = None, - ) -> None: - """Parse variable""" - if self.version == "1.0" or isinstance(obj, dict): - if obj is None: - obj = {} - extra_attrs = set(obj) - self.variable_attrs - else: - extra_attrs = [] - obj = {"default": obj} - if comment: - obj["description"] = comment - if extra_attrs: - raise DictConsistencyError( - f'"{path}" is not a valid variable, there are additional ' - f'attributes: "{", ".join(extra_attrs)}"', - 65, - [filename], - ) - self.parse_parameters( - path, - obj, - filename, - family_is_dynamic, - ) - self.parse_params(path, obj, filename) - self.parse_secret_manager(path, obj, filename, family_is_dynamic) - exists = obj.pop("exists", None) - if path in self.paths: - if not self.load_unexist_redefine: - if exists is False: - return - if not obj.pop("redefine", False): - msg = _('variable "{0}" define multiple time').format(path) - raise DictConsistencyError( - msg, 45, self.paths[path].xmlfiles + [filename] - ) - self.paths.add( - path, - self.paths[path].model_copy(update=obj), - family_is_dynamic, - parent_dynamic, - force=True, - ) - self.paths[path].xmlfiles.append(filename) - else: - if not self.load_unexist_redefine and exists is True: - # this variable must exist - # but it's not the case - # so do nothing - return - redefine = obj.pop("redefine", False) - if not self.load_unexist_redefine and redefine: - msg = f'cannot redefine the inexisting variable "{path}"' - raise DictConsistencyError(msg, 46, [filename]) - obj["path"] = path - self.add_variable(name, obj, filename, family_is_dynamic, parent_dynamic) - if family_is_leadership: - if first_variable: - self.leaders.append(path) - else: - self.followers.append(path) - - def parse_parameters( - self, - path: str, - obj: dict, - filename: str, - family_is_dynamic: bool, - *, - typ: str = "variable", - ): - """Parse variable or family parameters""" - if typ == "variable": - calculations = self.variable_calculations - else: - calculations = self.family_calculations - for key, value in obj.items(): - if self.is_calculation( - key, - value, - calculations, - False, - ): - try: - self.set_calculation( - obj, - key, - value, - path, - family_is_dynamic, - [filename], - ) - except ValidationError as err: - raise Exception( - f'the {typ} "{path}" in "{filename}" has an invalid "{key}": {err}' - ) from err - continue - if not isinstance(value, list): - continue - for idx, val in enumerate(value): - if not self.is_calculation( - key, - val, - calculations, - True, - ): - continue - try: - self.set_calculation( - obj, - key, - val, - path, - family_is_dynamic, - [filename], - inside_list=True, - index=idx, - ) - except ValidationError as err: - raise Exception( - f'the {typ} "{path}" in "{filename}" has an invalid "{key}" ' - f"at index {idx}: {err}" - ) from err - - def parse_params(self, path, obj, filename): - """Parse variable params""" - if "params" not in obj: - return - if not isinstance(obj["params"], dict): - raise DictConsistencyError( - _("params must be a dict for {0}").format(path), - 55, - [filename], - ) - params = [] - for key, val in obj["params"].items(): - try: - params.append( - AnyParam( - key=key, - value=val, - type="any", - path=None, - attribute=None, - family_is_dynamic=None, - namespace=self.namespace, - xmlfiles=[filename], - ) - ) - except ValidationError as err: - raise DictConsistencyError( - _('"{0}" has an invalid "params" for {1}: {2}').format( - key, path, err - ), - 54, - [filename], - ) from err - obj["params"] = params - - def parse_secret_manager(self, path, obj, filename, family_is_dynamic): - """Parse variable secret_manager""" - if "secret_manager" not in obj: - return - if not isinstance(obj["secret_manager"], dict): - raise DictConsistencyError( - _("secret_manager must be a dict for {0}").format(path), - 64, - [filename], - ) - secret_manager = { - "type": "jinja", - "jinja": self.secret_pattern, - "params": obj["secret_manager"], - } - self.set_calculation( - obj, - "secret_manager", - secret_manager, - path, - family_is_dynamic, - [filename], - ) - - def add_variable( - self, - name: str, - variable: dict, - filename: str, - family_is_dynamic: bool, - parent_dynamic: Optional[str], - ) -> None: - if "{ suffix" in variable["path"]: - raise Exception() - """Add a new variable""" - if not isinstance(filename, list): - filename = [filename] - - variable["namespace"] = self.namespace - variable["version"] = self.version - variable["xmlfiles"] = filename - variable_type = self.get_family_or_variable_type(variable) - obj = { - "symlink": SymLink, - "choice": self.variable, - }.get(variable_type, self.variable) - try: - variable_obj = obj(name=name, **variable) - except ValidationError as err: - raise Exception( - f'invalid variable "{variable["path"]}" in "{filename}": {err}' - ) from err - self.paths.add( - variable["path"], - variable_obj, - family_is_dynamic, - parent_dynamic, - ) - self.variables.append(variable["path"]) - if "." in variable["path"]: - parent_path = variable["path"].rsplit(".", 1)[0] - else: - parent_path = "." - self.parents[parent_path].append(variable["path"]) - self.set_name( - variable_obj, - "option_", - ) - - def del_family( - self, - path: str, - ) -> None: - """The family is empty, so delete it""" - del self.paths[path] - self.families.remove(path) - del self.parents[path] - if "." in path: - parent = path.rsplit(".", 1)[0] - else: - parent = "." - self.parents[parent].remove(path) - - ############################################################################################### - # set tiramisu file name - ############################################################################################### - def set_name( - self, - obj: Union[Variable, Family], - option_prefix: str, - ): - """Set Tiramisu object name""" - self.index += 1 - self.reflector_names[obj.path] = f"{option_prefix}{self.index}{self.suffix}" - - ############################################################################################### - # calculations - ############################################################################################### - def is_calculation( - self, - attribute: str, - value: dict, - calculations: list, - inside_list: bool, - ): - """Check if it's a calculation""" - if inside_list: - calculations = calculations[0] - else: - calculations = calculations[1] - if not isinstance(value, dict) or attribute not in calculations: - return False - return self.check_auto_type(value) - - def check_auto_type(self, value): - if "type" in value: - return value["type"] in CALCULATION_TYPES - # auto set type - typ = set(CALCULATION_TYPES) & set(value) - # XXX variable is also set to information - if typ == {"variable", "information"}: - typ = {"information"} - if len(typ) == 1: - value["type"] = list(typ)[0] - return True - return False - - def set_calculation( - self, - obj: dict, - attribute: str, - value: dict, - path: str, - family_is_dynamic: bool, - xmlfiles: List[str], - *, - inside_list: bool = False, - index: int = None, - ): - """This variable is a calculation""" - calculation_object = value.copy() - typ = calculation_object.pop("type") - - calculation_object["attribute_name"] = attribute - calculation_object["path"] = path - calculation_object["inside_list"] = inside_list - calculation_object["version"] = self.version - calculation_object["namespace"] = self.namespace - calculation_object["xmlfiles"] = xmlfiles - # - if "params" in calculation_object: - if not isinstance(calculation_object["params"], dict): - raise Exception("params must be a dict") - params = [] - for key, val in calculation_object["params"].items(): - if isinstance(val, dict) and "type" not in val: - # auto set type - param_typ = set(CALCULATION_TYPES) & set(val) - # XXX variable is also set to information - if param_typ == {"variable", "information"}: - param_typ = {"information"} - if len(param_typ) == 1: - val["type"] = list(param_typ)[0] - if not isinstance(val, dict) or "type" not in val: - param_typ = "any" - val = { - "value": val, - "type": "any", - } - else: - if self.version == "1.0" and val["type"] == "suffix": - val["type"] = "identifier" - param_typ = val["type"] - val["key"] = key - val["path"] = path - val["family_is_dynamic"] = family_is_dynamic - val["attribute"] = attribute - val["namespace"] = self.namespace - val["xmlfiles"] = xmlfiles - if param_typ not in PARAM_TYPES: - raise DictConsistencyError( - f'unknown type "{param_typ}" for "{path}"', - 52, - xmlfiles, - ) - try: - params.append(PARAM_TYPES[param_typ](**val)) - except ValidationError as err: - raise DictConsistencyError( - f'"{attribute}" has an invalid "{key}" for "{path}": {err}', - 29, - xmlfiles, - ) from err - calculation_object["params"] = params - # - return_type = calculation_object.get("return_type") - if return_type: - if return_type not in self.variable_types: - raise Exception( - f'unknown "return_type" in {attribute} of variable "{path}"' - ) - # - if typ == "identifier" and not family_is_dynamic: - msg = f'identifier calculation for "{attribute}" in "{path}" cannot be set variable is not in dynamic family' - raise DictConsistencyError(msg, 53, xmlfiles) - if attribute in PROPERTY_ATTRIBUTE: - calc = CALCULATION_PROPERTY_TYPES[typ](**calculation_object) - else: - calc = CALCULATION_TYPES[typ](**calculation_object) - if index is None: - obj[attribute] = calc - else: - obj[attribute][index] = calc - - -class RougailConvert(ParserVariable): - """Main Rougail conversion""" - - supported_version = ["1.0", "1.1"] - - def __init__(self, rougailconfig) -> None: - self.annotator = False - self.has_namespace = False - super().__init__(rougailconfig) - - def search_calculation( - self, - hint: dict, - ) -> Tuple[List[Any], List[Any]]: - """attribute is calculated if typing is like: Union[Calculation, xxx]""" - 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__ - ): - outside_list.append(key) - if ( - "Union" in value.__class__.__name__ - and "_GenericAlias" in value.__args__[0].__class__.__name__ - and Calculation in value.__args__[0].__args__ - ): - inside_list.append(key) - if ( - "Union" in value.__class__.__name__ - and value.__args__[0].__class__.__name__ == "_GenericAlias" - and "Union" in value.__args__[0].__args__[0].__class__.__name__ - and Calculation in value.__args__[0].__args__[0].__args__ - ): - inside_list.append(key) - return inside_list, outside_list - - def create_namespace( - self, namespace_description: str, namespace_path: Optional[str] = None - ) -> None: - if namespace_path is None: - namespace_path = self.namespace - self.version = "" - self.parse_family( - "", - self.namespace, - namespace_path, - { - "description": namespace_description, - }, - ) - - def get_comment( - self, - name: str, - objects: CommentedMap, - ) -> Optional[str]: - if name in objects.ca.items: - comment = objects.ca.items[name][2] - else: - comment = None - if comment: - comment = comment.value[1:].strip() - return comment - - def parse_root_file( - self, - filename: str, - path: str, - version: str, - objects: dict, - ) -> None: - self.version = version - for name, obj in objects.items(): - comment = self.get_comment(name, objects) - self.family_or_variable( - filename, - name, - path, - obj, - comment, - ) - - def validate_file_version( - self, - obj: dict, - filename: str, - ) -> None: - """version is mandatory in YAML file""" - if obj is None: - obj = {} - for name in ["_version", "version"]: - if name not in obj: - continue - version = str(obj.pop(name)) - break - else: - # the `version` attribute is not mandatory - default_version = self.default_dictionary_format_version - if default_version is not None: - version = default_version - else: - msg = '"version" attribut is mandatory in YAML file' - raise DictConsistencyError(msg, 27, [filename]) - - if version not in self.supported_version: - msg = f'version "{version}" is not supported, list of supported versions: {display_list(self.supported_version, separator="or", add_quote=True)}' - raise DictConsistencyError(msg, 28, [filename]) - return version - - def annotate( - self, - ): - """Apply annotation""" - if not self.paths.has_value(): - self.init() - self.parse_directories() - if self.annotator: - raise DictConsistencyError( - _("Cannot execute annotate multiple time"), 85, None - ) - SpaceAnnotator(self) - self.annotator = True - - def parse_directories(self) -> None: - if not self.walker: - msg = _( - 'invalid "structural" definition ({0}), we cannot load any structural file!' - ).format(self.structurals) - raise DictConsistencyError(msg, 51, None) - self.init() - self.walker(self) - - def reflect(self) -> None: - """Apply TiramisuReflector""" - functions_files = [ - func for func in self.functions_files if func not in self.exclude_imports - ] - self.reflector = TiramisuReflector( - self, - functions_files, - ) - - def save( - self, - ): - """Return tiramisu object declaration as a string""" - self.init() - self.annotate() - self.reflect() - output = self.reflector.get_text() + "\n" - filename = self.tiramisu_cache - if filename: - with open(filename, "w", encoding="utf-8") as tiramisu: - tiramisu.write(output) - # print(output) - return output diff --git a/src/rougail/convert/__init__.py b/src/rougail/convert/__init__.py new file mode 100644 index 000000000..9160ddef1 --- /dev/null +++ b/src/rougail/convert/__init__.py @@ -0,0 +1,63 @@ +"""Silique (https://www.silique.fr) +Copyright (C) 2022-2025 + +This program is free software: you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +""" + +from tiramisu import Config +from warnings import warn + +from .convert import RougailConvert +from ..config import RougailConfig +from ..user_datas import UserDatas +from ..tiramisu import tiramisu_display_name + + +class Rougail(UserDatas): + """Main Rougail object""" + + def __init__( + self, + rougailconfig=None, + ) -> None: + if rougailconfig is None: + rougailconfig = RougailConfig + self.rougailconfig = rougailconfig + self.converted = RougailConvert(self.rougailconfig) + self.config = None + + def run(self): + """Get Tiramisu Config""" + if not self.config: + tiram_obj = self.converted.save() + optiondescription = {} + custom_types = { + custom.__name__: custom + for custom in self.rougailconfig["custom_types"].values() + } + exec(tiram_obj, custom_types, optiondescription) # pylint: disable=W0122 + self.config = Config( + optiondescription["option_0"], + display_name=tiramisu_display_name, + ) + self.config.property.read_write() + return self.config + + def get_config(self): + warn( + "get_config is deprecated, use run instead", + DeprecationWarning, + stacklevel=2, + ) + return self.run() diff --git a/src/rougail/object_model.py b/src/rougail/convert/object_model.py similarity index 99% rename from src/rougail/object_model.py rename to src/rougail/convert/object_model.py index 98a275039..12bb0d272 100644 --- a/src/rougail/object_model.py +++ b/src/rougail/convert/object_model.py @@ -28,10 +28,10 @@ from pydantic import ( ) import tiramisu from tiramisu.config import get_common_path -from .utils import get_jinja_variable_to_param, calc_multi_for_type_variable, undefined -from .i18n import _ -from .error import DictConsistencyError, VariableCalculationDependencyError -from .tiramisu import CONVERT_OPTION +from ..utils import get_jinja_variable_to_param, calc_multi_for_type_variable, undefined +from ..i18n import _ +from ..error import DictConsistencyError, VariableCalculationDependencyError +from ..tiramisu import CONVERT_OPTION BASETYPE = Union[StrictBool, StrictInt, StrictFloat, StrictStr, None] PROPERTY_ATTRIBUTE = ["frozen", "hidden", "disabled", "mandatory"] diff --git a/src/rougail/path.py b/src/rougail/convert/path.py similarity index 99% rename from src/rougail/path.py rename to src/rougail/convert/path.py index a8f6d0040..2d29b5baf 100644 --- a/src/rougail/path.py +++ b/src/rougail/convert/path.py @@ -24,9 +24,9 @@ from typing import ( import logging from re import compile, findall -from .i18n import _ +from ..i18n import _ from .object_model import Family, Variable -from .utils import normalize_family +from ..tiramisu import normalize_family class Paths: diff --git a/src/rougail/tiramisureflector.py b/src/rougail/convert/tiramisureflector.py similarity index 99% rename from src/rougail/tiramisureflector.py rename to src/rougail/convert/tiramisureflector.py index 57434c60e..bd5c7e4c2 100644 --- a/src/rougail/tiramisureflector.py +++ b/src/rougail/convert/tiramisureflector.py @@ -30,10 +30,10 @@ from typing import Optional, Union from json import dumps from os.path import isfile, basename -from .i18n import _ -from .error import DictConsistencyError, VariableCalculationDependencyError -from .utils import normalize_family -from .object_model import Calculation, CONVERT_OPTION +from ..i18n import _ +from ..error import DictConsistencyError, VariableCalculationDependencyError +from ..tiramisu import normalize_family, CONVERT_OPTION +from .object_model import Calculation class BaseElt: # pylint: disable=R0903 diff --git a/src/rougail/error.py b/src/rougail/error.py index ab0161a40..f6661482f 100644 --- a/src/rougail/error.py +++ b/src/rougail/error.py @@ -26,6 +26,7 @@ along with this program. If not, see . """ from .i18n import _ +from .tiramisu import display_xmlfiles class ConfigError(Exception): diff --git a/src/rougail/structural_directory/__init__.py b/src/rougail/structural_directory/__init__.py index 8288f24bc..6b6cd9393 100644 --- a/src/rougail/structural_directory/__init__.py +++ b/src/rougail/structural_directory/__init__.py @@ -22,8 +22,8 @@ from pathlib import Path from ruamel.yaml import YAML -from ..utils import normalize_family -from ..path import Paths +from ..tiramisu import normalize_family +from ..convert.path import Paths from ..error import DictConsistencyError from ..i18n import _ diff --git a/src/rougail/user_datas.py b/src/rougail/user_datas.py index 2c08222ea..0bd75cac3 100644 --- a/src/rougail/user_datas.py +++ b/src/rougail/user_datas.py @@ -22,7 +22,6 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from typing import List from re import findall -from rougail.utils import normalize_family, undefined from tiramisu import Calculation from tiramisu.error import ( PropertiesOptionError, @@ -31,10 +30,11 @@ from tiramisu.error import ( ConfigError, CancelParam, ) +from .utils import undefined +from .tiramisu import normalize_family, CONVERT_OPTION from .error import DictConsistencyError from .i18n import _ -from .object_model import CONVERT_OPTION class UserDatas: diff --git a/tests/dictionaries/00_2default_calculated_variable_description_multi_line/tiramisu/base.py b/tests/dictionaries/00_2default_calculated_variable_description_multi_line/tiramisu/base.py new file mode 100644 index 000000000..9226f3b84 --- /dev/null +++ b/tests/dictionaries/00_2default_calculated_variable_description_multi_line/tiramisu/base.py @@ -0,0 +1,17 @@ +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 first variable", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['../rougail-tests/structures/00_2default_calculated_variable_description_multi_line/rougail/00-base.yml'], 'type': 'string'}) +option_3 = StrOption(name="var2", doc="a second variable", default=Calculation(func['calc_value'], Params((ParamOption(option_2)))), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/00_2default_calculated_variable_description_multi_line/rougail/00-base.yml'], 'type': 'string'}) +option_4 = StrOption(name="var3", doc="a new variable", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['../rougail-tests/structures/00_2default_calculated_variable_description_multi_line/rougail/00-base.yml'], 'type': 'string'}) +optiondescription_1 = OptionDescription(name="rougail", doc="Rougail", group_type=groups.namespace, children=[option_2, option_3, option_4], properties=frozenset({"basic"}), informations={'ymlfiles': ['']}) +option_0 = OptionDescription(name="baseoption", doc="baseoption", children=[optiondescription_1]) diff --git a/tests/dictionaries/00_2default_calculated_variable_description_multi_line/tiramisu/no_namespace.py b/tests/dictionaries/00_2default_calculated_variable_description_multi_line/tiramisu/no_namespace.py new file mode 100644 index 000000000..1b129710d --- /dev/null +++ b/tests/dictionaries/00_2default_calculated_variable_description_multi_line/tiramisu/no_namespace.py @@ -0,0 +1,12 @@ +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') +ALLOWED_LEADER_PROPERTIES.add("basic") +ALLOWED_LEADER_PROPERTIES.add("standard") +ALLOWED_LEADER_PROPERTIES.add("advanced") +option_1 = StrOption(name="var1", doc="a first variable", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['../rougail-tests/structures/00_2default_calculated_variable_description_multi_line/rougail/00-base.yml'], 'type': 'string'}) +option_2 = StrOption(name="var2", doc="a second variable", default=Calculation(func['calc_value'], Params((ParamOption(option_1)))), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/00_2default_calculated_variable_description_multi_line/rougail/00-base.yml'], 'type': 'string'}) +option_3 = StrOption(name="var3", doc="a new variable", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['../rougail-tests/structures/00_2default_calculated_variable_description_multi_line/rougail/00-base.yml'], 'type': 'string'}) +option_0 = OptionDescription(name="baseoption", doc="baseoption", children=[option_1, option_2, option_3])