""" Silique (https://www.silique.fr) Copyright (C) 2024-2026 distribued with GPL-2 or later license This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from typing import Union from re import compile from rougail.utils import undefined, PROPERTY_ATTRIBUTE from rougail.annotator.variable import Walk from rougail.output_doc.i18n import _ from rougail.convert.object_model import ( Calculation, JinjaCalculation, VariableCalculation, VariablePropertyCalculation, IdentifierCalculation, IdentifierPropertyCalculation, InformationCalculation, IndexCalculation, NamespaceCalculation, CONVERT_OPTION, ) try: from rougail.user_data_risotto.utils import RisottoCalculation except ImportError as err: RisottoCalculation = None class Annotator(Walk): """Annotate for documentation""" level = 95 def __init__( self, objectspace, *args, # pylint: disable=unused-argument **kwargs, ) -> None: if not objectspace.paths: return self.objectspace = objectspace if "force_default_value" in kwargs: self.default_values = kwargs["force_default_value"] else: self.default_values = self.objectspace.rougailconfig["doc.default_values"] self.regexp_description_get_paths = None self.populate_family() self.populate_variable() def populate_family(self) -> None: """Set doc, path, ... to family""" for family in self.get_families(): self.add_informations_from_properties(family) if family.type == "dynamic": self.force_default_value_in_identifier_variable(family) self.calculation_to_information( family.path, "dynamic", family.dynamic, family.version, ) path = family.path if path in self.objectspace.forced_descriptions: self.objectspace.informations.add( path, "forced_description", True, ) def force_default_value_in_identifier_variable(self, family) -> None: if not self.default_values: return if not isinstance(family.dynamic, list): self._force_default_value_in_suffix_variable(family, family.dynamic) else: for value in family.dynamic: self._force_default_value_in_suffix_variable( family, value, return_a_list=False ) def _force_default_value_in_suffix_variable( self, family, value, *, return_a_list=True, ) -> None: """For dynamic we must have values to document it""" if not isinstance(value, Calculation): return value_added = False if isinstance(value, (VariableCalculation, VariablePropertyCalculation)): variable = self.objectspace.paths.get_with_dynamic( value.variable, family.path, value.version, value.namespace, value.xmlfiles, )[0] if variable and not variable.default: values = self.add_examples_values(variable) value_added = True if not value_added: default_values = ["example"] if not return_a_list: default_values = default_values[0] value.default_values = default_values def add_examples_values(self, variable) -> list: """Check examples or test information to define examples values in a variable""" values = self.objectspace.informations.get(variable.path).get("examples", None) if not values: values = self.objectspace.informations.get(variable.path).get("test", None) if isinstance(values, tuple): values = list(values) if values: variable.default = list(values) else: variable.default = [CONVERT_OPTION[variable.type]["example"]] self.objectspace.informations.add( variable.path, "default_value_makes_sense", False ) return values def populate_variable(self) -> None: """convert variables""" for variable in self.get_variables(): if variable.type == "symlink": continue path = variable.path if variable.type == "choice": self.calculation_to_information( path, "choice", variable.choices, variable.version, ) self.manage_test(variable) default = variable.default if default is None and path in self.objectspace.default_multi: default = self.objectspace.default_multi[path] self.calculation_to_information( path, "default", default, variable.version, ) self.calculation_to_information( path, "validators", variable.validators, variable.version, ) self.add_informations_from_properties(variable) if path in self.objectspace.forced_descriptions: self.objectspace.informations.add( path, "forced_description", True, ) def manage_test(self, variable): # for version 1.0 and 1.1 values = self.objectspace.informations.get(variable.path).get("examples", None) if values: return values = self.objectspace.informations.get(variable.path).get("test", None) if values: self.objectspace.informations.add(variable.path, "examples", values) def add_informations_from_properties( self, variable: dict, ) -> None: """convert properties""" for prop in PROPERTY_ATTRIBUTE: prop_value = getattr(variable, prop, None) if not prop_value: continue self.calculation_to_information( variable.path, prop, prop_value, variable.version, ) if isinstance(prop_value, Calculation): prop_value.default_values = False def calculation_to_information( self, path: str, prop: str, values, version: str, ): """tranform calculation to an information""" one_is_calculation = False if not isinstance(values, list): if not isinstance(values, Calculation): return one_is_calculation = True datas = self.calculation_to_string(path, prop, values, version) else: datas = [] for idx, val in enumerate(values): data = self.calculation_to_string(path, prop, val, version) if data is None: continue if "type" in data or "description" in data: one_is_calculation = True datas.append(data) if one_is_calculation and datas: self.objectspace.informations.add( path, f"{prop}_calculation", datas, ) def calculation_to_string( self, path: str, prop: str, values, version: str, ): if not isinstance(values, Calculation): return {"value": values} if values.description: if not self.regexp_description_get_paths: self.regexp_description_get_paths = compile('"(.*?)"') index = 0 description = values.description variables = [] for r_path in self.regexp_description_get_paths.findall(description): variable, identifiers = self.objectspace.paths.get_with_dynamic( r_path, path, values.version, values.namespace, values.xmlfiles, ) if variable: description = description.replace(f'"{r_path}"', f'{{{index}}}') v = {"path": variable.path, "description": variable.description} if identifiers: v["identifiers"] = identifiers variables.append(v) index += 1 ret = {"description": description} if variables: ret["variables"] = variables return ret if isinstance(values, JinjaCalculation): if values.description: value = values.description else: value = True return { "type": "jinja", "value": value, } if isinstance(values, (VariableCalculation, VariablePropertyCalculation)): variable_path = self.get_path_from_variable(values, version, path) value = self.calculation_to_information_variable( variable_path, values, prop, version, path ) if value is None: return return { "type": "variable", "value": value, "ori_path": variable_path, "optional": values.optional, "propertyerror": values.propertyerror, } if isinstance(values, InformationCalculation): return self.calculation_to_information_information(values, version, path) if isinstance(values, IdentifierPropertyCalculation): ret = {} self.when_to_condition(values, ret) ret["type"] = "identifier" return ret if isinstance(values, IdentifierCalculation): return {"type": "identifier"} if isinstance(values, IndexCalculation): return { "type": "index", "value": True, } if isinstance(values, NamespaceCalculation): return { "type": "namespace", "value": True, } if RisottoCalculation and isinstance(values, RisottoCalculation): # FIXME return { "type": "risotto", "value": True, } raise Exception(f'unknown calculation {type(values)} "{values}"') def calculation_to_information_variable( self, variable_path: str, values, prop: str, version: str, path: str ) -> str: variable, identifiers = self.objectspace.paths.get_with_dynamic( variable_path, path, values.version, values.namespace, values.xmlfiles, ) if not variable or ( isinstance(values, VariableCalculation) and values.optional and not variable ): return None values_calculation = {"path": variable.path} if identifiers: values_calculation["identifiers"] = identifiers if prop in PROPERTY_ATTRIBUTE: # get comparative value self.when_to_condition(values, values_calculation) else: values_calculation["type"] = "variable" return values_calculation def when_to_condition(self, values, values_calculation): if values.when_not is not undefined: values_calculation["type"] = "condition" values_calculation["value"] = values.when_not values_calculation["condition"] = "when_not" elif values.when is not undefined: values_calculation["type"] = "condition" values_calculation["value"] = values.when values_calculation["condition"] = "when" elif values.propertyerror == "transitive": values_calculation["type"] = "transitive" else: values_calculation["type"] = "condition" values_calculation["value"] = True values_calculation["condition"] = "when" def calculation_to_information_information( self, values, version: str, path: str ) -> Union[str, bool]: values_calculation = { "type": "information", "information": values.information, } if values.variable: variable = self.objectspace.paths.get_with_dynamic( values.variable, path, values.version, values.namespace, values.xmlfiles, )[0] if not variable: return None values_calculation["path"] = variable.path return values_calculation def get_path_from_variable(self, values, version: str, path: str) -> str: variable_path = values.variable paths = self.objectspace.paths if version != "1.0" and paths.regexp_relative.search(variable_path): variable_path = paths.get_full_path( variable_path, path, ) elif version == "1.0" and "{{ suffix }}" in variable_path: variable_path = variable_path.replace("{{ suffix }}", "{{ identifier }}") return variable_path