rougail-output-doc/src/rougail/output_doc/__init__.py

581 lines
24 KiB
Python
Raw Normal View History

2024-07-10 21:27:48 +02:00
#!/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