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

723 lines
26 KiB
Python
Raw Normal View History

2024-07-10 21:27:48 +02:00
#!/usr/bin/env python3
"""
Silique (https://www.silique.fr)
2024-11-01 11:17:14 +01:00
Copyright (C) 2024
2024-07-10 21:27:48 +02:00
2024-11-01 11:17:14 +01:00
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 <http://www.gnu.org/licenses/>.
2024-07-10 21:27:48 +02:00
"""
2024-11-01 11:17:14 +01:00
# FIXME si plusieurs example dont le 1er est none tester les autres : tests/dictionaries/00_8test_none
2024-07-10 21:27:48 +02:00
from tiramisu import Calculation
from tiramisu.error import display_list, ConfigError
2024-07-10 21:27:48 +02:00
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
2024-11-01 11:17:14 +01:00
from .i18n import _
2024-07-10 21:27:48 +02:00
ENTER = "\n\n"
DocTypes = {
2024-11-01 11:17:14 +01:00
"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"),
2024-07-10 21:27:48 +02:00
},
},
2024-11-01 11:17:14 +01:00
"number": {
"params": {
"min_number": _("the minimum value is {0}"),
"max_number": _("the maximum value is {0}"),
2024-07-10 21:27:48 +02:00
},
},
2024-11-01 11:17:14 +01:00
"ip": {
"msg": "IP",
"params": {
"cidr": _("IP must be in CIDR format"),
"private_only": _("private IP are allowed"),
"allow_reserved": _("reserved IP are allowed"),
2024-07-10 21:27:48 +02:00
},
},
2024-11-01 11:17:14 +01:00
"hostname": {
"params": {
"allow_ip": _("the host name can be an IP"),
2024-07-10 21:27:48 +02:00
},
},
2024-11-01 11:17:14 +01:00
"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"
),
2024-07-10 21:27:48 +02:00
},
},
2024-11-01 11:17:14 +01:00
"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"),
2024-07-10 21:27:48 +02:00
},
},
"secret": {
"params": {
"min_len": _("minimum length for the secret"),
"max_len": _("maximum length for the secret"),
},
},
2024-07-10 21:27:48 +02:00
}
ROUGAIL_VARIABLE_TYPE = (
"https://rougail.readthedocs.io/en/latest/variable.html#variables-types"
)
class RougailOutputDoc:
2024-11-01 11:17:14 +01:00
def __init__(
self,
*,
config: "Config" = None,
rougailconfig: RougailConfig = None,
**kwarg,
):
2024-07-10 21:27:48 +02:00
if rougailconfig is None:
rougailconfig = RougailConfig
2024-11-01 11:17:14 +01:00
if rougailconfig["step.output"] != "doc":
rougailconfig["step.output"] = "doc"
if rougailconfig["step.output"] != "doc":
raise Exception("doc is not set as step.output")
2024-07-10 21:27:48 +02:00
self.rougailconfig = rougailconfig
outputs = OutPuts().get()
2024-11-01 11:17:14 +01:00
output = self.rougailconfig["doc.output_format"]
2024-07-10 21:27:48 +02:00
if output not in outputs:
2024-11-01 11:17:14 +01:00
raise Exception(
f'cannot find output "{output}", available outputs: {list(outputs)}'
)
2024-07-10 21:27:48 +02:00
if config is None:
rougail = Rougail(self.rougailconfig)
2024-11-01 11:17:14 +01:00
rougail.converted.plugins.append("output_doc")
config = rougail.run()
2024-07-10 21:27:48 +02:00
self.conf = config
2024-11-01 11:17:14 +01:00
self.conf.property.setdefault(frozenset({"advanced"}), "read_write", "append")
2024-07-10 21:27:48 +02:00
self.conf.property.read_write()
self.conf.property.remove("cache")
self.dynamic_paths = {}
self.formater = outputs[output]()
2024-11-01 11:17:14 +01:00
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())
2024-07-10 21:27:48 +02:00
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] = {}
2024-11-01 11:17:14 +01:00
doc = (
self._display_doc(
self.display_families(
namespace,
self.level + 1,
examples_mini[name],
examples_all[name],
),
[],
)
+ "\n"
)
2024-07-10 21:27:48 +02:00
if not examples_mini[name]:
del examples_mini[name]
if not examples_all[name]:
del examples_all[name]
else:
2024-11-01 11:17:14 +01:00
return_string += self.formater.title(
_('Variables for "{0}"').format(namespace.name()), self.level
)
2024-07-10 21:27:48 +02:00
return_string += doc
else:
2024-11-01 11:17:14 +01:00
doc = (
self._display_doc(
self.display_families(
self.conf.unrestraint,
self.level + 1,
examples_mini,
examples_all,
),
[],
)
+ "\n"
)
2024-07-10 21:27:48 +02:00
if examples_all:
2024-11-01 11:17:14 +01:00
return_string += self.formater.title(_("Variables"), self.level)
2024-07-10 21:27:48 +02:00
return_string += doc
if not examples_all:
2024-11-01 11:17:14 +01:00
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
2024-11-01 11:17:14 +01:00
)
return_string += self.formater.yaml(examples_all)
2024-07-10 21:27:48 +02:00
return return_string
def _display_doc(self, variables, add_paths):
2024-11-01 11:17:14 +01:00
return_string = ""
2024-07-10 21:27:48 +02:00
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:
2024-11-01 11:17:14 +01:00
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"]
2024-07-10 21:27:48 +02:00
description = variable["objects"][idx][1][0]
2024-11-01 11:17:14 +01:00
if "{{ identifier }}" in description:
if description.endswith("."):
2024-07-10 21:27:48 +02:00
description = description[:-1]
2024-11-01 11:17:14 +01:00
comment_msg = self.to_phrase(
display_list(
[
description.replace(
"{{ identifier }}",
self.formater.italic(identifier),
)
for identifier in identifiers
],
separator="or",
add_quote=True,
)
)
2024-07-10 21:27:48 +02:00
variable["objects"][idx][1][0] = comment_msg
2024-11-01 11:17:14 +01:00
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"
)
2024-07-10 21:27:48 +02:00
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():
2024-11-01 11:17:14 +01:00
self.dynamic_paths.setdefault(
path, {"paths": [], "identifiers": []}
)["paths"].append(child.path())
self.dynamic_paths[path]["identifiers"].append(
child.identifiers()[-1]
)
2024-07-10 21:27:48 +02:00
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:
2024-11-01 11:17:14 +01:00
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,
)
2024-07-10 21:27:48 +02:00
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:
2024-11-01 11:17:14 +01:00
msg += "\n".join(comment) + ENTER
help_ = self.to_phrase(family.information.get("help", ""))
if help_:
msg += "\n" + help_ + ENTER
2024-07-10 21:27:48 +02:00
if family.isleadership():
help_ = _("This family contains lists of variable blocks.")
msg += "\n" + help_ + ENTER
2024-07-10 21:27:48 +02:00
if isdynamic:
2024-11-01 11:17:14 +01:00
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):
2024-07-10 21:27:48 +02:00
if not isinstance(val, Calculation):
continue
2024-11-01 11:17:14 +01:00
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
2024-07-10 21:27:48 +02:00
return msg
2024-11-01 11:17:14 +01:00
def manage_properties(
self,
variable,
subparameter,
):
2024-07-10 21:27:48 +02:00
properties = variable.property.get(uncalculated=True)
2024-11-01 11:17:14 +01:00
for mode in self.rougailconfig["modes_level"]:
2024-07-10 21:27:48 +02:00
if mode in properties:
subparameter.append((mode, None, None, None))
2024-07-10 21:27:48 +02:00
break
for prop, msg in self.property_to_string:
if prop in properties:
subparameter.append((msg, None, None, None))
2024-11-01 11:17:14 +01:00
elif variable.information.get(f"{prop}_calculation", False):
subparameter.append((msg, msg, self.to_string(variable, prop), None))
2024-07-10 21:27:48 +02:00
2024-11-01 11:17:14 +01:00
def subparameter_to_string(
self,
subparameter,
):
subparameter_str = ""
2024-07-10 21:27:48 +02:00
for param in subparameter:
if param[3]:
subparameter_str += self.formater.link(param[0], param[3]) + " "
2024-07-10 21:27:48 +02:00
else:
if param[1]:
italic = True
else:
italic = False
subparameter_str += self.formater.prop(param[0], italic) + " "
2024-07-10 21:27:48 +02:00
return subparameter_str[:-1]
2024-11-01 11:17:14 +01:00
def subparameter_to_parameter(
self,
subparameter,
comment,
):
2024-07-10 21:27:48 +02:00
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:
2024-11-01 11:17:14 +01:00
return ""
2024-07-10 21:27:48 +02:00
msg = str(msg).strip()
2024-11-01 11:17:14 +01:00
if not msg.endswith("."):
msg += "."
2024-07-10 21:27:48 +02:00
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())]
2024-07-10 21:27:48 +02:00
description = variable.description(uncalculated=True)
comment = [self.to_phrase(description)]
2024-11-01 11:17:14 +01:00
help_ = self.to_phrase(variable.information.get("help", ""))
2024-07-10 21:27:48 +02:00
if help_:
comment.extend(help_.split("\n"))
subparameter = []
2024-11-01 11:17:14 +01:00
self.type_to_string(
variable,
subparameter,
comment,
)
self.manage_properties(
variable,
subparameter,
)
2024-07-10 21:27:48 +02:00
if variable.ismulti():
multi = not variable.isfollower() or variable.issubmulti()
else:
multi = False
if multi:
subparameter.append(("multiple", None, None, None))
2024-07-10 21:27:48 +02:00
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")),
)
2024-07-10 21:27:48 +02:00
warn(warning)
2024-11-01 11:17:14 +01:00
default = self.get_default(
variable,
comment,
)
2024-07-10 21:27:48 +02:00
default_in_choices = False
2024-11-01 11:17:14 +01:00
if variable.information.get("type") == "choice":
2024-07-10 21:27:48 +02:00
choices = variable.value.list(uncalculated=True)
if isinstance(choices, Calculation):
2024-11-01 11:17:14 +01:00
choices = self.to_string(variable, "choice")
2024-07-10 21:27:48 +02:00
if isinstance(choices, list):
for idx, val in enumerate(choices):
if not isinstance(val, Calculation):
if default is not None and val == default:
2024-11-01 11:17:14 +01:00
choices[idx] = str(val) + "" + _("(default)")
2024-07-10 21:27:48 +02:00
default_in_choices = True
continue
2024-11-01 11:17:14 +01:00
choices[idx] = self.to_string(variable, "choice", f"_{idx}")
2024-07-10 21:27:48 +02:00
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}")
2024-11-01 11:17:14 +01:00
self.manage_exemples(
multi,
variable,
examples_all,
examples_mini,
comment,
)
2024-07-10 21:27:48 +02:00
self.subparameter_to_parameter(subparameter, comment)
self.formater.columns(parameter, comment)
return [self.formater.join(parameter), comment]
2024-11-01 11:17:14 +01:00
def get_default(
self,
variable,
comment,
):
if variable.information.get("fake_default", False):
2024-07-10 21:27:48 +02:00
default = None
else:
default = variable.value.get(uncalculated=True)
if default in [None, []]:
default = self.to_string(variable, "default", do_not_raise=True)
2024-07-10 21:27:48 +02:00
if isinstance(default, Calculation):
2024-11-01 11:17:14 +01:00
default = self.to_string(variable, "default")
2024-07-10 21:27:48 +02:00
if isinstance(default, list):
for idx, val in enumerate(default):
if not isinstance(val, Calculation):
continue
2024-11-01 11:17:14 +01:00
default[idx] = self.to_string(variable, "default", f"_{idx}")
2024-07-10 21:27:48 +02:00
default = self.formater.list(default)
return default
2024-11-01 11:17:14 +01:00
def to_string(
self,
variable,
prop,
identifier="",
do_not_raise=False,
2024-11-01 11:17:14 +01:00
):
calculation_type = variable.information.get(
f"{prop}_calculation_type{identifier}", None
)
2024-07-10 21:27:48 +02:00
if not calculation_type:
if do_not_raise:
return
2024-11-01 11:17:14 +01:00
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
2024-11-01 11:17:14 +01:00
calculation = variable.information.get(f"{prop}_calculation{identifier}")
if calculation_type == "jinja":
2024-07-10 21:27:48 +02:00
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")),
)
2024-07-10 21:27:48 +02:00
warn(warning)
2024-11-01 11:17:14 +01:00
elif calculation_type == "variable":
2024-07-10 21:27:48 +02:00
if prop in PROPERTY_ATTRIBUTE:
values = self.formater.to_string(calculation)
else:
2024-11-01 11:17:14 +01:00
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
2024-07-10 21:27:48 +02:00
else:
values = _("the value of the {0}").format(calculation_type)
2024-11-01 11:17:14 +01:00
if not values.endswith("."):
values += "."
2024-07-10 21:27:48 +02:00
return values
2024-11-01 11:17:14 +01:00
def type_to_string(
self,
variable,
subparameter,
comment,
):
2024-07-10 21:27:48 +02:00
variable_type = variable.information.get("type")
2024-11-01 11:17:14 +01:00
doc_type = DocTypes.get(variable_type, {"params": {}})
subparameter.append(
(
doc_type.get("msg", variable_type),
2024-11-01 11:17:14 +01:00
None,
None,
ROUGAIL_VARIABLE_TYPE,
2024-11-01 11:17:14 +01:00
)
)
2024-07-10 21:27:48 +02:00
option = variable.get()
validators = []
2024-11-01 11:17:14 +01:00
for param, msg in doc_type["params"].items():
value = option.impl_get_extra(f"_{param}")
2024-07-10 21:27:48 +02:00
if value is None:
value = option.impl_get_extra(param)
if value is not None and value is not False:
2024-11-01 11:17:14 +01:00
validators.append(msg.format(value))
valids = [
name
for name in variable.information.list()
if name.startswith("validators_calculation_type_")
]
2024-07-10 21:27:48 +02:00
if valids:
for idx in range(len(valids)):
2024-11-01 11:17:14 +01:00
validators.append(
self.to_string(
variable,
"validators",
f"_{idx}",
)
)
2024-07-10 21:27:48 +02:00
if validators:
if len(validators) == 1:
comment.append(
self.formater.bold(_("Validator")) + _(": ") + validators[0]
)
2024-07-10 21:27:48 +02:00
else:
2024-11-01 11:17:14 +01:00
comment.append(
self.formater.bold(_("Validators"))
+ _(": ")
+ self.formater.list(validators)
2024-11-01 11:17:14 +01:00
)
2024-07-10 21:27:48 +02:00
2024-11-01 11:17:14 +01:00
def manage_exemples(
self,
multi,
variable,
examples_all,
examples_mini,
comment,
):
2024-07-10 21:27:48 +02:00
example_mini = None
example_all = None
2024-11-01 11:17:14 +01:00
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
2024-07-10 21:27:48 +02:00
if isinstance(example, tuple):
example = list(example)
mandatory = "mandatory" in variable.property.get(
uncalculated=True
) and not variable.value.get(uncalculated=True)
2024-07-10 21:27:48 +02:00
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:
2024-11-01 11:17:14 +01:00
title = _("Example")
2024-07-10 21:27:48 +02:00
comment.append(f"{self.formater.bold(title)}: {example}")
elif default not in [None, []]:
example_all = default
else:
2024-11-01 11:17:14 +01:00
example = CONVERT_OPTION.get(variable.information.get("type"), {}).get(
"example", None
)
2024-07-10 21:27:48 +02:00
if example is None:
2024-11-01 11:17:14 +01:00
example = "xxx"
2024-07-10 21:27:48 +02:00
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
2024-11-01 11:17:14 +01:00
RougailOutput = RougailOutputDoc
__all__ = ("RougailOutputDoc",)