rougail/src/rougail/annotator/condition.py

361 lines
16 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
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, Any
from ..i18n import _
from ..error import DictConsistencyError
from ..config import Config
from .target import TargetAnnotator
from .param import ParamAnnotator
from .variable import Walk
FREEZE_AUTOFREEZE_VARIABLE = 'module_instancie'
class ConditionAnnotator(TargetAnnotator, ParamAnnotator, Walk):
"""Annotate condition
"""
def __init__(self,
objectspace,
):
self.objectspace = objectspace
self.force_service_value = {}
if hasattr(objectspace.space, 'variables'):
self.convert_auto_freeze()
if not hasattr(objectspace.space, 'constraints') or \
not hasattr(self.objectspace.space.constraints, 'condition'):
return
self.target_is_uniq = False
self.only_variable = False
self.convert_target(self.objectspace.space.constraints.condition)
self.convert_param(self.objectspace.space.constraints.condition)
self.check_source_target()
self.check_condition_fallback()
self.convert_xxxlist()
self.convert_condition_source()
self.check_choice_option_condition()
self.remove_condition_with_empty_target()
self.convert_condition()
def convert_auto_freeze(self):
"""convert auto_freeze
only if FREEZE_AUTOFREEZE_VARIABLE == 'oui' this variable is frozen
"""
for variable in self.get_variables():
self._convert_auto_freeze(variable)
def _convert_auto_freeze(self,
variable: 'self.objectspace.variable',
) -> None:
if not variable.auto_freeze:
return
if variable.namespace != Config['variable_namespace']:
msg = _(f'auto_freeze is not allowed in extra "{variable.namespace}"')
raise DictConsistencyError(msg, 49, variable.xmlfiles)
new_condition = self.objectspace.condition(variable.xmlfiles)
new_condition.name = 'auto_frozen_if_not_in'
new_condition.namespace = variable.namespace
new_condition.source = FREEZE_AUTOFREEZE_VARIABLE
new_param = self.objectspace.param(variable.xmlfiles)
new_param.text = 'oui'
new_condition.param = [new_param]
new_target = self.objectspace.target(variable.xmlfiles)
new_target.type = 'variable'
new_target.name = variable.name
new_condition.target = [new_target]
if not hasattr(self.objectspace.space, 'constraints'):
self.objectspace.space.constraints = self.objectspace.constraints(variable.xmlfiles)
if not hasattr(self.objectspace.space.constraints, 'condition'):
self.objectspace.space.constraints.condition = []
self.objectspace.space.constraints.condition.append(new_condition)
def check_source_target(self):
"""verify that source != target in condition
"""
for condition in self.objectspace.space.constraints.condition:
for target in condition.target:
if target.type == 'variable' and \
condition.source in [target.name.name, target.name.path]:
msg = _('target name and source name must be different: '
f'{condition.source}')
raise DictConsistencyError(msg, 11, condition.xmlfiles)
def check_condition_fallback(self):
"""a condition with a fallback **and** the source variable doesn't exist
"""
remove_conditions = []
for idx, condition in enumerate(self.objectspace.space.constraints.condition):
# fallback
if condition.fallback is False or \
self.objectspace.paths.path_is_defined(condition.source):
continue
if condition.name in ['disabled_if_in', 'mandatory_if_in', 'hidden_if_in']:
apply_action = not condition.force_condition_on_fallback
else:
apply_action = condition.force_inverse_condition_on_fallback
remove_conditions.append(idx)
if apply_action:
self.force_actions_to_variable(condition)
remove_conditions.sort(reverse=True)
for idx in remove_conditions:
self.objectspace.space.constraints.condition.pop(idx)
def force_actions_to_variable(self,
condition: 'self.objectspace.condition',
) -> 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)
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_not_in':
return ['auto_frozen']
return [condition_name.split('_', 1)[0]]
def _get_family_variables_from_target(self,
target,
):
if target.type == 'variable':
if not self.objectspace.paths.is_leader(target.name.path):
return target.name, [target.name]
# it's a leader, so apply property to leadership
family_name = self.objectspace.paths.get_variable_family_path(target.name.path)
family = self.objectspace.paths.get_family(family_name,
target.name.namespace,
)
return family, family.variable
# it's a family
variable = self.objectspace.paths.get_family(target.name.path,
target.namespace,
)
if hasattr(variable, 'variable'):
return variable, list(variable.variable.values())
return variable, []
def convert_xxxlist(self):
"""transform *list to variable or family
"""
fills = {}
for condition in self.objectspace.space.constraints.condition:
remove_targets = []
for target_idx, target in enumerate(condition.target):
if target.type.endswith('list'):
listvars = self.objectspace.list_conditions.get(target.type,
{}).get(target.name)
if listvars:
self._convert_xxxlist_to_fill(condition,
target,
listvars,
fills,
)
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,
condition: 'self.objectspace.condition',
target: 'self.objectspace.target',
listvars: list,
fills: dict,
):
for listvar in listvars:
if target.name in self.force_service_value:
listvar.default = self.force_service_value[target.name]
continue
if listvar.path in fills:
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)
new_target.name = listvar.path
fill.target = [new_target]
fill.name = 'calc_value'
fill.namespace = 'services'
fill.index = 0
if not hasattr(self.objectspace.space.constraints, 'fill'):
self.objectspace.space.constraints.fill = []
self.objectspace.space.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)
else:
values = tuple([getattr(param, 'text', None) for param in condition.param])
param3 = self.objectspace.param(target.xmlfiles)
param3.name = f'condition_{fill.index}'
param3.type = 'variable'
param3.text = condition.source
fill.param.append(param3)
param4 = self.objectspace.param(target.xmlfiles)
param4.name = f'expected_{fill.index}'
param4.text = values
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):
"""remove condition for ChoiceOption that don't have param
"""
for condition in self.objectspace.space.constraints.condition:
try:
condition.source = self.objectspace.paths.get_variable(condition.source)
except DictConsistencyError as err:
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):
"""remove condition of ChoiceOption that doesn't match
"""
remove_conditions = []
for condition_idx, condition in enumerate(self.objectspace.space.constraints.condition):
if condition.source.path in self.objectspace.valid_enums:
valid_enum = self.objectspace.valid_enums[condition.source.path]['values']
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)
remove_conditions.append(condition_idx)
remove_conditions.sort(reverse=True)
for idx in remove_conditions:
self.objectspace.space.constraints.condition.pop(idx)
def remove_condition_with_empty_target(self):
"""remove condition with empty target
"""
# optional target are remove, condition could be empty
remove_conditions = [condition_idx for condition_idx, condition in \
enumerate(self.objectspace.space.constraints.condition) \
if not condition.target]
remove_conditions.sort(reverse=True)
for idx in remove_conditions:
self.objectspace.space.constraints.condition.pop(idx)
def convert_condition(self):
"""valid and manage <condition>
"""
for condition in self.objectspace.space.constraints.condition:
actions = self.get_actions_from_condition(condition.name)
for param in condition.param:
text = getattr(param, 'text', None)
for target in condition.target:
leader_or_variable, variables = self._get_family_variables_from_target(target)
# 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,
text,
condition,
main_action,
)
if isinstance(leader_or_variable, self.objectspace.variable) and \
(leader_or_variable.auto_save or leader_or_variable.auto_freeze) 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,
text,
condition,
action,
)
def build_property(self,
obj,
text: Any,
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 = text
prop.name = action
if not hasattr(obj, 'properties'):
obj.properties = []
obj.properties.append(prop)