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

932 lines
36 KiB
Python
Raw Normal View History

2024-11-15 08:13:45 +01:00
"""
Silique (https://www.silique.fr)
2025-02-10 09:52:12 +01:00
Copyright (C) 2024-2025
2024-11-15 08:13:45 +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/>.
"""
from warnings import warn
from typing import Optional
from itertools import chain
2025-04-25 14:17:07 +02:00
from re import compile
2024-11-15 08:13:45 +01:00
2025-05-09 07:45:10 +02:00
from tiramisu import Calculation, groups
from tiramisu.error import ConfigError, display_list, PropertiesOptionError
2025-06-18 06:42:33 +02:00
from rougail.tiramisu import display_xmlfiles, normalize_family
from rougail.utils import undefined, get_properties_to_string, PROPERTY_ATTRIBUTE
2025-10-02 22:43:25 +02:00
from rougail.error import VariableCalculationDependencyError, RougailWarning
2024-11-15 08:13:45 +01:00
from .config import OutPuts
from .i18n import _
2025-10-14 12:58:39 +02:00
from .utils import DocTypes, dump, to_phrase, calc_path
2024-11-15 08:13:45 +01:00
from .example import Examples
2025-10-14 12:58:39 +02:00
from .changelog import Changelog
2024-11-15 08:13:45 +01:00
HIDDEN_PROPERTIES = [
2025-10-15 09:44:22 +02:00
"hidden",
"disabled",
]
2025-10-14 12:58:39 +02:00
class RougailOutputDoc(Examples, Changelog):
2024-11-15 08:13:45 +01:00
"""Rougail Output Doc:
Generate documentation from rougail description files
"""
def __init__(
self,
2024-12-02 20:21:31 +01:00
config: "Config",
2024-11-15 08:13:45 +01:00
*,
rougailconfig: "RougailConfig" = None,
**kwarg,
):
# Import here to avoid circular import
2025-06-18 15:50:11 +02:00
from rougail.tiramisu import CONVERT_OPTION
2024-11-15 08:13:45 +01:00
self.convert_option = CONVERT_OPTION
if rougailconfig is None:
from rougail import RougailConfig
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.outputs = OutPuts().get()
2024-11-15 08:13:45 +01:00
self.conf = config
self.modes_level = rougailconfig["modes_level"]
if self.modes_level:
self.disabled_modes = rougailconfig["doc.disabled_modes"]
if self.disabled_modes:
self.conf.property.setdefault(
frozenset(self.disabled_modes), "read_write", "append"
)
else:
self.disabled_modes = []
2024-11-15 08:13:45 +01:00
self.conf.property.read_write()
# self.conf.property.remove("cache")
self.rougailconfig = rougailconfig
2024-11-15 08:13:45 +01:00
self.informations = None
try:
groups.namespace
self.support_namespace = True
except AttributeError:
self.support_namespace = False
self.property_to_string = get_properties_to_string()
2025-11-03 09:03:59 +01:00
self.formatter = None
2024-11-15 08:13:45 +01:00
super().__init__()
def run(self) -> str:
2024-11-15 08:13:45 +01:00
"""Print documentation in stdout"""
2025-10-14 13:50:02 +02:00
self.load()
2025-11-03 09:03:59 +01:00
self.load_formatter()
2025-10-15 09:44:22 +02:00
return_string = ""
contents = self.rougailconfig["doc.contents"]
if "variables" in contents:
return_string += self.formatter.run(self.informations)
if "example" in contents:
2025-10-14 12:58:39 +02:00
return_string += self.gen_doc_examples()
if "changelog" in contents:
2025-10-14 12:58:39 +02:00
return_string += self.gen_doc_changelog()
2025-02-10 09:52:12 +01:00
return True, return_string
2024-11-15 08:13:45 +01:00
2025-11-03 09:03:59 +01:00
def load_formatter(self) -> str:
output_format = self.rougailconfig["doc.output_format"]
self.formatter = self.outputs[output_format](self.rougailconfig)
2025-01-04 11:52:37 +01:00
def print(self) -> None:
2025-02-10 09:52:12 +01:00
ret, data = self.run()
print(data)
return ret
2025-01-04 11:52:37 +01:00
2025-10-18 06:40:23 +02:00
def load(self):
2025-10-14 12:58:39 +02:00
self.dynamic_paths = {}
2024-11-15 08:13:45 +01:00
config = self.conf.unrestraint
2025-10-18 06:40:23 +02:00
self.populate_dynamics(config=config)
2025-10-16 08:19:03 +02:00
informations = self.parse_families(config)
2024-11-15 08:13:45 +01:00
if informations is None:
informations = {}
self.informations = informations
2025-10-18 06:40:23 +02:00
def populate_dynamics(self, *, config=None, reload=False):
if config is None:
config = self.conf.unrestraint
self._populate_dynamics(config, reload)
def _populate_dynamics(self, family, reload, uncalculated=False) -> None:
def populate(child, uncalculated):
2025-10-14 12:58:39 +02:00
if child.isoptiondescription():
type_ = "family"
2024-11-15 08:13:45 +01:00
else:
2025-10-14 12:58:39 +02:00
type_ = "variable"
if child.isdynamic():
2025-10-18 06:40:23 +02:00
self.populate_dynamic(child, type_, reload, uncalculated)
2025-10-18 11:27:22 +02:00
if child.isoptiondescription():
2025-10-18 06:40:23 +02:00
self._populate_dynamics(child, reload, uncalculated)
for child in family.list(uncalculated=uncalculated):
populate(child, uncalculated)
if not uncalculated:
for child in family.list(uncalculated=True):
if child.isdynamic() and child.path(uncalculated=True) not in self.dynamic_paths:
populate(family, uncalculated=True)
def populate_dynamic(self, obj, type_, reload, uncalculated) -> None:
2025-10-16 08:19:03 +02:00
path = obj.path(uncalculated=True)
2024-11-15 08:13:45 +01:00
if path not in self.dynamic_paths:
2025-10-14 12:58:39 +02:00
new_name = True
description = obj.description(uncalculated=True)
name = obj.name(uncalculated=True)
2025-10-15 09:44:22 +02:00
self.dynamic_paths[path] = {
"names": [],
"identifiers": [],
"path": path,
}
2025-10-14 12:58:39 +02:00
if not obj.information.get("forced_description", False):
2025-10-15 09:44:22 +02:00
self.dynamic_paths[path]["description"] = self._convert_description(
2025-10-29 10:46:57 +01:00
description, type_, its_a_path=False
2025-10-15 09:44:22 +02:00
)
2025-10-14 12:58:39 +02:00
elif obj.isoptiondescription():
2025-10-15 09:44:22 +02:00
self.dynamic_paths[path]["description"] = self._convert_description(
2025-10-29 10:46:57 +01:00
description, type_, its_a_path=True
2025-10-15 09:44:22 +02:00
)
2025-10-18 06:40:23 +02:00
if uncalculated:
return
2025-10-14 12:58:39 +02:00
dynamic_obj = self.dynamic_paths[path]
2025-10-16 08:19:03 +02:00
if reload and obj.identifiers() in dynamic_obj["identifiers"]:
return
2025-10-14 12:58:39 +02:00
dynamic_obj["names"].append(obj.name())
dynamic_obj["identifiers"].append(obj.identifiers())
2024-11-15 08:13:45 +01:00
2025-10-16 08:19:03 +02:00
def parse_families(self, family) -> dict:
2024-11-15 08:13:45 +01:00
informations = {}
leader = None
for child in family.list():
2025-10-18 06:40:23 +02:00
if self.is_inaccessible_user_data(child):
2024-11-15 08:13:45 +01:00
continue
if child.type(only_self=True) == "symlink":
2025-01-04 11:52:37 +01:00
continue
2024-11-15 08:13:45 +01:00
if not child.isoptiondescription():
2025-10-16 08:19:03 +02:00
leader = self.parse_variable(child, leader, informations)
2024-11-15 08:13:45 +01:00
else:
2025-10-16 08:19:03 +02:00
self.parse_family(child, informations)
2024-11-15 08:13:45 +01:00
return informations
2025-10-18 06:40:23 +02:00
def is_inaccessible_user_data(self, child):
2024-11-15 08:13:45 +01:00
"""If family is not accessible in read_write mode (to load user_data),
do not comment this family
"""
properties = child.property.get(uncalculated=True)
for hidden_property in HIDDEN_PROPERTIES:
2024-11-15 08:13:45 +01:00
if hidden_property in properties:
return True
calculation = child.information.get(f"{hidden_property}_calculation", None)
2025-05-11 19:13:12 +02:00
if calculation and calculation.get("type") == "variable":
variable_path, value, condition = calculation["value"]
variable = self.conf.forcepermissive.option(variable_path)
try:
variable.value.get()
except AttributeError:
variable = None
if variable and self.is_inaccessible_user_data(variable):
try:
variable_value = self._get_unmodified_default_value(variable)
except VariableCalculationDependencyError:
pass
else:
if (condition == "when" and value == variable_value) or (condition == "when_not" and value != variable_value):
return True
2025-02-17 20:53:58 +01:00
if not child.isoptiondescription():
for hidden_property in self.disabled_modes:
if hidden_property in properties:
return True
2024-11-15 08:13:45 +01:00
return False
2025-10-18 06:40:23 +02:00
def parse_family(self, family, informations: dict, *, force_injection=False) -> None:
2025-10-16 08:19:03 +02:00
path = family.path(uncalculated=True)
name = family.name(uncalculated=True)
sub_informations = self.parse_families(family)
2025-10-18 06:40:23 +02:00
if not force_injection and not sub_informations:
2024-11-15 08:13:45 +01:00
return
2025-10-15 09:44:22 +02:00
# if self.with_family:
2025-10-14 12:58:39 +02:00
family_informations = self._populate_family(
2025-10-15 09:44:22 +02:00
family,
path,
)
2025-10-14 12:58:39 +02:00
if family_informations is not False:
informations[name] = {
"type": self._get_family_type(family),
"informations": family_informations,
"children": sub_informations,
}
2025-10-15 09:44:22 +02:00
2025-09-22 09:42:46 +02:00
def parse_variable(
self,
variable,
leader: dict,
informations: dict,
*,
only_one=False,
2024-11-15 08:13:45 +01:00
) -> Optional[dict]:
2025-10-16 08:19:03 +02:00
path = variable.path(uncalculated=True)
name = variable.name(uncalculated=True)
potential_leader = None
2024-11-15 08:13:45 +01:00
if variable.isdynamic():
# information is already set
2024-11-20 21:12:56 +01:00
potential_leader = self._parse_variable_dynamic(
2025-09-22 09:42:46 +02:00
variable, leader, name, path, informations, only_one
2024-11-15 08:13:45 +01:00
)
2025-10-26 15:23:40 +01:00
elif variable.isfollower() and variable.index():
self._parse_variable_follower_with_index(
variable, leader, name, informations
)
2024-11-15 08:13:45 +01:00
else:
2025-10-26 15:23:40 +01:00
potential_leader = self.parse_variable_normal(
variable, leader, name, path, informations
)
2024-11-15 08:13:45 +01:00
if potential_leader:
leader = potential_leader
return leader
2025-10-16 08:19:03 +02:00
def parse_variable_normal(
2024-11-15 08:13:45 +01:00
self, variable, leader, name: str, path: str, informations: dict
) -> Optional[dict]:
if variable.isdynamic():
sub_informations = self.dynamic_paths[path]
elif variable.isfollower() and path in informations: # variable.index():
sub_informations = informations[name]
2024-11-15 08:13:45 +01:00
else:
sub_informations = {}
if not self._populate_variable(
2024-11-15 08:13:45 +01:00
variable,
sub_informations,
2025-10-15 09:44:22 +02:00
):
return None
self._add_examples(variable, sub_informations, leader)
informations[name] = sub_informations
2024-11-15 08:13:45 +01:00
if variable.isleader():
return sub_informations
return None
def _parse_variable_follower_with_index(
2025-10-16 08:19:03 +02:00
self, variable, leader: dict, name: str, informations: dict
2024-11-15 08:13:45 +01:00
) -> None:
if (variable.index() + 1) > len(leader["gen_examples"][-1]):
return
informations[name]["gen_examples"][-1][variable.index()] = self._get_example(
variable, informations[name], None
2024-11-15 08:13:45 +01:00
)
2024-11-20 21:12:56 +01:00
def _parse_variable_dynamic(
2025-09-22 09:42:46 +02:00
self, variable, leader, name, path, informations, only_one
2024-11-20 21:12:56 +01:00
) -> None:
2025-10-16 08:19:03 +02:00
# if path not in self.dynamic_paths:
# self.populate_dynamic(variable, path)
2024-11-15 08:13:45 +01:00
dynamic_variable = self.dynamic_paths[path]
2025-09-22 09:42:46 +02:00
if (not only_one or path in informations) and "type" in dynamic_variable:
dynamic_variable["gen_examples"].append(
self._get_example(variable, dynamic_variable, leader)
)
2024-11-15 08:13:45 +01:00
if variable.isleader():
return dynamic_variable
2025-09-22 09:42:46 +02:00
if not only_one:
return None
2025-10-16 08:19:03 +02:00
return self.parse_variable_normal(variable, leader, name, path, informations)
2024-11-15 08:13:45 +01:00
def _get_family_type(self, family) -> str:
if self.support_namespace and family.group_type() is groups.namespace:
return "namespace"
if family.isleadership():
return "leadership"
if family.isdynamic(only_self=True):
return "dynamic"
return "family"
def _populate_family(
self,
family,
path: str,
) -> dict:
if family.isdynamic():
informations = self.dynamic_paths[path]
else:
informations = {}
2025-10-15 09:44:22 +02:00
if not self._populate(family, informations, "family"):
return False
2024-11-15 08:13:45 +01:00
if family.isleadership():
informations.setdefault("help", []).append(
2025-10-29 10:46:57 +01:00
_("This family contains lists of variable blocks")
2024-11-15 08:13:45 +01:00
)
if family.isdynamic(only_self=True):
identifiers = self._to_string(family, "dynamic", do_not_raise=True)
if identifiers is None:
identifiers = family.identifiers(only_self=True)
2025-10-14 12:58:39 +02:00
if not isinstance(identifiers, list):
identifiers = [identifiers]
informations["identifier"] = identifiers
2024-11-15 08:13:45 +01:00
informations.setdefault("help", []).append(
2025-10-29 10:46:57 +01:00
_("This family builds families dynamically")
2024-11-15 08:13:45 +01:00
)
return informations
def _populate_variable(
self,
variable,
informations: dict,
):
informations["type"] = "variable"
default = self._get_default(
variable,
)
if default is not None:
2025-09-22 09:42:46 +02:00
informations["default"] = {"name": _("Default"), "values": default}
2024-11-15 08:13:45 +01:00
self._parse_type(
variable,
informations,
)
2025-10-15 09:44:22 +02:00
if not self._populate(variable, informations, "variable"):
return False
2024-11-15 08:13:45 +01:00
if variable.ismulti():
multi = not variable.isfollower() or variable.issubmulti()
else:
multi = False
if multi:
informations["properties"].append(
{
"type": "multiple",
"name": _("multiple"),
}
2024-11-20 21:12:56 +01:00
)
2024-11-15 08:13:45 +01:00
examples = variable.information.get("examples", None)
if examples is None:
examples = variable.information.get("test", None)
if examples is not None:
2025-09-22 09:42:46 +02:00
if len(examples) == 1:
2025-10-26 15:23:40 +01:00
name = _("Example")
values = examples[0]
2025-09-22 09:42:46 +02:00
else:
2025-10-26 15:23:40 +01:00
name = _("Examples")
values = list(examples)
informations["examples"] = {
"name": name,
"values": values
}
tags = variable.information.get("tags", None)
if tags:
if len(tags) == 1:
name = _("Tag")
values = tags[0]
else:
name = _("Tags")
values = list(tags)
informations["tags"] = {
"name": name,
"values": values,
}
return True
2024-11-15 08:13:45 +01:00
def _populate(
self,
child,
2024-11-15 08:13:45 +01:00
informations: dict,
2025-10-14 12:58:39 +02:00
type_: str,
2024-11-15 08:13:45 +01:00
):
need_disabled, properties = self._parse_properties(child)
if not need_disabled:
return False
2025-10-14 12:58:39 +02:00
name = child.name(uncalculated=True)
if child.information.get("forced_description", False):
2025-10-15 09:44:22 +02:00
if (
not child.isoptiondescription()
or not self.support_namespace
or child.group_type() is not groups.namespace
):
if (
child.isoptiondescription()
or not child.isfollower()
or not child.index()
):
2025-10-14 12:58:39 +02:00
warning = _('No attribute "description" for "{0}" in {1}').format(
child.path(uncalculated=True),
display_xmlfiles(child.information.get("ymlfiles")),
)
2025-10-15 09:44:22 +02:00
warn(
warning,
RougailWarning,
)
2025-10-14 12:58:39 +02:00
if child.isoptiondescription():
2025-10-15 09:44:22 +02:00
description = self._convert_description(
2025-10-29 10:46:57 +01:00
child.description(uncalculated=True), type_, its_a_path=True
2025-10-15 09:44:22 +02:00
)
2025-10-14 12:58:39 +02:00
else:
description = None
else:
2025-10-15 09:44:22 +02:00
description = self._convert_description(
2025-10-29 10:46:57 +01:00
child.description(uncalculated=True), type_, its_a_path=False
2025-10-15 09:44:22 +02:00
)
if not child.isdynamic():
2025-10-14 12:58:39 +02:00
informations["path"] = child.path(uncalculated=True)
informations["names"] = [child.name()]
2025-10-14 12:58:39 +02:00
if description is not None:
informations["description"] = description
help_ = child.information.get("help", None)
2024-11-15 08:13:45 +01:00
if help_:
2025-10-14 12:58:39 +02:00
informations["help"] = [to_phrase(help_)]
if "properties" in informations:
informations["properties"].extend(properties)
else:
informations["properties"] = properties
return True
2024-11-15 08:13:45 +01:00
2025-10-29 10:46:57 +01:00
def _convert_description(self, description, type_, its_a_path=False):
2025-10-14 12:58:39 +02:00
if not its_a_path:
description = to_phrase(description, type_)
2024-11-15 08:13:45 +01:00
return description
def _add_examples(self, variable, informations: dict, leader) -> None:
2025-09-22 09:42:46 +02:00
if not variable.index():
example = self._get_example(variable, informations, leader)
informations["gen_examples"] = [example]
2024-11-15 08:13:45 +01:00
informations["mandatory_without_value"] = "mandatory" in variable.property.get(
uncalculated=True
2025-05-11 19:13:12 +02:00
) and (
not variable.information.get("default_value_makes_sense", True)
or variable.value.get(uncalculated=True) in [None, []]
)
2024-11-15 08:13:45 +01:00
def _get_example(self, variable, informations: dict, leader):
2025-09-22 09:42:46 +02:00
example = informations.get("examples", {}).get("values")
2024-11-15 08:13:45 +01:00
if example is not None:
if isinstance(example, tuple):
example = list(example)
for prop in informations["properties"]:
if prop["type"] == "multiple":
2025-09-22 09:42:46 +02:00
if not isinstance(example, list):
example = [example]
break
else:
2025-09-22 09:42:46 +02:00
if isinstance(example, list):
index = variable.index()
if index is None or len(example) - 1 >= index:
index = 0
example = example[index]
2024-11-15 08:13:45 +01:00
else:
if variable.information.get("fake_default", False):
default = None
else:
try:
default = variable.value.get()
except ConfigError:
default = None
if default not in [None, []]:
example = default
else:
2025-09-22 09:42:46 +02:00
example = self.get_type_default_value(
variable, informations["properties"]
)
2024-11-15 08:13:45 +01:00
if leader is not None and variable.isfollower():
example = [example] + [undefined] * (len(leader["gen_examples"][-1]) - 1)
2024-11-15 08:13:45 +01:00
return example
2025-09-22 09:42:46 +02:00
def get_type_default_value(self, variable, properties):
example = self.convert_option.get(variable.information.get("type"), {}).get(
"example", None
)
if example is None:
example = "xxx"
for prop in properties:
if prop["type"] == "multiple":
multi = True
break
else:
multi = False
if multi:
example = [example]
return example
2024-11-15 08:13:45 +01:00
def _parse_type(
self,
child,
2024-11-15 08:13:45 +01:00
informations,
):
variable_type = child.information.get("type")
2024-11-15 08:13:45 +01:00
doc_type = DocTypes.get(variable_type, {"params": {}})
informations["properties"] = [
{
"type": "type",
"name": doc_type.get("msg", variable_type),
}
]
# extra parameters for types
option = child.get()
2025-09-22 09:42:46 +02:00
validators = []
2024-11-15 08:13:45 +01:00
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:
if isinstance(value, set):
value = list(value)
if isinstance(value, list):
2025-03-29 14:37:45 +01:00
value = display_list(value, add_quote=True)
2025-09-22 09:42:46 +02:00
validators.append(msg.format(value))
2024-11-15 08:13:45 +01:00
# get validation information from annotator
for name in child.information.list():
2024-11-15 08:13:45 +01:00
if not name.startswith("validators_calculation"):
continue
2025-09-22 09:42:46 +02:00
validators.extend(
2024-11-15 08:13:45 +01:00
self._to_string(
child,
2024-11-15 08:13:45 +01:00
"validators",
)
)
break
if child.information.get("type") == "regexp":
2025-09-22 09:42:46 +02:00
validators.append(
2025-10-15 09:44:22 +02:00
_('text based with regular expressions "{0}"').format(child.pattern())
2025-09-22 09:42:46 +02:00
)
if validators:
if len(validators) == 1:
key = _("Validator")
validators = validators[0]
else:
key = _("Validators")
informations["validators"] = {"name": key, "values": validators}
if child.information.get("type") == "choice":
choices = self._to_string(child, "choice", do_not_raise=True)
2024-11-15 08:13:45 +01:00
if choices is None:
choices = child.value.list()
2024-11-15 08:13:45 +01:00
for idx, val in enumerate(choices):
2025-10-14 12:58:39 +02:00
if isinstance(val, Calculation):
choices[idx] = self._to_string(child, "choice", f"_{idx}")
2025-09-22 09:42:46 +02:00
informations["choices"] = {"name": _("Choices"), "values": choices}
2024-11-15 08:13:45 +01:00
def _parse_properties(
self,
2025-10-03 06:44:25 +02:00
child,
2024-11-15 08:13:45 +01:00
):
informations = []
2025-10-03 06:44:25 +02:00
properties = child.property.get(uncalculated=True)
2024-11-15 08:13:45 +01:00
for mode in self.modes_level:
if mode not in properties:
continue
informations.append(
2024-11-15 08:13:45 +01:00
{
"type": "mode",
"name": mode,
}
)
break
for prop, translated_prop in self.property_to_string:
2024-11-15 08:13:45 +01:00
if prop in properties:
prop_obj = {
"type": "property",
"name": translated_prop,
2024-11-15 08:13:45 +01:00
}
2025-10-03 06:44:25 +02:00
elif child.information.get(f"{prop}_calculation", False):
annotation = self._to_string(child, prop)
if annotation is None or isinstance(annotation, bool):
if annotation is None and prop in HIDDEN_PROPERTIES:
return False, {}
if not annotation:
continue
prop_obj = {
"type": "property",
"name": translated_prop,
}
else:
prop_obj = {
"type": "property",
"name": translated_prop,
"annotation": annotation,
}
2024-11-15 08:13:45 +01:00
else:
# this property is not in the variable so, do not comment it
2024-11-15 08:13:45 +01:00
continue
informations.append(prop_obj)
return True, informations
2024-11-15 08:13:45 +01:00
def _get_default(
self,
variable,
):
default = self._to_string(variable, "default", do_not_raise=True)
if default is not None:
if default == []:
default = None
2024-11-15 08:13:45 +01:00
return default
if variable.information.get("default_value_makes_sense", True):
default_ = variable.value.get(uncalculated=True)
if not isinstance(default_, Calculation):
default = default_
if default == []:
default = None
2024-11-15 08:13:45 +01:00
return default
def _to_string(
self,
child,
2024-11-15 08:13:45 +01:00
prop,
do_not_raise=False,
):
calculation = child.information.get(f"{prop}_calculation", None)
2024-11-15 08:13:45 +01:00
if not calculation:
if do_not_raise:
return None
raise Exception(
f'cannot find "{prop}_calculation" information, '
"do you have declare doc has a plugins?"
)
if isinstance(calculation, list):
values = []
for cal in calculation:
2025-10-14 12:58:39 +02:00
value = self._calculation_to_string(child, cal, prop, inside_list=True)
2024-11-15 08:13:45 +01:00
if value is not None:
values.append(value)
return values
return self._calculation_to_string(child, calculation, prop)
2024-11-15 08:13:45 +01:00
2025-10-14 12:58:39 +02:00
def _calculation_to_string(self, child, calculation, prop, inside_list=False):
2025-05-04 14:29:51 +02:00
if "description" in calculation:
values = calculation["description"]
2025-10-29 10:46:57 +01:00
# if not values.endswith("."):
# values += "."
2025-05-04 14:29:51 +02:00
return values
2024-11-15 08:13:45 +01:00
if "type" not in calculation:
return calculation["value"]
if calculation["type"] == "jinja":
values = self._calculation_jinja_to_string(child, calculation, prop)
2024-11-15 08:13:45 +01:00
elif calculation["type"] == "variable":
values = self._calculation_variable_to_string(child, calculation, prop)
2024-11-15 08:13:45 +01:00
elif calculation["type"] == "identifier":
if prop in PROPERTY_ATTRIBUTE:
values = calculation["value"]
else:
values = _("the value of the identifier")
elif calculation["type"] == "information":
values = calculation["value"]
else:
values = _("the value of the {0}").format(calculation["type"])
2025-10-29 10:46:57 +01:00
# if not inside_list and isinstance(values, str) and not values.endswith("."):
# values += "."
2024-11-15 08:13:45 +01:00
return values
def _calculation_jinja_to_string(self, child, calculation, prop):
if calculation["value"] is not True:
values = calculation["value"]
else:
values = _("depends on a calculation")
if (
child.isoptiondescription()
or not child.isfollower()
or not child.index()
):
warning = _(
'"{0}" is a calculation for {1} but has no description in {2}'
).format(
prop,
child.path(),
display_xmlfiles(child.information.get("ymlfiles")),
)
2025-10-15 09:44:22 +02:00
warn(
warning,
RougailWarning,
)
return values
def _calculation_variable_to_string(self, child, calculation, prop):
if prop in PROPERTY_ATTRIBUTE:
2025-10-29 10:46:57 +01:00
values = self._calculation_variable_to_string_known_property(child, calculation, prop)
else:
if calculation["optional"]:
path = calculation["value"]
if "{{ identifier }}" in path:
if path not in self.dynamic_paths:
return None
else:
try:
self.conf.forcepermissive.option(path).get()
except AttributeError:
return None
if not calculation["optional"]:
true_msg = _('the value of the variable "{0}"')
else:
true_msg = _('the value of the variable "{0}" if it is defined')
if "{{ identifier }}" in calculation["ori_path"]:
values = []
2025-10-29 10:46:57 +01:00
all_is_undocumented = False
for cpath, description, identifiers in self.get_annotation_variable(calculation["value"], calculation["ori_path"]):
if cpath:
all_is_undocumented = False
path_obj = {
"path": cpath,
}
if identifiers:
path_obj["identifiers"] = identifiers
values.append({
"message": true_msg,
"path": path_obj,
"description": description,
})
else:
if all_is_undocumented is None:
all_is_undocumented = True
values.append(_("the value of an undocumented variable"))
if all_is_undocumented:
if len(values) > 1:
values = _("the values of undocumented variables")
else:
values = values[0]
else:
2025-10-29 10:46:57 +01:00
# FIXME A MUTUALISER AUSSI
variable_path = calculation["ori_path"]
variable = self.conf.forcepermissive.option(variable_path)
try:
isfollower = variable.isfollower()
except AttributeError as err:
pass
else:
2025-10-18 06:40:23 +02:00
if not isfollower and self.is_inaccessible_user_data(variable):
try:
uncalculated = variable.value.get(uncalculated=True)
except PropertiesOptionError:
true_msg = None
else:
if uncalculated and not isinstance(
uncalculated, Calculation
):
if isinstance(uncalculated, list):
2025-10-15 09:44:22 +02:00
true_msg = {
"submessage": _(
"(from an undocumented variable)"
),
"values": uncalculated,
}
else:
if not isinstance(uncalculated, str):
uncalculated = dump(uncalculated)
true_msg = _(
"{0} (from an undocumented variable)"
).format(uncalculated)
else:
true_msg = _("depends on an undocumented variable")
if true_msg:
2025-10-14 12:58:39 +02:00
if isinstance(true_msg, dict):
values = true_msg
else:
try:
description = self._convert_description(self.conf.option(calculation["ori_path"]).description(uncalculated=True), "description", its_a_path=False)
except AttributeError:
description = calculation["ori_path"]
2025-10-29 10:46:57 +01:00
values = {
"message": true_msg,
"path": {
"path": calculation["ori_path"],
},
"description": description,
}
else:
values = None
return values
2025-10-29 10:46:57 +01:00
def _calculation_variable_to_string_known_property(self, child, calculation, prop):
variable_path, value, condition = calculation["value"]
if isinstance(value, str):
str_value = value
else:
str_value = dump(value)
values = []
if "{{ identifier }}" in calculation["ori_path"] or "{{ identifier }}" in variable_path:
variables = self.get_annotation_variable(variable_path, calculation["ori_path"])
else:
option = self.conf.option(variable_path)
try:
is_inaccessible = self.is_inaccessible_user_data(option)
except AttributeError as err:
if err.code != "option-not-found":
raise err from err
is_inaccessible = True
if is_inaccessible:
variables = [[None, None, None]]
else:
description = self._convert_description(option.description(uncalculated=True), "description", its_a_path=False)
variables = [[variable_path, description, None]]
for cpath, description, identifiers in variables:
if not cpath:
variable = self.conf.forcepermissive.option(variable_path)
try:
variable_value = self._get_unmodified_default_value(variable)
except PropertiesOptionError as err:
if calculation["propertyerror"]:
raise err from err
variable_value = value
except VariableCalculationDependencyError:
values.append(_("depends on an undocumented variable"))
continue
except AttributeError as err:
# if err.code != "option-not-found" or not calculation.get("optional", False):
# raise err from err
2025-10-29 10:46:57 +01:00
return calculation.get("default", False)
if (
condition == "when"
and value == variable_value
or condition == "when_not"
and value != variable_value
):
if prop in HIDDEN_PROPERTIES:
return False
# always "prop"
return True
# never "prop"
return False
else:
if condition == "when_not":
if calculation["optional"]:
if not calculation["propertyerror"]:
msg = _(
'when the variable "{{0}}" is defined, accessible and hasn\'t the value "{0}"'
)
else:
msg = _(
'when the variable "{{0}}" is defined and hasn\'t the value "{0}"'
)
elif not calculation["propertyerror"]:
msg = _('when the variable "{{0}}" is accessible and hasn\'t the value "{0}"')
else:
msg = _('when the variable "{{0}}" hasn\'t the value "{0}"')
else:
if calculation["optional"]:
if not calculation["propertyerror"]:
msg = _(
'when the variable "{{0}}" is defined, is accessible and has the value "{0}"'
)
else:
msg = _(
'when the variable "{{0}}" is defined and has the value "{0}"'
)
elif not calculation["propertyerror"]:
msg = _('when the variable "{{0}}" is accessible and has the value "{0}"')
else:
msg = _('when the variable "{{0}}" has the value "{0}"')
path_obj = {
"path": variable_path,
}
if identifiers:
path_obj["identifiers"] = identifiers
values.append({
"message": msg.format(str_value),
"path": path_obj,
"description": description,
})
if len(values) == 1:
return values[0]
return values
def get_annotation_variable(self, current_path, ori_path):
if current_path == ori_path:
regexp = None
else:
regexp = compile(
"^"
+ ori_path.replace("{{ identifier }}", "(.*)")
+ "$"
)
information = self.dynamic_paths[current_path]
path = information["path"]
for identifiers in information["identifiers"]:
cpath = calc_path(path, identifiers=identifiers)
if regexp and not regexp.search(cpath):
continue
if self.is_inaccessible_user_data(self.conf.option(cpath)):
yield None, None, None
else:
description = self._convert_description(self.conf.option(path).description(uncalculated=True), "description", its_a_path=False)
if "{{ identifier }}" in path:
yield path, description, identifiers.copy()
else:
yield path, description, None
def _get_unmodified_default_value(self, child):
calculation = child.information.get(f"default_calculation", None)
if not calculation:
return child.value.get()
if calculation["type"] == "variable":
variable = self.conf.forcepermissive.option(calculation["value"])
2025-10-18 06:40:23 +02:00
if variable and self.is_inaccessible_user_data(variable):
return self._get_unmodified_default_value(variable)
raise VariableCalculationDependencyError()