""" Silique (https://www.silique.fr) Copyright (C) 2024-2026 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 warnings import warn from typing import Optional from tiramisu import Calculation, groups from tiramisu.error import display_list, PropertiesOptionError from rougail.tiramisu import display_xmlfiles from rougail.utils import PROPERTY_ATTRIBUTE from rougail.error import VariableCalculationDependencyError, RougailWarning, ExtensionError from .utils import dump, to_phrase, calc_path from .i18n import _ HIDDEN_PROPERTIES = [ "hidden", "disabled", ] DISABLED_PROPERTIES = ["disabled"] class _ToString: def _to_string( self, child, prop, do_not_raise=False, ): calculation = child.information.get(f"{prop}_calculation", None) if not calculation: if do_not_raise: return None raise ExtensionError( _( 'cannot find "{0}_calculation" information, ' "do you have annotate this configuration?" ).format(prop) ) if isinstance(calculation, list): values = [] for cal in calculation: value = self._calculation_to_string(child, cal, prop) if value is not None: values.append(value) return values ret = self._calculation_to_string(child, calculation, prop) if isinstance(ret, list) and len(ret) == 1: return ret[0] return ret def _calculation_to_string(self, child, calculation, attribute_type): if "description" in calculation: values = calculation if self.document_a_type and "variables" in values: for variable in list(values["variables"]): variable["path"] = self.doc_path(variable["path"]) elif "type" not in calculation: values = calculation["value"] elif calculation["type"] == "jinja": values = self._calculation_jinja_to_string( child, calculation, attribute_type ) elif calculation["type"] == "variable": values = self._calculation_variable_to_string( child, calculation, attribute_type ) elif calculation["type"] == "identifier": values = self._calculation_identifier_to_string( child, calculation, attribute_type ) elif calculation["type"] == "information": if "path" in calculation: variable_path = self.doc_path(calculation["path"]) values = _( 'the value of the information "{0}" of the variable "{1}"' ).format(calculation["information"], variable_path) else: values = _('the value of the global information "{0}"').format( calculation["information"] ) else: values = _("the value of the {0}").format(calculation["type"]) return values def _calculation_jinja_to_string(self, child, calculation, attribute_type): if calculation["value"] is not True: values = calculation["value"] else: values = _("depends on a calculation") warning = _( '"{0}" is a calculation for {1} but has no description in {2}' ).format( attribute_type, self.doc_path(child.path(uncalculated=True)), display_xmlfiles(child.information.get("ymlfiles")), ) warn( warning, RougailWarning, ) return values def _calculation_variable_to_string(self, child, calculation, attribute_type): if attribute_type in PROPERTY_ATTRIBUTE: func = self._calculation_variable_to_string_known_property else: func = self._calculation_variable_to_string_not_properties return func(child, calculation, attribute_type) def _calculation_variable_to_string_known_property(self, child, informations, prop): condition = informations["value"] variable_path = condition["path"] values = [] option = self.true_config.option(variable_path) if option.isdynamic(): variables = self._get_annotation_variable( option, condition.get("identifiers") ) else: option = self.true_config.option(variable_path) try: is_inaccessible = self.is_inaccessible_user_data(option) except AttributeError as err: if err.code != "option-not-found": raise err from err is_inaccessible = True if is_inaccessible: variables = [[None, None, None]] else: description = self._convert_description( option.description(uncalculated=True), "description", its_a_path=False, ) variables = [[variable_path, description, None]] for cpath, description, identifiers in variables: if not cpath: # we cannot access to this variable, so try with permissive if condition["type"] == "transitive": value = None else: value = condition["value"] option = self.true_config.forcepermissive.option(variable_path) try: variable_value = self._get_unmodified_default_value(option) except PropertiesOptionError as err: if informations["propertyerror"] is True: raise err from err if informations["propertyerror"] == "transitive": return True return False except VariableCalculationDependencyError: values.append(_("depends on an undocumented variable")) continue except AttributeError as err: return informations.get("default", False) if condition["type"] == "transitive": return True if prop in HIDDEN_PROPERTIES: condition_type = condition["condition"] if ( condition_type == "when" and value == variable_value or condition_type == "when_not" and value != variable_value ): # always "prop" return True return False if condition["type"] == "transitive": submsg = _('is "{0}"').format(prop) else: condition_type = condition["condition"] conditions = [] if not informations["propertyerror"]: conditions.append(_("is accessible")) if informations["optional"]: conditions.append(_("is defined")) value = condition["value"] if not isinstance(value, str): value = dump(value) if condition_type == "when_not": conditions.append(_('hasn\'t the value "{0}"').format(value)) else: conditions.append(_('has the value "{0}"').format(value)) submsg = display_list(conditions, sort=False) if informations["propertyerror"] == "transitive": submsg = display_list( [_("is {0}").format(prop), submsg], separator="or", sort=False ) msg = _('when the variable "{{0}}" {0}').format(submsg) path_obj = { "path": self.doc_path(variable_path), } if identifiers: path_obj["identifiers"] = identifiers values.append( { "message": msg, "path": path_obj, "description": description, } ) if len(values) == 1: return values[0] return values def _calculation_variable_to_string_not_properties( self, child, calculation, attribute_type ): variable = self.true_config.unrestraint.option(calculation["value"]["path"]) if calculation["optional"]: try: variable.get() except AttributeError: return None true_msg = _('the value of the variable "{0}" if it is defined') else: true_msg = _('the value of the variable "{0}"') if not variable.isdynamic(): func = self._calculation_normal_variable_to_string_not_properties else: func = self._calculation_dynamic_variable_to_string_not_properties return func(variable, calculation["value"], true_msg) def _calculation_normal_variable_to_string_not_properties( self, child, obj, true_msg ): isfollower = child.isfollower() if not isfollower and self.is_inaccessible_user_data(child): uncalculated = child.value.default(uncalculated=True) if uncalculated and not isinstance(uncalculated, Calculation): if isinstance(uncalculated, list): return { "submessage": _("(from an undocumented variable)"), "values": uncalculated, } else: if not isinstance(uncalculated, str): uncalculated = dump(uncalculated) true_msg = _("{0} (from an undocumented variable)").format( uncalculated ) else: true_msg = _("the value of an undocumented variable") try: description = self._convert_description( child.description(uncalculated=True), "description", its_a_path=False ) except AttributeError: description = path return { "message": true_msg, "path": obj, "description": description, } def _calculation_dynamic_variable_to_string_not_properties( self, child, obj, true_msg ): values = [] for path, description, identifiers in self._get_annotation_variable( child, obj.get("identifiers") ): cpath = calc_path(path, identifiers=identifiers) variable = self.true_config.option(cpath) path_obj = { "path": self.doc_path(path), } if identifiers: path_obj["identifiers"] = identifiers values.append( self._calculation_normal_variable_to_string_not_properties( variable, path_obj, true_msg ) ) return values def _calculation_identifier_to_string(self, child, calculation, attribute_type): if attribute_type in PROPERTY_ATTRIBUTE: if calculation["condition"] == "when": return _('when the identifier is "{0}"').format(calculation["value"]) if calculation["condition"] == "when_not": return _('when the identifier is not "{0}"').format( calculation["value"] ) return True else: return _("the value of the identifier") def doc_path(self, path): if self.document_a_type: if not "." in path: return None return path.split(".", 1)[-1] return path def _get_annotation_variable(self, child, ori_identifiers): path = child.path(uncalculated=True) if ori_identifiers: if None in ori_identifiers: all_identifiers = self.true_config.option( calc_path(path, identifiers=ori_identifiers) ).identifiers() else: all_identifiers = [ori_identifiers] else: all_identifiers = child.identifiers() for identifiers in all_identifiers: cpath = calc_path(path, identifiers=identifiers) description = self._convert_description( child.description(uncalculated=True), "description", its_a_path=False ) if child.isdynamic(): yield path, description, identifiers.copy() else: yield path, description, None class Collect(_ToString): def collect_families(self, family) -> dict: informations = {} for child in family.list(uncalculated=True): if child.type(only_self=True) == "symlink" or ( not child.isdynamic() and self.is_inaccessible_user_data(child) ): # do not document symlink option or inacesssible variable # (dynamic variable could be accessible only in one context) continue if child.isoptiondescription(): func = self.collect_family else: func = self.collect_variable func(child, informations) return informations def collect_family(self, family, informations: dict) -> None: family_type = self._get_family_type(family) family_informations = self._collect_family(family, family_type) if family_informations is False: return sub_informations = self.collect_families(family) if not sub_informations: # a family without subfamily/subvariable return name = family.name(uncalculated=True) informations[name] = { "type": family_type, "informations": family_informations, "children": sub_informations, } def _get_family_type(self, family) -> str: if self.support_namespace and family.group_type() is groups.namespace: return "namespace" if family.isleadership(): return "leadership" if family.isdynamic(only_self=True): return "dynamic" return "family" def _collect_family( self, family, family_type, *, with_identifier: bool = True, current_identifier_only: bool = False, ) -> dict: path = family.path(uncalculated=True) informations = {} if not self._collect(family, informations, family_type=family_type): return False, [] if family_type == "leadership": informations.setdefault("help", []).append( _("This family contains lists of variable blocks") ) elif family_type == "dynamic": informations.setdefault("help", []).append( _("This family builds families dynamically") ) if with_identifier: identifier = self._to_string(family, "dynamic", do_not_raise=True) if identifier is None: identifier = family.identifiers(only_self=True) if not isinstance(identifier, list): identifier = [identifier] informations["identifier"] = identifier elif family_type == "namespace": informations.setdefault("help", []).append(_("This family is a namespace")) return informations def collect_variable( self, child, informations: dict, *, only_one=False, ) -> Optional[dict]: name = child.name(uncalculated=True) sub_informations = {} if not self._collect_variable( child, sub_informations, ): return None informations[name] = sub_informations if child.isleader(): # if not self.default_values: # child.value.set(sub_informations["gen_examples"][0]) return sub_informations return None def _collect_variable( self, child, informations: dict, ): if not self._collect(child, informations): return False informations["type"] = "variable" default = self._set_default( child, informations, ) self._parse_type( child, informations, ) if child.ismulti(): multi = not child.isfollower() or child.issubmulti() else: multi = False if multi: informations["multiple"] = True examples = child.information.get("examples", None) if examples is None: examples = child.information.get("test", None) if examples is not None: if len(examples) == 1: name = _("Example") values = examples[0] else: name = _("Examples") values = list(examples) informations["examples"] = {"name": name, "values": values} tags = child.information.get("tags", None) if tags: if len(tags) == 1: name = _("Tag") values = tags[0] else: name = _("Tags") values = list(tags) informations["tags"] = { "name": name, "values": values, } alternative_name = child.information.get("alternative_name", None) if alternative_name: informations["alternative_name"] = alternative_name return True def _collect( self, child, informations: dict, *, family_type: str = None, ): display, properties = self._get_properties(child, informations) if not display: return False informations["path"] = self.doc_path(child.path(uncalculated=True)) informations["name"] = child.name(uncalculated=True) description = self._get_description(child, family_type) if description is not None: informations["description"] = description help_ = child.information.get("help", None) if help_: informations["help"] = [to_phrase(help_)] if "properties" in informations: informations["properties"].extend(properties) else: informations["properties"] = properties properties = child.property.get(uncalculated=True) for mode in self.modes_level: if mode not in properties: continue informations["mode"] = mode break if child.isdynamic(): informations["identifiers"] = [] path = child.path(uncalculated=True) if child.has_identifiers(): identifiers = child.identifiers() else: identifiers = [child.identifiers()] for identifier in identifiers: cpath = calc_path(path, identifiers=identifier) child_identifier = self.true_config.option(cpath) if not self.is_inaccessible_user_data(child_identifier): informations["identifiers"].append(identifier) if not informations["identifiers"]: informations["identifiers"] = [["example"]] return True def _parse_type( self, child, informations, ): variable_type = child.information.get("type") doc_type = self.convert_option.get(variable_type, {"params": {}}) informations["variable_type"] = doc_type.get("msg", variable_type) # extra parameters for types option = child.get() validators = [] if "params" in doc_type: for param, msg in doc_type["params"].items(): value = option.impl_get_extra(f"_{param}") if value is None: value = option.impl_get_extra(param) if value is not None and value is not False: if isinstance(value, set): value = list(value) if isinstance(value, list): value = display_list(value, add_quote=True) if "doc" in msg: validators.append(msg["doc"].format(value)) else: validators.append(msg["description"].format(value)) # get validation information from annotator for name in child.information.list(): if not name.startswith("validators_calculation"): continue validators.extend( self._to_string( child, "validators", ) ) break if variable_type == "regexp": validators.append( _('text based with regular expressions "{0}"').format(child.pattern()) ) if validators: if len(validators) == 1: key = _("Validator") validators = validators[0] else: key = _("Validators") informations["validators"] = {"name": key, "values": validators} if variable_type == "choice": choices = self._to_string(child, "choice", do_not_raise=True) if choices is None: choices = child.value.list(uncalculated=True) for idx, val in enumerate(choices): if isinstance(val, Calculation): choices[idx] = self._to_string(child, "choice", f"_{idx}") informations["choices"] = {"name": _("Choices"), "values": choices} def _get_properties( self, child, child_informations, ): informations = [] properties = child.property.get(uncalculated=True) if "not_for_commandline" in properties: child_informations["not_for_commandline"] = True for prop, translated_prop in self.property_to_string: annotation = False if child.information.get(f"{prop}_calculation", False): annotation = self._to_string(child, prop) if annotation is None and prop in HIDDEN_PROPERTIES: return False, [] if annotation is True and prop in DISABLED_PROPERTIES: return False, [] if not annotation: continue elif prop not in properties: # this property is not in the variable so, do not comment it continue elif prop in HIDDEN_PROPERTIES: return False, [] prop_obj = { "type": "property", "name": translated_prop, "ori_name": prop, "access_control": prop in HIDDEN_PROPERTIES, } if annotation: prop_obj["annotation"] = annotation informations.append(prop_obj) return True, informations def _set_default( self, child, informations, ): default = self._to_string(child, "default", do_not_raise=True) if default is None and child.information.get("default_value_makes_sense", True): default_ = child.value.default(uncalculated=True) if not isinstance(default_, Calculation): default = default_ if default == []: default = None if default is not None: informations["default"] = {"name": _("Default"), "values": default} def _get_description(self, child, family_type): if child.information.get("forced_description", False): if ( child.isoptiondescription() or not child.isfollower() or not child.index() ): # all variables must have description but display error only for first slave msg = _('No attribute "description" for "{0}" in {1}').format( child.path(uncalculated=True), display_xmlfiles(child.information.get("ymlfiles")), ) warn( msg, RougailWarning, ) if family_type is not None: # it's a vaariable return self._convert_description( child.description(uncalculated=True), "family", its_a_path=True ) return None var_type = "variable" if family_type is None else "family" return self._convert_description( child.description(uncalculated=True), var_type, its_a_path=False ) def _convert_description(self, description, type_, its_a_path=False): if not its_a_path: description = to_phrase(description, type_) return description def is_inaccessible_user_data(self, child, *, only_disabled=False): """If family is not accessible in read_write mode (to load user_data), do not comment this family """ properties = child.property.get(uncalculated=True) if only_disabled: hidden_properties = DISABLED_PROPERTIES else: hidden_properties = HIDDEN_PROPERTIES for hidden_property in hidden_properties: if hidden_property in properties: return True calculation = child.information.get(f"{hidden_property}_calculation", None) if calculation: calculation_type = calculation.get("type") if ( calculation_type == "variable" and calculation["value"]["type"] == "condition" ): condition = calculation["value"] variable = self.true_config.forcepermissive.option( condition["path"] ) try: variable.value.get() except AttributeError: variable = None if variable and self.is_inaccessible_user_data( variable, only_disabled=only_disabled ): try: variable_value = self._get_unmodified_default_value( variable ) except VariableCalculationDependencyError: pass else: condition_type = condition["condition"] value = condition["value"] if self.calc_condition_when( condition_type, value, variable_value ): return True elif calculation_type == "identifier": if self.calc_condition_when( calculation["condition"], calculation["value"], child.identifiers()[-1], ): return True if not child.isoptiondescription(): for hidden_property in self.disabled_modes: if hidden_property in properties: return True return False def calc_condition_when(self, condition, value, current_value): return (condition == "when" and value == current_value) or ( condition == "when_not" and value != current_value ) def _get_unmodified_default_value(self, child): calculation = child.information.get(f"default_calculation", None) if not calculation: return child.value.get() if calculation["type"] == "variable": variable = self.true_config.forcepermissive.option( calculation["value"]["path"] ) if variable and self.is_inaccessible_user_data(variable): return self._get_unmodified_default_value(variable) raise VariableCalculationDependencyError()