rougail/src/rougail/convert/tiramisureflector.py

565 lines
19 KiB
Python

"""loader
flattened XML specific
Created by:
EOLE (http://eole.orion.education.fr)
Copyright (C) 2005-2018
Forked by:
Cadoles (http://www.cadoles.com)
Copyright (C) 2019-2021
Silique (https://www.silique.fr)
Copyright (C) 2022-2025
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 typing import Optional, Union
from json import dumps
from os.path import isfile, basename
from ..i18n import _
from ..error import DictConsistencyError, VariableCalculationDependencyError
from ..tiramisu import normalize_family, CONVERT_OPTION
from .object_model import Calculation
class BaseElt: # pylint: disable=R0903
"""Base element"""
path = "."
type = "family"
def sorted_func_name(func_name):
s_func_name = func_name.split("/")
s_func_name.reverse()
return "/".join(s_func_name)
class TiramisuReflector:
"""Convert object to tiramisu representation"""
def __init__(
self,
objectspace,
funcs_paths,
):
self.informations_idx = -1
self.reflector_objects = {}
self.text = {
"header": [],
"option": [],
}
self.objectspace = objectspace
if self.objectspace.export_with_import:
if self.objectspace.internal_functions:
for func in self.objectspace.internal_functions:
self.text["header"].append(f"func[func] = func")
self.text["header"].extend(
[
"from tiramisu import *",
"from tiramisu.setting import ALLOWED_LEADER_PROPERTIES",
"from re import compile as re_compile",
"from rougail.tiramisu import func, dict_env, load_functions, ConvertDynOptionDescription",
]
)
if funcs_paths:
for funcs_path in sorted(funcs_paths, key=sorted_func_name):
if not isfile(funcs_path):
continue
self.text["header"].append(f"load_functions('{funcs_path}')")
if self.objectspace.export_with_import:
if self.objectspace.has_namespace:
self.text["header"].extend(
[
"try:",
" groups.namespace",
"except:",
" groups.addgroup('namespace')",
]
)
for mode in self.objectspace.modes_level:
self.text["header"].append(f'ALLOWED_LEADER_PROPERTIES.add("{mode}")')
self.make_tiramisu_objects()
for key, value in self.objectspace.jinja.items():
self.add_jinja_to_function(key, value)
def add_jinja_to_function(
self,
variable_name: str,
jinja: str,
) -> None:
jinja_text = dumps(jinja, ensure_ascii=False)
self.text["header"].append(f"dict_env['{variable_name}'] = {jinja_text}")
def make_tiramisu_objects(self) -> None:
"""make tiramisu objects"""
baseelt = BaseElt()
self.objectspace.reflector_names[baseelt.path] = (
f"option_0{self.objectspace.suffix}"
)
basefamily = Family(
baseelt,
self,
)
for elt in self.objectspace.paths.get():
if elt.path in self.objectspace.families:
Family(
elt,
self,
)
else:
Variable(
elt,
self,
)
baseelt.name = normalize_family(self.objectspace.base_option_name)
baseelt.description = self.objectspace.base_option_name
self.reflector_objects[baseelt.path].get(
[], baseelt.description
) # pylint: disable=E1101
def set_name(
self,
elt,
):
"""Set name"""
if elt.path not in self.objectspace.reflector_names:
self.objectspace.set_name(elt, "optiondescription_")
return self.objectspace.reflector_names[elt.path]
def get_information_name(self):
self.informations_idx += 1
return f"information_{self.informations_idx}"
def get_text(self):
"""Get text"""
return "\n".join(self.text["header"] + self.text["option"])
class Common:
"""Common function for variable and family"""
def __init__(
self,
elt,
tiramisu,
):
self.objectspace = tiramisu.objectspace
self.elt = elt
self.option_name = None
self.tiramisu = tiramisu
tiramisu.reflector_objects[elt.path] = self
self.object_type = None
self.informations = []
def get(self, calls, parent_name):
"""Get tiramisu's object"""
if self.elt.path in calls:
msg = f'"{self.elt.path}" will make an infinite loop'
raise DictConsistencyError(msg, 80, self.elt.xmlfiles)
self_calls = calls.copy()
self_calls.append(self.elt.path)
self.calls = self_calls
if self.option_name is None:
self.option_name = self.objectspace.reflector_names[self.elt.path]
self.populate_attrib()
if self.informations:
for information in self.informations:
self.tiramisu.text["option"].append(
f"{information}.set_option({self.option_name})"
)
return self.option_name
def populate_attrib(self):
"""Populate attributes"""
keys = {"name": self.convert_str(self.elt.name)}
if hasattr(self.elt, "description") and self.elt.description:
keys["doc"] = self.convert_str(self.elt.description)
self._populate_attrib(keys)
if self.elt.path in self.objectspace.properties:
keys["properties"] = self.properties_to_string(
self.objectspace.properties[self.elt.path]
)
self.populate_informations(keys)
attrib = ", ".join([f"{key}={value}" for key, value in keys.items()])
self.tiramisu.text["option"].append(
f"{self.option_name} = {self.object_type}({attrib})"
)
def _populate_attrib(
self,
keys: dict,
) -> None: # pragma: no cover
raise NotImplementedError()
@staticmethod
def convert_str(value):
"""convert string"""
if value is None:
return "None"
return dumps(value, ensure_ascii=False)
def properties_to_string(
self,
values: list,
) -> None:
"""Change properties to string"""
properties = []
calc_properties = []
for property_, value in values.items():
if not isinstance(value, list):
value = [value]
for val in value:
ret = self.calculation_property(val)
if isinstance(ret, bool):
if ret:
properties.append(self.convert_str(property_))
elif ret is not None:
calc_properties.append(ret)
if properties or calc_properties:
return "frozenset({" + ", ".join(sorted(properties) + calc_properties) + "})"
raise Exception('ca existe alors ...')
def calculation_property(
self,
value: Union[Calculation, bool],
) -> Optional[bool]:
if isinstance(value, Calculation):
try:
return self.calculation_value(value)
except VariableCalculationDependencyError:
return None
return value
def calc_properties(
self,
prop,
calculation,
) -> str:
"""Populate properties"""
option_name = self.tiramisu.reflector_objects[child.source.path].get(
self.calls, self.elt.path
)
kwargs = (
f"'condition': ParamOption({option_name}, notraisepropertyerror=True), "
f"'expected': {self.populate_param(child.expected)}"
)
if child.inverse:
kwargs += ", 'reverse_condition': ParamValue(True)"
return (
f"Calculation(func['calc_value'], Params(ParamValue('{child.name}'), "
f"kwargs={{{kwargs}}}), func['calc_value_property_help'])"
)
def populate_informations(self, keys):
"""Populate Tiramisu's informations"""
informations = self.objectspace.informations.get(self.elt.path)
if not informations:
return
keys["informations"] = informations
def populate_param(
self,
param,
):
"""Populate variable parameters"""
if not isinstance(param, dict):
param = {
"type": "any",
"value": param,
}
if param["type"] == "value":
return f"ParamValue({param['value']})"
if param["type"] == "information":
return self.build_information_param(param)
if param["type"] == "identifier":
if "identifier" in param and param["identifier"] != None:
return f"ParamIdentifier(identifier_index={param['identifier']})"
return "ParamIdentifier()"
if param["type"] == "index":
return "ParamIndex()"
if param["type"] == "variable":
return self.build_variable_param(param)
if param["type"] == "any":
if isinstance(param["value"], str):
value = self.convert_str(param["value"])
else:
value = str(param["value"])
return "ParamValue(" + value + ")"
raise Exception("pfff")
def build_information_param(self, param: dict) -> str:
# default? really?
if self.elt.multi:
default = []
else:
default = None
if "variable" in param:
information_variable_path = param["variable"].path
if information_variable_path == self.elt.path:
return f'ParamSelfInformation("{param["information"]}", {default})'
information_variable = self.tiramisu.reflector_objects[
information_variable_path
]
if information_variable_path not in self.calls:
option_name = information_variable.get(self.calls, self.elt.path)
return f'ParamInformation("{param["information"]}", {default}, option={option_name})'
else:
# if we want to get information from the a parent family
information = f'ParamInformation("{param["information"]}", {default})'
information_name = self.tiramisu.get_information_name()
self.tiramisu.text["option"].append(
f"{information_name} = {information}"
)
information_variable.informations.append(information_name)
return information_name
return f'ParamInformation("{param["information"]}", {default})'
def build_variable_param(
self,
param: dict,
) -> str:
"""build variable parameters"""
variable = param["variable"]
whole = param.get("whole", False)
if variable.path == self.elt.path:
return f"ParamSelfOption(whole={whole})"
if whole:
msg = f'variable param "{variable.path}" has whole attribute but it\'s not allowed for external variable'
raise DictConsistencyError(msg, 34, self.elt.xmlfiles)
option_name = self.tiramisu.reflector_objects[variable.path].get(
self.calls, self.elt.path
)
params = [f"{option_name}"]
identifier = param.get("identifier")
if identifier is not None:
if not self.objectspace.paths.is_dynamic(variable.path):
msg = _("internal error, {0} is not a dynamic variable").format(
variable.path
)
raise DictConsistencyError(msg, 49, self.elt.xmlfiles)
param_type = "ParamDynOption"
identifiers = []
for ident in identifier:
if isinstance(ident, str):
ident = self.convert_str(ident)
identifiers.append(str(ident))
params.append("[" + ", ".join(identifiers) + "]")
if param.get("optional", False):
params.append("optional=True")
else:
param_type = "ParamOption"
if not param.get("propertyerror", True):
params.append("notraisepropertyerror=True")
return f'{param_type}({", ".join(params)})'
def calculation_value(
self,
function,
) -> str:
"""Generate calculated value"""
child = function.to_function(self.objectspace)
if isinstance(child, str):
return self.convert_str(child)
elif not isinstance(child, dict):
return child
new_args = []
kwargs = []
if "params" in child:
for key, value in child["params"].items():
if key is None:
for val in value:
new_args.append(self.populate_param(val))
else:
kwargs.append(f"'{key}': " + self.populate_param(value))
ret = (
f"Calculation(func['{child['function']}'], Params(("
+ ", ".join(new_args)
+ ")"
)
if kwargs:
ret += ", kwargs={" + ", ".join(kwargs) + "}"
ret += ")"
if hasattr(child, "warnings_only"):
ret += f", warnings_only={child.warnings_only}"
if "help" in child:
ret += f", help_function=func['{child['help']}']"
ret = ret + ")"
return ret
def populate_calculation(
self,
datas: Union[Calculation, str, list],
return_a_tuple: bool = False,
) -> str:
if isinstance(datas, Calculation):
datas = self.calculation_value(datas)
elif isinstance(datas, str):
datas = self.convert_str(datas)
if not isinstance(datas, list):
return datas
params = []
for idx, data in enumerate(datas):
if isinstance(data, Calculation):
try:
params.append(self.calculation_value(data))
except VariableCalculationDependencyError:
continue
elif isinstance(data, str):
params.append(self.convert_str(data))
elif isinstance(data, dict):
params.append(data)
else:
params.append(str(data))
if return_a_tuple:
ret = "("
else:
ret = "["
ret += ", ".join(params)
if return_a_tuple:
if len(params) <= 1:
ret += ","
ret += ")"
else:
ret += "]"
return ret
class Variable(Common):
"""Manage variable"""
def __init__(
self,
elt,
tiramisu,
):
super().__init__(elt, tiramisu)
if elt.type in self.tiramisu.objectspace.custom_types:
self.object_type = self.tiramisu.objectspace.custom_types[elt.type].__name__
else:
self.object_type = CONVERT_OPTION[elt.type]["opttype"]
def _populate_attrib(
self,
keys: dict,
):
if self.elt.type == "symlink":
keys["opt"] = self.tiramisu.reflector_objects[self.elt.opt.path].get(
self.calls, self.elt.path
)
return
if self.elt.type == "choice":
try:
keys["values"] = self.populate_calculation(
self.elt.choices, return_a_tuple=True
)
if keys["values"] == '(,)':
keys["values"] = tuple()
except VariableCalculationDependencyError:
keys["values"] = tuple()
if self.elt.type == "regexp":
self.object_type = "Regexp_" + self.option_name
self.tiramisu.text["header"].append(
f"""class {self.object_type}(RegexpOption):
__slots__ = tuple()
_type = 'value'
{self.object_type}._regexp = re_compile(r"{self.elt.regexp}")
"""
)
if self.elt.path in self.objectspace.multis:
keys["multi"] = self.objectspace.multis[self.elt.path]
if hasattr(self.elt, "default") and self.elt.default is not None:
try:
keys["default"] = self.populate_calculation(self.elt.default)
except VariableCalculationDependencyError:
pass
if self.elt.path in self.objectspace.default_multi:
try:
keys["default_multi"] = self.populate_calculation(
self.objectspace.default_multi[self.elt.path]
)
except VariableCalculationDependencyError:
pass
if self.elt.validators:
keys["validators"] = self.populate_calculation(self.elt.validators)
for key, value in (
CONVERT_OPTION.get(self.elt.type, {}).get("initkwargs", {}).items()
):
if isinstance(value, str):
value = self.convert_str(value)
keys[key] = value
if self.elt.params:
for param in self.elt.params:
value = param.value
if isinstance(value, str):
value = self.convert_str(value)
keys[param.key] = value
class Family(Common):
"""Manage family"""
def __init__(
self,
elt,
tiramisu,
):
super().__init__(elt, tiramisu)
if self.elt.type == "dynamic":
self.tiramisu.objectspace.has_dyn_option = True
self.object_type = "ConvertDynOptionDescription"
elif self.elt.type == "leadership":
self.object_type = "Leadership"
else:
self.object_type = "OptionDescription"
if hasattr(self.elt, "name") and self.elt.name == self.elt.namespace:
self.group_type = "groups.namespace"
else:
self.group_type = None
self.children = []
def add(self, child):
"""Add a child"""
self.children.append(child)
def _populate_attrib(
self,
keys: list,
) -> None:
if self.group_type:
keys["group_type"] = self.group_type
if self.elt.type == "dynamic":
try:
keys["identifiers"] = self.populate_calculation(self.elt.dynamic)
except VariableCalculationDependencyError:
keys["identifiers"] = []
children = []
for path in self.objectspace.parents[self.elt.path]:
children.append(self.objectspace.paths[path])
keys["children"] = (
"["
+ ", ".join(
[
self.tiramisu.reflector_objects[child.path].get(
self.calls, self.elt.path
)
for child in children
]
)
+ "]"
)