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

395 lines
14 KiB
Python

"""
Silique (https://www.silique.fr)
Copyright (C) 2024-2026
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
"""
from typing import Union
from re import compile
from rougail.utils import undefined, PROPERTY_ATTRIBUTE
from rougail.annotator.variable import Walk
from rougail.output_doc.i18n import _
from rougail.convert.object_model import (
Calculation,
JinjaCalculation,
VariableCalculation,
VariablePropertyCalculation,
IdentifierCalculation,
IdentifierPropertyCalculation,
InformationCalculation,
IndexCalculation,
NamespaceCalculation,
CONVERT_OPTION,
)
try:
from rougail.user_data_risotto.utils import RisottoCalculation
except ImportError as err:
RisottoCalculation = None
class Annotator(Walk):
"""Annotate for documentation"""
level = 95
def __init__(
self,
objectspace,
*args, # pylint: disable=unused-argument
**kwargs,
) -> None:
if not objectspace.paths:
return
self.objectspace = objectspace
if "force_default_value" in kwargs:
self.default_values = kwargs["force_default_value"]
else:
self.default_values = self.objectspace.rougailconfig["doc.default_values"]
self.regexp_description_get_paths = None
self.populate_family()
self.populate_variable()
def populate_family(self) -> None:
"""Set doc, path, ... to family"""
for family in self.get_families():
self.add_informations_from_properties(family)
if family.type == "dynamic":
self.force_default_value_in_identifier_variable(family)
self.calculation_to_information(
family.path,
"dynamic",
family.dynamic,
family.version,
)
path = family.path
if path in self.objectspace.forced_descriptions:
self.objectspace.informations.add(
path,
"forced_description",
True,
)
def force_default_value_in_identifier_variable(self, family) -> None:
if not self.default_values:
return
if not isinstance(family.dynamic, list):
self._force_default_value_in_suffix_variable(family, family.dynamic)
else:
for value in family.dynamic:
self._force_default_value_in_suffix_variable(
family, value, return_a_list=False
)
def _force_default_value_in_suffix_variable(
self,
family,
value,
*,
return_a_list=True,
) -> None:
"""For dynamic we must have values to document it"""
if not isinstance(value, Calculation):
return
value_added = False
if isinstance(value, (VariableCalculation, VariablePropertyCalculation)):
variable = self.objectspace.paths.get_with_dynamic(
value.variable,
family.path,
value.version,
value.namespace,
value.xmlfiles,
)[0]
if variable and not variable.default:
values = self.add_examples_values(variable)
value_added = True
if not value_added:
default_values = ["example"]
if not return_a_list:
default_values = default_values[0]
value.default_values = default_values
def add_examples_values(self, variable) -> list:
"""Check examples or test information to define examples values in a variable"""
values = self.objectspace.informations.get(variable.path).get("examples", None)
if not values:
values = self.objectspace.informations.get(variable.path).get("test", None)
if isinstance(values, tuple):
values = list(values)
if values:
variable.default = list(values)
else:
variable.default = [CONVERT_OPTION[variable.type]["example"]]
self.objectspace.informations.add(
variable.path, "default_value_makes_sense", False
)
return values
def populate_variable(self) -> None:
"""convert variables"""
for variable in self.get_variables():
if variable.type == "symlink":
continue
path = variable.path
if variable.type == "choice":
self.calculation_to_information(
path,
"choice",
variable.choices,
variable.version,
)
self.manage_test(variable)
default = variable.default
if default is None and path in self.objectspace.default_multi:
default = self.objectspace.default_multi[path]
self.calculation_to_information(
path,
"default",
default,
variable.version,
)
self.calculation_to_information(
path,
"validators",
variable.validators,
variable.version,
)
self.add_informations_from_properties(variable)
if path in self.objectspace.forced_descriptions:
self.objectspace.informations.add(
path,
"forced_description",
True,
)
def manage_test(self, variable):
# for version 1.0 and 1.1
values = self.objectspace.informations.get(variable.path).get("examples", None)
if values:
return
values = self.objectspace.informations.get(variable.path).get("test", None)
if values:
self.objectspace.informations.add(variable.path, "examples", values)
def add_informations_from_properties(
self,
variable: dict,
) -> None:
"""convert properties"""
for prop in PROPERTY_ATTRIBUTE:
prop_value = getattr(variable, prop, None)
if not prop_value:
continue
self.calculation_to_information(
variable.path,
prop,
prop_value,
variable.version,
)
if isinstance(prop_value, Calculation):
prop_value.default_values = False
def calculation_to_information(
self,
path: str,
prop: str,
values,
version: str,
):
"""tranform calculation to an information"""
one_is_calculation = False
if not isinstance(values, list):
if not isinstance(values, Calculation):
return
one_is_calculation = True
datas = self.calculation_to_string(path, prop, values, version)
else:
datas = []
for idx, val in enumerate(values):
data = self.calculation_to_string(path, prop, val, version)
if data is None:
continue
if "type" in data or "description" in data:
one_is_calculation = True
datas.append(data)
if one_is_calculation and datas:
self.objectspace.informations.add(
path,
f"{prop}_calculation",
datas,
)
def calculation_to_string(
self,
path: str,
prop: str,
values,
version: str,
):
if not isinstance(values, Calculation):
return {"value": values}
if values.description:
if not self.regexp_description_get_paths:
self.regexp_description_get_paths = compile('"(.*?)"')
index = 0
val_description = values.description
variables = []
for r_path in self.regexp_description_get_paths.findall(val_description):
variable, identifiers = self.objectspace.paths.get_with_dynamic(
r_path,
path,
values.version,
values.namespace,
values.xmlfiles,
)
if variable:
val_description = val_description.replace(f'"{r_path}"', f'{{{index}}}')
v = {"path": variable.path, "description": variable.description}
if identifiers:
v["identifiers"] = [identifiers]
v["identifier_type"] = "many"
variables.append(v)
index += 1
description = {"description": True,
"value": val_description,
}
if variables:
description["variables"] = variables
else:
description = {}
if isinstance(values, JinjaCalculation):
if not description:
description["value"] = True
description["type"] = "jinja"
return description
if isinstance(values, (VariableCalculation, VariablePropertyCalculation)):
variable_path = self.get_path_from_variable(values, version, path)
if not description:
value = self.calculation_to_information_variable(
variable_path, values, prop, version, path
)
if value is None:
return
description["value"] = value
description["type"] = "variable"
description["ori_path"] = variable_path
description["optional"] = values.optional
description["propertyerror"] = values.propertyerror
return description
if isinstance(values, InformationCalculation):
# FIXME
return self.calculation_to_information_information(values, version, path)
if isinstance(values, IdentifierPropertyCalculation):
self.when_to_condition(values, description)
description["type"] = "identifier"
return description
if isinstance(values, IdentifierCalculation):
description["type"] = "identifier"
return description
if isinstance(values, IndexCalculation):
if not description:
description["value"] = True
description["type"] = "index"
return description
if isinstance(values, NamespaceCalculation):
if not description:
description["value"] = True
description["type"] = "namespace"
return description
if RisottoCalculation and isinstance(values, RisottoCalculation):
# FIXME
if not description:
description["value"] = True
description["type"] = "risotto"
return description
raise Exception(f'unknown calculation {type(values)} "{values}"')
def calculation_to_information_variable(
self, variable_path: str, values, prop: str, version: str, path: str
) -> str:
variable, identifiers = self.objectspace.paths.get_with_dynamic(
variable_path,
path,
values.version,
values.namespace,
values.xmlfiles,
)
if not variable or (
isinstance(values, VariableCalculation) and values.optional and not variable
):
return None
values_calculation = {"path": variable.path}
if identifiers:
values_calculation["identifiers"] = identifiers
values_calculation["identifier_type"] = "many"
if prop in PROPERTY_ATTRIBUTE:
# get comparative value
self.when_to_condition(values, values_calculation)
else:
values_calculation["type"] = "variable"
return values_calculation
def when_to_condition(self, values, values_calculation):
if values.when_not is not undefined:
values_calculation["type"] = "condition"
values_calculation["value"] = values.when_not
values_calculation["condition"] = "when_not"
elif values.when is not undefined:
values_calculation["type"] = "condition"
values_calculation["value"] = values.when
values_calculation["condition"] = "when"
elif values.propertyerror == "transitive":
values_calculation["type"] = "transitive"
else:
values_calculation["type"] = "condition"
values_calculation["value"] = True
values_calculation["condition"] = "when"
def calculation_to_information_information(
self, values, version: str, path: str
) -> Union[str, bool]:
values_calculation = {
"type": "information",
"information": values.information,
}
if values.variable:
variable = self.objectspace.paths.get_with_dynamic(
values.variable,
path,
values.version,
values.namespace,
values.xmlfiles,
)[0]
if not variable:
return None
values_calculation["path"] = variable.path
return values_calculation
def get_path_from_variable(self, values, version: str, path: str) -> str:
variable_path = values.variable
paths = self.objectspace.paths
if version != "1.0" and paths.regexp_relative.search(variable_path):
variable_path = paths.get_full_path(
variable_path,
path,
)
elif version == "1.0" and "{{ suffix }}" in variable_path:
variable_path = variable_path.replace("{{ suffix }}", "{{ identifier }}")
return variable_path