rougail/src/rougail/annotator/condition.py
2023-11-10 22:33:05 +01:00

392 lines
18 KiB
Python

"""Annotate condition
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-2023
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 List
from rougail.i18n import _
from rougail.error import DictConsistencyError
from rougail.annotator.target import TargetAnnotator
from rougail.annotator.param import ParamAnnotator
from rougail.annotator.variable import Walk
class FakeVariable:
def __getattr__(self, name):
return None
class Annotator(TargetAnnotator, ParamAnnotator, Walk):
"""Annotate condition
"""
level = 50
def __init__(self,
objectspace,
*args,
):
self.objectspace = objectspace
# self.target_is_uniq = False
# self.only_variable = False
# self.allow_function = False
# self.force_service_value = {}
#for path_prefix, constraints in self.get_constraints():
# if not hasattr(constraints, 'condition'):
# continue
# self.convert_target(constraints.condition, path_prefix)
# self.check_condition_optional(constraints, path_prefix)
# self.convert_condition_source(constraints, path_prefix)
# self.convert_param(constraints.condition, path_prefix)
# self.check_source_target(constraints)
# self.convert_xxxlist(constraints, path_prefix)
# self.check_choice_option_condition(constraints, path_prefix)
# self.remove_condition_with_empty_target(constraints)
# self.convert_condition(constraints, path_prefix)
def valid_type_validation(self,
obj,
) -> None:
if obj.source.type == 'choice':
return None
return obj.source.type
def check_source_target(self, constraints):
"""verify that source != target in condition
"""
for condition in constraints.condition:
for target in condition.target:
if target.type == 'variable' and \
condition.source.path == target.name.path:
msg = _('target name and source name must be different: '
f'{condition.source.path}')
raise DictConsistencyError(msg, 11, condition.xmlfiles)
def check_condition_optional(self,
constraints,
path_prefix,
):
"""a condition with a optional **and** the source variable doesn't exist
"""
remove_conditions = []
for idx, condition in enumerate(constraints.condition):
if condition.optional is False or \
self.objectspace.paths.path_is_defined(condition.source,
condition.namespace,
path_prefix,
):
continue
remove_conditions.append(idx)
if (hasattr(condition, 'apply_on_fallback') and condition.apply_on_fallback) or \
(not hasattr(condition, 'apply_on_fallback') and \
condition.name.endswith('_if_in')):
self.force_actions_to_variable(condition,
path_prefix,
)
remove_conditions.sort(reverse=True)
for idx in remove_conditions:
constraints.condition.pop(idx)
def force_actions_to_variable(self,
condition: 'self.objectspace.condition',
path_prefix,
) -> None:
"""force property to a variable
for example disabled_if_not_in => variable.disabled = True
"""
actions = self.get_actions_from_condition(condition.name)
for target in condition.target:
main_action = actions[0]
if target.type.endswith('list'):
self.force_service_value[target.name] = main_action != 'disabled'
continue
leader_or_var, variables = self._get_family_variables_from_target(target,
path_prefix,
)
setattr(leader_or_var, main_action, True)
for action in actions[1:]:
for variable in variables:
setattr(variable, action, True)
@staticmethod
def get_actions_from_condition(condition_name: str) -> List[str]:
"""get action's name from a condition
"""
if condition_name.startswith('hidden_if_'):
return ['hidden', 'frozen', 'force_default_on_freeze']
if condition_name == 'auto_frozen_if_in':
return ['frozen']
return [condition_name.split('_', 1)[0]]
def _get_family_variables_from_target(self,
target,
path_prefix,
):
if target.type == 'variable':
if not self.objectspace.paths.is_leader(target.name):
return target.name, [target.name]
# it's a leader, so apply property to leadership
namespace = target.name.namespace
family_name = self.objectspace.paths.get_variable_family_path(target.name.path,
namespace,
force_path_prefix=path_prefix,
)
else:
family_name = target.name.path
namespace = target.namespace
variable = self.objectspace.paths.get_family(family_name,
namespace,
path_prefix,
)
if hasattr(variable, 'variable'):
return variable, list(variable.variable.values())
return variable, []
def convert_xxxlist(self, constraints, path_prefix):
"""transform *list to variable or family
"""
fills = {}
for condition in constraints.condition:
remove_targets = []
for target_idx, target in enumerate(condition.target):
if target.type.endswith('list'):
listvars = self.objectspace.paths.list_conditions[path_prefix].get(target.type,
{}).get(target.name)
if listvars:
self._convert_xxxlist_to_fill(constraints,
condition,
target,
listvars,
fills,
)
elif not target.optional and self.objectspace.just_doc is False:
msg = f'cannot found target "{target.type}" "{target.name}"'
raise DictConsistencyError(_(msg), 2, target.xmlfiles)
remove_targets.append(target_idx)
remove_targets.sort(reverse=True)
for target_idx in remove_targets:
condition.target.pop(target_idx)
def _convert_xxxlist_to_fill(self,
constraints,
condition: 'self.objectspace.condition',
target: 'self.objectspace.target',
listvars: list,
fills: dict,
):
for listvar in listvars:
if target.name in self.force_service_value:
# do not add fill, just change default value
listvar.default = self.force_service_value[target.name]
continue
if listvar.path in fills:
# a fill is already set for this path
fill = fills[listvar.path]
or_needed = True
for param in fill.param:
if hasattr(param, 'name') and \
param.name == 'condition_operator':
or_needed = False
break
fill.index += 1
else:
fill = self.objectspace.fill(target.xmlfiles)
new_target = self.objectspace.target(target.xmlfiles)
path = listvar.path
if listvar.path_prefix:
path = path.split('.', 1)[-1]
new_target.name = path
fill.target = [new_target]
fill.name = 'calc_value'
fill.namespace = 'services'
fill.index = 0
if not hasattr(constraints, 'fill'):
constraints.fill = []
constraints.fill.append(fill)
fills[listvar.path] = fill
param1 = self.objectspace.param(target.xmlfiles)
param1.text = False
param1.type = 'boolean'
param2 = self.objectspace.param(target.xmlfiles)
param2.name = 'default'
param2.text = True
param2.type = 'boolean'
fill.param = [param1, param2]
or_needed = len(condition.param) != 1
if len(condition.param) == 1:
values = getattr(condition.param[0], 'text', None)
param_type = condition.param[0].type
else:
values = tuple([getattr(param, 'text', None) for param in condition.param])
param_type = None
for param in condition.param:
if param_type is None or param_type == 'nil':
param_type = param.type
param3 = self.objectspace.param(target.xmlfiles)
param3.name = f'condition_{fill.index}'
param3.type = 'variable'
path = condition.source.path
if condition.source.path_prefix:
path = path.split('.', 1)[-1]
param3.text = path
param3.propertyerror = False
fill.param.append(param3)
param4 = self.objectspace.param(target.xmlfiles)
param4.name = f'expected_{fill.index}'
param4.text = values
param4.type = param_type
fill.param.append(param4)
if condition.name != 'disabled_if_in':
param5 = self.objectspace.param(target.xmlfiles)
param5.name = f'reverse_condition_{fill.index}'
param5.text = True
param5.type = 'boolean'
fill.param.append(param5)
if or_needed:
param6 = self.objectspace.param(target.xmlfiles)
param6.name = 'condition_operator'
param6.text = 'OR'
fill.param.append(param6)
def convert_condition_source(self, constraints, path_prefix):
"""remove condition for ChoiceOption that don't have param
"""
for condition in constraints.condition:
try:
condition.source = self.objectspace.paths.get_variable(condition.source,
condition.namespace,
allow_variable_namespace=True,
force_path_prefix=path_prefix,
add_path_prefix=True,
)
except DictConsistencyError as err:
if self.objectspace.just_doc:
condition.source = FakeVariable()
continue
if err.errno == 36:
msg = _(f'the source "{condition.source}" in condition cannot be a dynamic '
f'variable')
raise DictConsistencyError(msg, 20, condition.xmlfiles) from err
if err.errno == 42:
msg = _(f'the source "{condition.source}" in condition is an unknown variable')
raise DictConsistencyError(msg, 23, condition.xmlfiles) from err
raise err from err # pragma: no cover
def check_choice_option_condition(self,
constraints,
path_prefix,
):
"""remove condition of ChoiceOption that doesn't match
"""
remove_conditions = []
for condition_idx, condition in enumerate(constraints.condition):
if not self.objectspace.paths.has_valid_enums(condition.source.path,
condition.source.path_prefix,
):
continue
valid_enum = self.objectspace.paths.get_valid_enums(condition.source.path,
condition.source.path_prefix,
)
remove_param = [param_idx for param_idx, param in enumerate(condition.param) \
if param.type != 'variable' and param.text not in valid_enum]
if not remove_param:
continue
remove_param.sort(reverse=True)
for idx in remove_param:
del condition.param[idx]
if not condition.param and condition.name.endswith('_if_not_in'):
self.force_actions_to_variable(condition,
path_prefix,
)
remove_conditions.append(condition_idx)
remove_conditions.sort(reverse=True)
for idx in remove_conditions:
constraints.condition.pop(idx)
def remove_condition_with_empty_target(self, constraints):
"""remove condition with empty target
"""
# optional target are remove, condition could be empty
remove_conditions = [condition_idx for condition_idx, condition in \
enumerate(constraints.condition) \
if not condition.target]
remove_conditions.sort(reverse=True)
for idx in remove_conditions:
constraints.condition.pop(idx)
def convert_condition(self, constraints, path_prefix):
"""valid and manage <condition>
"""
for condition in constraints.condition:
actions = self.get_actions_from_condition(condition.name)
for param in condition.param:
for target in condition.target:
leader_or_variable, variables = self._get_family_variables_from_target(target,
path_prefix,
)
# if option is already disable, do not apply disable_if_in
# check only the first action (example of multiple actions:
# 'hidden', 'frozen', 'force_default_on_freeze')
main_action = actions[0]
if getattr(leader_or_variable, main_action, False) is True:
continue
self.build_property(leader_or_variable,
param,
condition,
main_action,
)
if isinstance(leader_or_variable, self.objectspace.variable) and \
leader_or_variable.auto_save and \
'force_default_on_freeze' in actions:
continue
for action in actions[1:]:
# other actions are set to the variable or children of family
for variable in variables:
self.build_property(variable,
param,
condition,
action,
)
def build_property(self,
obj,
param: 'self.objectspace.param',
condition: 'self.objectspace.condition',
action: str,
) -> 'self.objectspace.property_':
"""build property_ for a condition
"""
prop = self.objectspace.property_(obj.xmlfiles)
prop.type = 'calculation'
prop.inverse = condition.name.endswith('_if_not_in')
prop.source = condition.source
prop.expected = param
prop.name = action
if not hasattr(obj, 'properties'):
obj.properties = []
obj.properties.append(prop)