386 lines
16 KiB
Python
386 lines
16 KiB
Python
"""Annotate variable
|
|
|
|
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-2026
|
|
|
|
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 tiramisu.error import display_list
|
|
from rougail.i18n import _
|
|
from rougail.utils import calc_multi_for_type_variable
|
|
from rougail.error import DictConsistencyError
|
|
from rougail.convert.object_model import (
|
|
Calculation,
|
|
VariableCalculation,
|
|
VariableParam,
|
|
IndexCalculation,
|
|
JinjaCalculation,
|
|
)
|
|
from rougail.tiramisu import display_xmlfiles, RENAME_TYPE
|
|
|
|
from warnings import warn
|
|
|
|
|
|
class Walk:
|
|
"""Walk to objectspace to find variable or family"""
|
|
|
|
objectspace = None
|
|
|
|
def get_variables(self):
|
|
"""Iter all variables from the objectspace"""
|
|
for path in self.objectspace.variables:
|
|
yield self.objectspace.paths[path]
|
|
|
|
# yield from get_variables(self.objectspace)
|
|
|
|
def get_families(self):
|
|
"""Iter all families from the objectspace"""
|
|
for path in self.objectspace.families:
|
|
yield self.objectspace.paths[path]
|
|
|
|
|
|
class Annotator(Walk): # pylint: disable=R0903
|
|
"""Annotate variable"""
|
|
|
|
level = 30
|
|
|
|
def __init__(
|
|
self,
|
|
objectspace,
|
|
*args,
|
|
):
|
|
if not objectspace.paths:
|
|
return
|
|
self.objectspace = objectspace
|
|
# default type inference from a default value with :term:`basic types`
|
|
self.basic_types = {
|
|
str: "string",
|
|
int: "integer",
|
|
bool: "boolean",
|
|
float: "float",
|
|
}
|
|
self.verify_secret_managers()
|
|
self.verify_choices()
|
|
self.convert_variable()
|
|
self.convert_test()
|
|
self.convert_examples()
|
|
self.convert_help()
|
|
|
|
def verify_secret_managers(self):
|
|
for variable in self.get_variables():
|
|
if not variable.secret_manager:
|
|
continue
|
|
path = variable.path
|
|
if variable.type not in ["unix_user", "secret"]:
|
|
msg = _(
|
|
'only "unix_user" or "secret" variable type can have "secret_manager" attribute, but "{0}" has type "{1}"'
|
|
)
|
|
raise DictConsistencyError(
|
|
msg.format(path, variable.type), 56, variable.xmlfiles
|
|
)
|
|
if variable.multi and path not in self.objectspace.leaders:
|
|
msg = _(
|
|
'the variable "{0}" has attribute "secret_manager" but is a multi variable'
|
|
)
|
|
raise DictConsistencyError(msg.format(path), 57, variable.xmlfiles)
|
|
if variable.default is not None:
|
|
msg = _(
|
|
'the variable "{0}" has attribute "secret_manager" so must not have default value'
|
|
)
|
|
raise DictConsistencyError(msg.format(path), 59, variable.xmlfiles)
|
|
|
|
def convert_variable(self):
|
|
"""convert variable"""
|
|
for variable in self.get_variables():
|
|
if variable.type == "symlink":
|
|
continue
|
|
if variable.version != "1.0":
|
|
self._convert_variable_inference(variable)
|
|
self._convert_variable_multi(variable)
|
|
for variable in self.get_variables():
|
|
if variable.type == "symlink":
|
|
continue
|
|
if variable.type in RENAME_TYPE:
|
|
warning = f'the variable "{ variable.path }" has a depreciated type "{variable.type}", please use "{RENAME_TYPE[variable.type]}" instead in {display_xmlfiles(variable.xmlfiles)}'
|
|
warn(
|
|
warning,
|
|
DeprecationWarning,
|
|
)
|
|
variable.type = RENAME_TYPE[variable.type]
|
|
if variable.type == "cidr":
|
|
warning = f'the variable "{ variable.path }" has a depreciated type "{variable.type}", please use type "ip" with attribute cidr=True instead in {display_xmlfiles(variable.xmlfiles)}'
|
|
warn(
|
|
warning,
|
|
DeprecationWarning,
|
|
)
|
|
if variable.type == "network_cidr":
|
|
warning = f'the variable "{ variable.path }" has a depreciated type "{variable.type}", please use type "network" with attribute cidr=True instead in {display_xmlfiles(variable.xmlfiles)}'
|
|
warn(
|
|
warning,
|
|
DeprecationWarning,
|
|
)
|
|
self.objectspace.informations.add(
|
|
variable.path, "ymlfiles", variable.xmlfiles
|
|
)
|
|
if variable.version != "1.0" and variable.type is None:
|
|
if isinstance(variable.default, IndexCalculation):
|
|
variable.type = "integer"
|
|
elif isinstance(variable.default, VariableCalculation):
|
|
calculated_variable_path, calculated_variable, identifier = (
|
|
variable.default.get_variable(self.objectspace)
|
|
)
|
|
if calculated_variable is not None:
|
|
self._default_variable_copy_informations(
|
|
variable, calculated_variable
|
|
)
|
|
self._convert_variable(variable)
|
|
if variable.multi and variable.params:
|
|
indexes_to_remove = []
|
|
for idx, param in enumerate(variable.params):
|
|
if param.key == "multi_length":
|
|
jinja = f'{{% if var | length != {param.value} %}}{_("{0} values needed, but there are {{{{ var | length }}}}").format(param.value)}{{% endif %}}'
|
|
description = _("needs exactly {0} values").format(param.value)
|
|
elif param.key == "multi_max_length":
|
|
jinja = f'{{% if var | length > {param.value} %}}{_("a maximum of {0} values are needed, but there are {{{{ var | length }}}}").format(param.value)}{{% endif %}}'
|
|
description = _("needs a maximum of {0} values").format(
|
|
param.value
|
|
)
|
|
elif param.key == "multi_min_length":
|
|
jinja = f'{{% if var | length < {param.value} %}}{_("a minimum of {0} values are needed, but there are {{{{ var | length }}}}").format(param.value)}{{% endif %}}'
|
|
description = _("needs a minimum of {0} values").format(
|
|
param.value
|
|
)
|
|
else:
|
|
continue
|
|
indexes_to_remove.append(idx)
|
|
if variable.validators is None:
|
|
variable.validators = []
|
|
|
|
variable.validators.append(
|
|
JinjaCalculation(
|
|
path=variable.path,
|
|
inside_list=True,
|
|
version=variable.version,
|
|
xmlfiles=variable.xmlfiles,
|
|
attribute_name="validators",
|
|
namespace=variable.namespace,
|
|
jinja=jinja,
|
|
description=description,
|
|
params=[
|
|
VariableParam(
|
|
variable.path,
|
|
"validators",
|
|
False,
|
|
variable.xmlfiles,
|
|
key="var",
|
|
namespace=variable.namespace,
|
|
type="variable",
|
|
variable=variable.path,
|
|
whole=True,
|
|
)
|
|
],
|
|
)
|
|
)
|
|
for idx in reversed(indexes_to_remove):
|
|
variable.params.pop(idx)
|
|
|
|
def _convert_variable_inference(
|
|
self,
|
|
variable,
|
|
) -> None:
|
|
# variable has no type
|
|
if variable.type is not None:
|
|
return
|
|
# choice type inference from the `choices` attribute
|
|
if variable.choices is not None:
|
|
variable.type = "choice"
|
|
elif variable.regexp is not None:
|
|
variable.type = "regexp"
|
|
elif variable.default not in [None, []]:
|
|
if isinstance(variable.default, list):
|
|
tested_value = variable.default[0]
|
|
else:
|
|
tested_value = variable.default
|
|
variable.type = self.basic_types.get(type(tested_value), None)
|
|
|
|
def _convert_variable_multi(
|
|
self,
|
|
variable,
|
|
) -> None:
|
|
# variable has no multi attribute
|
|
if variable.multi is not None:
|
|
return
|
|
if variable.path in self.objectspace.leaders:
|
|
variable.multi = self.objectspace.multis[variable.path] = True
|
|
elif variable.version != "1.0" and isinstance(
|
|
variable.default, VariableCalculation
|
|
):
|
|
calculated_variable_path, calculated_variable, identifier = (
|
|
variable.default.get_variable(self.objectspace)
|
|
)
|
|
if calculated_variable is not None:
|
|
if calculated_variable.multi is None:
|
|
if (
|
|
isinstance(calculated_variable.default, VariableCalculation)
|
|
and variable.path == calculated_variable.default.path
|
|
):
|
|
msg = _(
|
|
'the "{0}" default value is a calculation with itself'
|
|
).format(variable.path)
|
|
raise DictConsistencyError(msg, 75, variable.xmlfiles)
|
|
self._convert_variable_multi(calculated_variable)
|
|
variable.multi = calc_multi_for_type_variable(
|
|
variable,
|
|
calculated_variable_path,
|
|
calculated_variable,
|
|
self.objectspace,
|
|
)[1]
|
|
if (
|
|
calculated_variable.path in self.objectspace.followers
|
|
and variable.mandatory is calculated_variable.mandatory is False
|
|
and calculated_variable.path.rsplit(".", 1)[0]
|
|
!= variable.path.rsplit(".", 1)[0]
|
|
):
|
|
variable.empty = False
|
|
else:
|
|
variable.multi = isinstance(variable.default, list)
|
|
|
|
def _default_variable_copy_informations(
|
|
self,
|
|
variable,
|
|
calculated_variable,
|
|
) -> None:
|
|
# copy type and params
|
|
variable.type = calculated_variable.type
|
|
if variable.params is None and calculated_variable.params is not None:
|
|
variable.params = calculated_variable.params
|
|
if variable.type == "choice" and variable.choices is None:
|
|
variable.choices = calculated_variable.choices
|
|
if variable.type == "regexp" and variable.regexp is None:
|
|
variable.regexp = calculated_variable.regexp
|
|
|
|
def _convert_variable(
|
|
self,
|
|
variable: dict,
|
|
) -> None:
|
|
# variable without description: description is the name
|
|
if not variable.description:
|
|
variable.description = variable.name
|
|
self.objectspace.forced_descriptions.append(variable.path)
|
|
if variable.type is None:
|
|
variable.type = "string"
|
|
self.objectspace.informations.add(variable.path, "type", variable.type)
|
|
if variable.path in self.objectspace.followers:
|
|
if not variable.multi:
|
|
self.objectspace.multis[variable.path] = True
|
|
else:
|
|
self.objectspace.multis[variable.path] = "submulti"
|
|
elif variable.multi:
|
|
self.objectspace.multis[variable.path] = True
|
|
if variable.path in self.objectspace.leaders:
|
|
family = self.objectspace.paths[variable.path.rsplit(".", 1)[0]]
|
|
if variable.hidden:
|
|
family.hidden = variable.hidden
|
|
# elif family.hidden:
|
|
# variable.hidden = family.hidden
|
|
variable.hidden = None
|
|
if variable.regexp is not None and variable.type != "regexp":
|
|
msg = _(
|
|
'the variable "{0}" has regexp attribut but has not the "regexp" type'
|
|
).format(variable.path)
|
|
raise DictConsistencyError(msg, 37, variable.xmlfiles)
|
|
if variable.mandatory is None:
|
|
variable.mandatory = True
|
|
|
|
def convert_test(self):
|
|
"""Convert variable tests value"""
|
|
for variable in self.get_variables():
|
|
if variable.type == "symlink":
|
|
continue
|
|
if variable.test is None:
|
|
continue
|
|
self.objectspace.informations.add(
|
|
variable.path, "test", tuple(variable.test)
|
|
)
|
|
|
|
def convert_examples(self):
|
|
"""Convert variable tests value"""
|
|
for variable in self.get_variables():
|
|
if variable.type == "symlink":
|
|
continue
|
|
if variable.examples is None:
|
|
continue
|
|
self.objectspace.informations.add(
|
|
variable.path, "examples", tuple(variable.examples)
|
|
)
|
|
|
|
def convert_help(self):
|
|
"""Convert variable help"""
|
|
for variable in self.get_variables():
|
|
if not hasattr(variable, "help") or not variable.help:
|
|
continue
|
|
self.objectspace.informations.add(variable.path, "help", variable.help)
|
|
del variable.help
|
|
|
|
def verify_choices(self):
|
|
for variable in self.get_variables():
|
|
if variable.type is None and variable.choices:
|
|
# choice type inference from the `choices` attribute
|
|
variable.type = "choice"
|
|
if variable.choices is not None and variable.type != "choice":
|
|
msg = _(
|
|
'the variable "{0}" has choices attribut but has not the "choice" type'
|
|
).format(variable.path)
|
|
raise DictConsistencyError(msg, 11, variable.xmlfiles)
|
|
if variable.type != "choice":
|
|
continue
|
|
if variable.default is None:
|
|
continue
|
|
if None in variable.choices and variable.mandatory is None:
|
|
variable.mandatory = False
|
|
if not isinstance(variable.choices, list):
|
|
continue
|
|
choices = variable.choices
|
|
has_calculation = False
|
|
for choice in choices:
|
|
if isinstance(choice, Calculation):
|
|
has_calculation = True
|
|
break
|
|
if has_calculation:
|
|
continue
|
|
|
|
default = variable.default
|
|
if not isinstance(default, list):
|
|
default = [default]
|
|
for value in default:
|
|
if isinstance(value, Calculation):
|
|
continue
|
|
if value not in choices:
|
|
msg = _(
|
|
'the variable "{0}" has an unvalid default value "{1}" should be in {2}'
|
|
).format(
|
|
variable.path,
|
|
value,
|
|
display_list(choices, separator="or", add_quote=True),
|
|
)
|
|
raise DictConsistencyError(msg, 26, variable.xmlfiles)
|