#!/usr/bin/env python3 """ Silique (https://www.silique.fr) Copyright (C) 2024 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 . """ # FIXME si plusieurs example dont le 1er est none tester les autres : tests/dictionaries/00_8test_none from tiramisu import Calculation from tiramisu.error import display_list, ConfigError import tabulate as tabulate_module from tabulate import tabulate from warnings import warn from typing import Optional from rougail.error import display_xmlfiles from rougail import RougailConfig, Rougail, CONVERT_OPTION from rougail.object_model import PROPERTY_ATTRIBUTE from .config import OutPuts from .i18n import _ ENTER = "\n\n" DocTypes = { "domainname": { "params": { "allow_startswith_dot": _("the domain name can starts by a dot"), "allow_without_dot": _("the domain name can be a hostname"), "allow_ip": _("the domain name can be an IP"), "allow_cidr_network": _("the domain name can be network in CIDR format"), }, }, "number": { "params": { "min_number": _("the minimum value is {0}"), "max_number": _("the maximum value is {0}"), }, }, "ip": { "msg": "IP", "params": { "cidr": _("IP must be in CIDR format"), "private_only": _("private IP are allowed"), "allow_reserved": _("reserved IP are allowed"), }, }, "hostname": { "params": { "allow_ip": _("the host name can be an IP"), }, }, "web_address": { "params": { "allow_ip": _("the domain name in web address can be an IP"), "allow_without_dot": _( "the domain name in web address can be only a hostname" ), }, }, "port": { "params": { "allow_range": _("can be range of port"), "allow_protocol": _("can have the protocol"), "allow_zero": _("port 0 is allowed"), "allow_wellknown": _("ports 1 to 1023 are allowed"), "allow_registred": _("ports 1024 to 49151 are allowed"), "allow_private": _("ports greater than 49152 are allowed"), }, }, "secret": { "params": { "min_len": _("minimum length for the secret"), "max_len": _("maximum length for the secret"), }, }, } ROUGAIL_VARIABLE_TYPE = ( "https://rougail.readthedocs.io/en/latest/variable.html#variables-types" ) class RougailOutputDoc: def __init__( self, *, config: "Config" = None, rougailconfig: RougailConfig = None, **kwarg, ): if rougailconfig is None: rougailconfig = RougailConfig if rougailconfig["step.output"] != "doc": rougailconfig["step.output"] = "doc" if rougailconfig["step.output"] != "doc": raise Exception("doc is not set as step.output") self.rougailconfig = rougailconfig outputs = OutPuts().get() output = self.rougailconfig["doc.output_format"] if output not in outputs: raise Exception( f'cannot find output "{output}", available outputs: {list(outputs)}' ) if config is None: rougail = Rougail(self.rougailconfig) rougail.converted.plugins.append("output_doc") config = rougail.run() self.conf = config self.conf.property.setdefault(frozenset({"advanced"}), "read_write", "append") self.conf.property.read_write() self.conf.property.remove("cache") self.dynamic_paths = {} self.formater = outputs[output]() self.level = self.rougailconfig["doc.title_level"] # self.property_to_string = [('mandatory', 'obligatoire'), ('hidden', 'cachée'), ('disabled', 'désactivée'), ('unique', 'unique'), ('force_store_value', 'modifié automatiquement')] self.property_to_string = [ ("mandatory", _("mandatory")), ("hidden", _("hidden")), ("disabled", _("disabled")), ("unique", _("unique")), ("force_store_value", _("auto modified")), ] def run(self): print(self.gen_doc()) def gen_doc(self): tabulate_module.PRESERVE_WHITESPACE = True examples_mini = {} examples_all = {} return_string = self.formater.header() if self.rougailconfig["main_namespace"]: for namespace in self.conf.unrestraint.list(): name = namespace.name() examples_mini[name] = {} examples_all[name] = {} doc = ( self._display_doc( self.display_families( namespace, self.level + 1, examples_mini[name], examples_all[name], ), [], ) + "\n" ) if not examples_mini[name]: del examples_mini[name] if not examples_all[name]: del examples_all[name] else: return_string += self.formater.title( _('Variables for "{0}"').format(namespace.name()), self.level ) return_string += doc else: doc = ( self._display_doc( self.display_families( self.conf.unrestraint, self.level + 1, examples_mini, examples_all, ), [], ) + "\n" ) if examples_all: return_string += self.formater.title(_("Variables"), self.level) return_string += doc if not examples_all: return "" if self.rougailconfig["doc.with_example"]: if examples_mini: return_string += self.formater.title( _("Example with mandatory variables not filled in"), self.level ) return_string += self.formater.yaml(examples_mini) if examples_all: return_string += self.formater.title( _("Example with all variables modifiable"), self.level ) return_string += self.formater.yaml(examples_all) return return_string def _display_doc(self, variables, add_paths): return_string = "" for variable in variables: typ = variable["type"] path = variable["path"] if path in add_paths: continue if typ == "family": return_string += variable["title"] return_string += self._display_doc(variable["objects"], add_paths) else: for idx, path in enumerate(variable["paths"]): if path in self.dynamic_paths: paths_msg = display_list( [ self.formater.bold(path_) for path_ in self.dynamic_paths[path]["paths"] ], separator="or", ) variable["objects"][idx][0] = variable["objects"][idx][ 0 ].replace("{{ ROUGAIL_PATH }}", paths_msg) identifiers = self.dynamic_paths[path]["identifiers"] description = variable["objects"][idx][1][0] if "{{ identifier }}" in description: if description.endswith("."): description = description[:-1] comment_msg = self.to_phrase( display_list( [ description.replace( "{{ identifier }}", self.formater.italic(identifier), ) for identifier in identifiers ], separator="or", add_quote=True, ) ) variable["objects"][idx][1][0] = comment_msg variable["objects"][idx][1] = self.formater.join( variable["objects"][idx][1] ) return_string += ( self.formater.table( tabulate( variable["objects"], headers=self.formater.table_header( ["Variable", "Description"] ), tablefmt=self.formater.name, ) ) + "\n\n" ) add_paths.append(path) return return_string def is_hidden(self, child): properties = child.property.get(uncalculated=True) for hidden_property in ["hidden", "disabled", "advanced"]: if hidden_property in properties: return True return False def display_families( self, family, level, examples_mini, examples_all, ): variables = [] for child in family.list(): if self.is_hidden(child): continue if not child.isoptiondescription(): if child.isfollower() and child.index() != 0: # only add to example self.display_variable( child, examples_mini, examples_all, ) continue path = child.path(uncalculated=True) if child.isdynamic(): self.dynamic_paths.setdefault( path, {"paths": [], "identifiers": []} )["paths"].append(child.path()) self.dynamic_paths[path]["identifiers"].append( child.identifiers()[-1] ) if not variables or variables[-1]["type"] != "variables": variables.append( { "type": "variables", "objects": [], "path": path, "paths": [], } ) variables[-1]["objects"].append( self.display_variable( child, examples_mini, examples_all, ) ) variables[-1]["paths"].append(path) else: name = child.name() if child.isleadership(): examples_mini[name] = [] examples_all[name] = [] else: examples_mini[name] = {} examples_all[name] = {} variables.append( { "type": "family", "title": self.display_family( child, level, ), "path": child.path(uncalculated=True), "objects": self.display_families( child, level + 1, examples_mini[name], examples_all[name], ), } ) if not examples_mini[name]: del examples_mini[name] if not examples_all[name]: del examples_all[name] return variables def display_family( self, family, level, ): if family.name() != family.description(uncalculated=True): title = f"{family.description(uncalculated=True)}" else: warning = f'No attribute "description" for family "{family.path()}" in {display_xmlfiles(family.information.get("dictionaries"))}' warn(warning) title = f"{family.path()}" isdynamic = family.isdynamic(only_self=True) if isdynamic: identifiers = family.identifiers(only_self=True) if "{{ identifier }}" in title: title = display_list( [ title.replace( "{{ identifier }}", self.formater.italic(identifier) ) for identifier in identifiers ], separator="or", add_quote=True, ) msg = self.formater.title(title, level) subparameter = [] self.manage_properties(family, subparameter) if subparameter: msg += self.subparameter_to_string(subparameter) + ENTER comment = [] self.subparameter_to_parameter(subparameter, comment) if comment: msg += "\n".join(comment) + ENTER help_ = self.to_phrase(family.information.get("help", "")) if help_: msg += "\n" + help_ + ENTER if family.isleadership(): help_ = _("This family contains lists of variable blocks.") msg += "\n" + help_ + ENTER if isdynamic: identifiers = family.identifiers(only_self=True, uncalculated=True) if isinstance(identifiers, Calculation): identifiers = self.to_string(family, "dynamic") if isinstance(identifiers, list): for idx, val in enumerate(identifiers): if not isinstance(val, Calculation): continue identifiers[idx] = self.to_string(family, "dynamic", f"_{idx}") identifiers = self.formater.list(identifiers) help_ = _("This family builds families dynamically.\n\n{0}: {1}").format( self.formater.bold("Identifiers"), identifiers ) msg += "\n" + help_ + ENTER return msg def manage_properties( self, variable, subparameter, ): properties = variable.property.get(uncalculated=True) for mode in self.rougailconfig["modes_level"]: if mode in properties: subparameter.append((mode, None, None, None)) break for prop, msg in self.property_to_string: if prop in properties: subparameter.append((msg, None, None, None)) elif variable.information.get(f"{prop}_calculation", False): subparameter.append((msg, msg, self.to_string(variable, prop), None)) def subparameter_to_string( self, subparameter, ): subparameter_str = "" for param in subparameter: if param[3]: subparameter_str += self.formater.link(param[0], param[3]) + " " else: if param[1]: italic = True else: italic = False subparameter_str += self.formater.prop(param[0], italic) + " " return subparameter_str[:-1] def subparameter_to_parameter( self, subparameter, comment, ): for param in subparameter: if not param[1]: continue msg = param[2] comment.append(f"{self.formater.bold(param[1].capitalize())}: {msg}") def to_phrase(self, msg): if not msg: return "" msg = str(msg).strip() if not msg.endswith("."): msg += "." return msg[0].upper() + msg[1:] def display_variable( self, variable, examples_mini, examples_all, ): if variable.isdynamic(): parameter = ["{{ ROUGAIL_PATH }}"] else: parameter = [self.formater.bold(variable.path())] description = variable.description(uncalculated=True) comment = [self.to_phrase(description)] help_ = self.to_phrase(variable.information.get("help", "")) if help_: comment.extend(help_.split("\n")) subparameter = [] self.type_to_string( variable, subparameter, comment, ) self.manage_properties( variable, subparameter, ) if variable.ismulti(): multi = not variable.isfollower() or variable.issubmulti() else: multi = False if multi: subparameter.append(("multiple", None, None, None)) if subparameter: parameter.append(self.subparameter_to_string(subparameter)) if variable.name() == description: warning = _('No attribute "description" for variable "{0}" in {1}').format( variable.path(), display_xmlfiles(variable.information.get("dictionaries")), ) warn(warning) default = self.get_default( variable, comment, ) default_in_choices = False if variable.information.get("type") == "choice": choices = variable.value.list(uncalculated=True) if isinstance(choices, Calculation): choices = self.to_string(variable, "choice") if isinstance(choices, list): for idx, val in enumerate(choices): if not isinstance(val, Calculation): if default is not None and val == default: choices[idx] = str(val) + " ← " + _("(default)") default_in_choices = True continue choices[idx] = self.to_string(variable, "choice", f"_{idx}") choices = self.formater.list(choices) comment.append(f'{self.formater.bold(_("Choices"))}: {choices}') # choice if default is not None and not default_in_choices: comment.append(f"{self.formater.bold(_('Default'))}: {default}") self.manage_exemples( multi, variable, examples_all, examples_mini, comment, ) self.subparameter_to_parameter(subparameter, comment) self.formater.columns(parameter, comment) return [self.formater.join(parameter), comment] def get_default( self, variable, comment, ): if variable.information.get("fake_default", False): default = None else: default = variable.value.get(uncalculated=True) if default in [None, []]: default = self.to_string(variable, "default", do_not_raise=True) if isinstance(default, Calculation): default = self.to_string(variable, "default") if isinstance(default, list): for idx, val in enumerate(default): if not isinstance(val, Calculation): continue default[idx] = self.to_string(variable, "default", f"_{idx}") default = self.formater.list(default) return default def to_string( self, variable, prop, identifier="", do_not_raise=False, ): calculation_type = variable.information.get( f"{prop}_calculation_type{identifier}", None ) if not calculation_type: if do_not_raise: return raise Exception( f"cannot find {prop}_calculation_type{identifier} information, do you have declare doc has a plugins?" ) if do_not_raise and variable.information.get( f"{prop}_calculation_optional{identifier}", False ): return calculation = variable.information.get(f"{prop}_calculation{identifier}") if calculation_type == "jinja": if calculation is not True: values = self.formater.to_string(calculation) else: values = _("depends on a calculation") warning = _( '"{0}" is a calculation for {1} but has no description in {2}' ).format( prop, variable.path(), display_xmlfiles(variable.information.get("dictionaries")), ) warn(warning) elif calculation_type == "variable": if prop in PROPERTY_ATTRIBUTE: values = self.formater.to_string(calculation) else: values = _('the value of the variable "{0}"').format(calculation) elif calculation_type == "identifier": if prop in PROPERTY_ATTRIBUTE: values = self.formater.to_string(calculation) else: values = _("the value of the identifier") elif calculation_type == "information": values = calculation else: values = _("the value of the {0}").format(calculation_type) if not values.endswith("."): values += "." return values def type_to_string( self, variable, subparameter, comment, ): variable_type = variable.information.get("type") doc_type = DocTypes.get(variable_type, {"params": {}}) subparameter.append( ( doc_type.get("msg", variable_type), None, None, ROUGAIL_VARIABLE_TYPE, ) ) option = variable.get() validators = [] 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: validators.append(msg.format(value)) valids = [ name for name in variable.information.list() if name.startswith("validators_calculation_type_") ] if valids: for idx in range(len(valids)): validators.append( self.to_string( variable, "validators", f"_{idx}", ) ) if validators: if len(validators) == 1: comment.append( self.formater.bold(_("Validator")) + _(": ") + validators[0] ) else: comment.append( self.formater.bold(_("Validators")) + _(": ") + self.formater.list(validators) ) def manage_exemples( self, multi, variable, examples_all, examples_mini, comment, ): example_mini = None example_all = None example = variable.information.get("examples", None) if example is None: example = variable.information.get("test", None) try: default = variable.value.get() except ConfigError: default = None if isinstance(example, tuple): example = list(example) mandatory = "mandatory" in variable.property.get( uncalculated=True ) and not variable.value.get(uncalculated=True) if example: if not multi: example = example[0] title = _("Example") if mandatory: example_mini = example example_all = example else: if mandatory: example_mini = "\n - example" example_all = example len_test = len(example) example = self.formater.list(example) if len_test > 1: title = _("Examples") else: title = _("Example") comment.append(f"{self.formater.bold(title)}: {example}") elif default not in [None, []]: example_all = default else: example = CONVERT_OPTION.get(variable.information.get("type"), {}).get( "example", None ) if example is None: example = "xxx" if multi: example = [example] if mandatory: example_mini = example example_all = example if variable.isleader(): if example_mini is not None: for mini in example_mini: examples_mini.append({variable.name(): mini}) if example_all is not None: for mall in example_all: examples_all.append({variable.name(): mall}) elif variable.isfollower(): if example_mini is not None: for idx in range(0, len(examples_mini)): examples_mini[idx][variable.name()] = example_mini if example_all is not None: for idx in range(0, len(examples_all)): examples_all[idx][variable.name()] = example_all else: if example_mini is not None: examples_mini[variable.name()] = example_mini examples_all[variable.name()] = example_all RougailOutput = RougailOutputDoc __all__ = ("RougailOutputDoc",)