#!/usr/bin/env python3 """ Silique (https://www.silique.fr) Copyright (C) 2022-2024 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 """ #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 import tabulate as tabulate_module from tabulate import tabulate from warnings import warn from typing import Optional from gettext import gettext as _ from rougail.error import display_xmlfiles from rougail import RougailConfig, Rougail, CONVERT_OPTION from rougail.object_model import PROPERTY_ATTRIBUTE from .config import OutPuts 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 only 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 {value}'), 'max_number': _('the maximum value is {value}'), }, }, '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'), }, }, } ROUGAIL_VARIABLE_TYPE = ( "https://rougail.readthedocs.io/en/latest/variable.html#variables-types" ) class RougailOutputDoc: def __init__(self, *, config: 'Config'=None, rougailconfig: RougailConfig=None, ): if rougailconfig is None: rougailconfig = RougailConfig 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.get_config() 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 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(_(f'Variables for "{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(_(f'Variables'), self.level) return_string += doc if not examples_all: return '' if examples_mini: #"Exemple avec les variables obligatoires non renseignées" return_string += self.formater.title( _("Example with mandatory variables not filled in"), self.level ) return_string += self.formater.yaml(examples_mini) if examples_all: #"Exemple avec tous les variables modifiables" 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) suffixes = self.dynamic_paths[path]['suffixes'] description = variable["objects"][idx][1][0] if "{{ suffix }}" in description: if description.endswith('.'): description = description[:-1] comment_msg = self.to_phrase(display_list([description.replace('{{ suffix }}', self.formater.italic(suffix)) for suffix in suffixes], 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': [], 'suffixes': []})['paths'].append(child.path()) self.dynamic_paths[path]['suffixes'].append(child.suffixes()[-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: suffixes = family.suffixes(only_self=True) if '{{ suffix }}' in title: title = display_list([title.replace('{{ suffix }}', self.formater.italic(suffix)) for suffix in suffixes], 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 = "Cette famille contient des listes de bloc de variables." help = "This family contains lists of variable blocks." msg += "\n" + help + ENTER if isdynamic: suffixes = family.suffixes(only_self=True , uncalculated=True) if isinstance(suffixes, Calculation): suffixes = self.to_string(family, 'dynamic') if isinstance(suffixes, list): for idx, val in enumerate(suffixes): if not isinstance(val, Calculation): continue suffixes[idx] = self.to_string(family, 'dynamic', f'_{idx}') suffixes = self.formater.list(suffixes) #help = f"Cette famille construit des familles dynamiquement.\n\n{self.formater.bold('Suffixes')}: {suffixes}" help = f"This family builds families dynamically.\n\n{self.formater.bold('Suffixes')}: {suffixes}" 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((self.formater.prop(mode), None, None)) break for prop, msg in self.property_to_string: if prop in properties: subparameter.append((self.formater.prop(msg), None, None)) elif variable.information.get(f'{prop}_calculation', False): subparameter.append((self.formater.prop(msg), msg, self.to_string(variable, prop))) def subparameter_to_string(self, subparameter, ): subparameter_str = '' for param in subparameter: if param[1]: subparameter_str += f"_{param[0]}_ " else: subparameter_str += f"{param[0]} " 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 = [f"{self.formater.bold(variable.path())}"] subparameter = [] description = variable.description(uncalculated=True) comment = [self.to_phrase(description)] help_ = self.to_phrase(variable.information.get("help", '')) if help_: comment.append(help_) 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((self.formater.prop("multiple"), None, None)) if subparameter: parameter.append(self.subparameter_to_string(subparameter)) if variable.name() == description: warning = f'No attribute "description" for variable "{variable.path()}" in {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, []]: return 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, suffix='', ): calculation_type = variable.information.get(f'{prop}_calculation_type{suffix}', None) if not calculation_type: raise Exception(f'cannot find {prop}_calculation_type{suffix} information, do you have declare doc has a plugins?') calculation = variable.information.get(f'{prop}_calculation{suffix}') if calculation_type == 'jinja': if calculation is not True: values = self.formater.to_string(calculation) else: values = "issu d'un calcul" warning = f'"{prop}" is a calculation for {variable.path()} but has no description in {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 = _(f'the value of the variable "{calculation}"') else: values = _(f"value of the {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((self.formater.link(doc_type.get('msg', variable_type), ROUGAIL_VARIABLE_TYPE), None)) 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=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(f'{self.formater.bold("Validator")}: ' + validators[0]) else: comment.append(f'{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("test", None) default = variable.value.get() if isinstance(example, tuple): example = list(example) mandatory = 'mandatory' in variable.property.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 = _("Exemple") 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