From 529bb1ae7d08bf1bbd273c43a92d3c3ed4173520 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 17 Jan 2021 18:00:29 +0100 Subject: [PATCH] split check, condition and fill --- src/rougail/annotator/__init__.py | 14 +- src/rougail/annotator/check.py | 230 +++++++ src/rougail/annotator/condition.py | 295 +++++++++ src/rougail/annotator/constrainte.py | 597 ------------------ src/rougail/annotator/fill.py | 109 ++++ src/rougail/objspace.py | 1 + tests/dictionaries/10check_base/00-base.xml | 1 - .../80family_dynamic_check/00-base.xml | 21 + .../80family_dynamic_check/__init__.py | 0 .../80family_dynamic_check/errno_22 | 0 10 files changed, 666 insertions(+), 602 deletions(-) create mode 100644 src/rougail/annotator/check.py create mode 100644 src/rougail/annotator/condition.py delete mode 100644 src/rougail/annotator/constrainte.py create mode 100644 src/rougail/annotator/fill.py create mode 100644 tests/dictionaries/80family_dynamic_check/00-base.xml create mode 100644 tests/dictionaries/80family_dynamic_check/__init__.py create mode 100644 tests/dictionaries/80family_dynamic_check/errno_22 diff --git a/src/rougail/annotator/__init__.py b/src/rougail/annotator/__init__.py index 28563885..c6bac217 100644 --- a/src/rougail/annotator/__init__.py +++ b/src/rougail/annotator/__init__.py @@ -3,7 +3,9 @@ from .group import GroupAnnotator from .service import ServiceAnnotator, ERASED_ATTRIBUTES from .variable import VariableAnnotator, CONVERT_OPTION -from .constrainte import ConstrainteAnnotator +from .check import CheckAnnotator +from .condition import Conditionnnotator +from .fill import FillAnnotator from .family import FamilyAnnotator, modes from .property import PropertyAnnotator @@ -15,9 +17,13 @@ class SpaceAnnotator: GroupAnnotator(objectspace) ServiceAnnotator(objectspace) VariableAnnotator(objectspace) - ConstrainteAnnotator(objectspace, - eosfunc_file, - ) + CheckAnnotator(objectspace, + eosfunc_file, + ) + Conditionnnotator(objectspace) + FillAnnotator(objectspace, + eosfunc_file, + ) FamilyAnnotator(objectspace) PropertyAnnotator(objectspace) diff --git a/src/rougail/annotator/check.py b/src/rougail/annotator/check.py new file mode 100644 index 00000000..d8853f12 --- /dev/null +++ b/src/rougail/annotator/check.py @@ -0,0 +1,230 @@ +"""Annotate check +""" +from importlib.machinery import SourceFileLoader +from typing import List, Any + +from .variable import CONVERT_OPTION + +from ..i18n import _ +from ..error import DictConsistencyError + +INTERNAL_FUNCTIONS = ['valid_enum', 'valid_in_network', 'valid_differ', 'valid_entier'] + +class CheckAnnotator: + """Annotate check + """ + def __init__(self, + objectspace, + eosfunc_file, + ): + if not hasattr(objectspace.space, 'constraints') or \ + not hasattr(objectspace.space.constraints, 'check'): + return + self.objectspace = objectspace + eosfunc = SourceFileLoader('eosfunc', eosfunc_file).load_module() + self.functions = dir(eosfunc) + self.functions.extend(INTERNAL_FUNCTIONS) + self.check_check() + self.check_valid_enum() + self.check_change_warning() + self.convert_check() + + def check_check(self): + """valid and manage + """ + remove_indexes = [] + for check_idx, check in enumerate(self.objectspace.space.constraints.check): + if not check.name in self.functions: + xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) + msg = _(f'cannot find check function "{check.name}" in {xmlfiles}') + raise DictConsistencyError(msg, 1) + check_name = check.target + # let's replace the target by the an object + try: + check.target = self.objectspace.paths.get_variable_obj(check.target) + except DictConsistencyError as err: + if err.errno == 36: + xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) + msg = _(f'the target "{check.target}" in check cannot be a dynamic ' + f'variable in {xmlfiles}') + raise DictConsistencyError(msg, 22) + raise err + check.is_in_leadership = self.objectspace.paths.is_in_leadership(check_name) + if not hasattr(check, 'param'): + continue + param_option_indexes = [] + for idx, param in enumerate(check.param): + if param.type == 'variable': + if not self.objectspace.paths.path_is_defined(param.text): + if not param.optional: + xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) + msg = _(f'cannot find check param "{param.text}" in {xmlfiles}') + raise DictConsistencyError(msg, 2) + param_option_indexes.append(idx) + else: + # let's replace params by the path + param.text = self.objectspace.paths.get_variable_obj(param.text) + param_option_indexes.sort(reverse=True) + for idx in param_option_indexes: + check.param.pop(idx) + if check.param == []: + remove_indexes.append(check_idx) + remove_indexes.sort(reverse=True) + for idx in remove_indexes: + del self.objectspace.space.constraints.check[idx] + + @staticmethod + def check_valid_enum_value(variable, + values, + ) -> None: + """check that values in valid_enum are valid + """ + for value in variable.value: + if value.name not in values: + msg = _(f'value "{value.name}" of variable "{variable.name}" is not in list ' + f'of all expected values ({values})') + raise DictConsistencyError(msg, 15) + + def check_valid_enum(self): + """verify valid_enum + """ + remove_indexes = [] + for idx, check in enumerate(self.objectspace.space.constraints.check): + if check.name != 'valid_enum': + continue + if check.target.path in self.objectspace.valid_enums: + check_xmlfiles = self.objectspace.valid_enums[check.target.path]['xmlfiles'] + old_xmlfiles = self.objectspace.display_xmlfiles(check_xmlfiles) + xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) + msg = _(f'valid_enum define in {xmlfiles} but already set in {old_xmlfiles} ' + f'for "{check.target.name}", did you forget remove_check?') + raise DictConsistencyError(msg, 3) + if not hasattr(check, 'param'): + xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) + msg = _(f'param is mandatory for a valid_enum of variable "{check.target.name}" ' + f'in {xmlfiles}') + raise DictConsistencyError(msg, 4) + variable_type = check.target.type + values = self._set_valid_enum(check.target, + check, + ) + if values: + if hasattr(check.target, 'value'): + # check value + self.check_valid_enum_value(check.target, values) + else: + # no value, set the first choice has default value + new_value = self.objectspace.value(check.xmlfiles) + new_value.name = values[0] + new_value.type = variable_type + check.target.value = [new_value] + remove_indexes.append(idx) + remove_indexes.sort(reverse=True) + for idx in remove_indexes: + del self.objectspace.space.constraints.check[idx] + + def _set_valid_enum(self, + variable, + check, + ) -> List[Any]: + # value for choice's variable is mandatory + variable.mandatory = True + # build choice + variable.choice = [] + variable_type = variable.type + variable.type = 'choice' + + has_variable = False + values = [] + for param in check.param: + if has_variable: + xmlfiles = self.objectspace.display_xmlfiles(param.xmlfiles) + msg = _(f'only one "variable" parameter is allowed for valid_enum ' + f'of variable "{variable.name}" in {xmlfiles}') + raise DictConsistencyError(msg, 5) + param_type = variable_type + if param.type == 'variable': + has_variable = True + if param.optional is True: + xmlfiles = self.objectspace.display_xmlfiles(param.xmlfiles) + msg = _(f'optional parameter in valid_enum for variable "{variable.name}" ' + f'is not allowed in {xmlfiles}') + raise DictConsistencyError(msg, 14) + param_variable = param.text + if not param_variable.multi: + xmlfiles = self.objectspace.display_xmlfiles(param.xmlfiles) + msg = _(f'only multi "variable" parameter is allowed for valid_enum ' + f'of variable "{variable.name}" in {xmlfiles}') + raise DictConsistencyError(msg, 6) + param_type = 'calculation' + value = param.text + else: + if 'type' in vars(param) and variable_type != param.type: + xmlfiles = self.objectspace.display_xmlfiles(param.xmlfiles) + msg = _(f'parameter in valid_enum has incompatible type "{param.type}" ' + f'with type of the variable "{variable.name}" ("{variable_type}") ' + f'in {xmlfiles}') + raise DictConsistencyError(msg, 7) + if hasattr(param, 'text'): + try: + value = CONVERT_OPTION[variable_type].get('func', str)(param.text) + except ValueError as err: + msg = _(f'unable to change type of a valid_enum entry "{param.text}" ' + f'is not a valid "{variable_type}" for "{variable.name}"') + raise DictConsistencyError(msg, 13) from err + else: + if param.type == 'number': + xmlfiles = self.objectspace.display_xmlfiles(param.xmlfiles) + msg = _('param type is number, so value is mandatory for valid_enum ' + f'of variable "{variable.name}" in {xmlfiles}') + raise DictConsistencyError(msg, 8) + value = None + values.append(value) + choice = self.objectspace.choice(variable.xmlfiles) + choice.name = value + choice.type = param_type + variable.choice.append(choice) + + if has_variable: + return None + + self.objectspace.valid_enums[check.target.path] = {'type': variable_type, + 'values': values, + 'xmlfiles': check.xmlfiles, + } + return values + + def check_change_warning(self): + """convert level to "warnings_only" + """ + for check in self.objectspace.space.constraints.check: + check.warnings_only = check.level == 'warning' + check.level = None + + def convert_check(self) -> None: + """valid and manage + """ + for check in self.objectspace.space.constraints.check: + if check.name == 'valid_entier': + if not hasattr(check, 'param'): + msg = _(f'{check.name} must have, at least, 1 param') + raise DictConsistencyError(msg, 17) + for param in check.param: + if param.type != 'number': + xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) + msg = _(f'param in "valid_entier" must be an "integer", not "{param.type}"' + f' in {xmlfiles}') + raise DictConsistencyError(msg, 18) + if param.name == 'mini': + check.target.min_number = int(param.text) + elif param.name == 'maxi': + check.target.max_number = int(param.text) + else: + xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) + msg = _(f'unknown parameter "{param.name}" in check "valid_entier" ' + f'for variable "{check.target.name}" in {xmlfiles}') + raise DictConsistencyError(msg, 19) + else: + if not hasattr(check.target, 'check'): + check.target.check = [] + check.target.check.append(check) diff --git a/src/rougail/annotator/condition.py b/src/rougail/annotator/condition.py new file mode 100644 index 00000000..abe21ed9 --- /dev/null +++ b/src/rougail/annotator/condition.py @@ -0,0 +1,295 @@ +"""Annotate condition +""" +from typing import List, Any + + +from ..i18n import _ +from ..error import DictConsistencyError +from ..config import Config + +FREEZE_AUTOFREEZE_VARIABLE = 'module_instancie' + + +class Conditionnnotator: + """Annotate condition + """ + def __init__(self, + objectspace, + ): + self.objectspace = objectspace + 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.convert_condition_target() + self.convert_xxxlist_to_variable() + self.check_condition_fallback() + 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 + """ + def _convert_auto_freeze(variable): + if not variable.auto_freeze: + return + if variable.namespace != Config['variable_namespace']: + xmlfiles = self.objectspace.display_xmlfiles(variable.xmlfiles) + msg = _(f'auto_freeze is not allowed in extra "{variable.namespace}" in {xmlfiles}') + raise DictConsistencyError(msg, 49) + 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) + for variables in self.objectspace.space.variables.values(): + for family in variables.family.values(): + if not hasattr(family, 'variable'): + continue + for variable in family.variable.values(): + if isinstance(variable, self.objectspace.leadership): + for follower in variable.variable: + _convert_auto_freeze(follower) + else: + _convert_auto_freeze(variable) + + def convert_condition_target(self): + """verify and manage target in condition + """ + for condition in self.objectspace.space.constraints.condition: + if not hasattr(condition, 'target'): + xmlfiles = self.objectspace.display_xmlfiles(condition.xmlfiles) + msg = _(f'target is mandatory in a condition for source "{condition.source}" ' + f'in {xmlfiles}') + raise DictConsistencyError(msg, 9) + remove_targets = [] + for index, target in enumerate(condition.target): + try: + if target.type == 'variable': + if condition.source == target.name: + msg = _('target name and source name must be different: ' + f'{condition.source}') + raise DictConsistencyError(msg, 11) + target.name = self.objectspace.paths.get_variable_obj(target.name) + elif target.type == 'family': + target.name = self.objectspace.paths.get_family(target.name, + condition.namespace, + ) + elif target.type.endswith('list') and \ + condition.name not in ['disabled_if_in', 'disabled_if_not_in']: + xmlfiles = self.objectspace.display_xmlfiles(target.xmlfiles) + msg = _(f'target "{target.type}" not allow in condition "{condition.name}" ' + f'in {xmlfiles}') + raise DictConsistencyError(msg, 10) + except DictConsistencyError as err: + if err.errno != 42: + raise err + # for optional variable + if not target.optional: + xmlfiles = self.objectspace.display_xmlfiles(condition.xmlfiles) + msg = f'cannot found target "{target.name}" in the condition in {xmlfiles}' + raise DictConsistencyError(_(msg), 12) + remove_targets.append(index) + remove_targets.sort(reverse=True) + for index in remove_targets: + condition.target.pop(index) + + + def convert_xxxlist_to_variable(self): + """transform *list to variable or family + """ + for condition in self.objectspace.space.constraints.condition: + new_targets = [] + remove_targets = [] + for target_idx, target in enumerate(condition.target): + if target.type.endswith('list'): + listname = target.type + listvars = self.objectspace.list_conditions.get(listname, + {}).get(target.name) + if listvars: + for listvar in listvars: + type_ = 'variable' + new_target = self.objectspace.target(listvar.xmlfiles) + new_target.type = type_ + new_target.name = listvar + new_targets.append(new_target) + remove_targets.append(target_idx) + remove_targets.sort(reverse=True) + for target_idx in remove_targets: + condition.target.pop(target_idx) + condition.target.extend(new_targets) + + 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 True and \ + not self.objectspace.paths.path_is_defined(condition.source): + apply_action = False + 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 = list(set(remove_conditions)) + 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: + leader_or_var, variables = self._get_family_variables_from_target(target) + main_action = actions[0] + 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, + ) + return variable, list(variable.variable.values()) + + 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_obj(condition.source) + except DictConsistencyError as err: + if err.errno == 36: + xmlfiles = self.objectspace.display_xmlfiles(condition.xmlfiles) + msg = _(f'the source "{condition.source}" in condition cannot be a dynamic ' + f'variable in {xmlfiles}') + raise DictConsistencyError(msg, 20) + + def check_choice_option_condition(self): + """remove condition for ChoiceOption that don't have param + """ + remove_conditions = [] + for condition_idx, condition in enumerate(self.objectspace.space.constraints.condition): + # FIXME only string? + if condition.source.path in self.objectspace.valid_enums and \ + self.objectspace.valid_enums[condition.source.path]['type'] == 'string': + valid_enum = self.objectspace.valid_enums[condition.source.path]['values'] + remove_param = [param_idx for param_idx, param in enumerate(condition.param) \ + if param.text not in valid_enum] + 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 + """ + 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, 'property'): + obj.property = [] + obj.property.append(prop) diff --git a/src/rougail/annotator/constrainte.py b/src/rougail/annotator/constrainte.py deleted file mode 100644 index 287f7313..00000000 --- a/src/rougail/annotator/constrainte.py +++ /dev/null @@ -1,597 +0,0 @@ -"""Annotate constraints -""" -from importlib.machinery import SourceFileLoader -from typing import List, Any - -from .variable import CONVERT_OPTION - -from ..i18n import _ -from ..error import DictConsistencyError -from ..config import Config - -FREEZE_AUTOFREEZE_VARIABLE = 'module_instancie' - -INTERNAL_FUNCTIONS = ['valid_enum', 'valid_in_network', 'valid_differ', 'valid_entier'] - - -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 check_valid_enum_value(variable, - values, - ) -> None: - """check that values in valid_enum are valid - """ - for value in variable.value: - if value.name not in values: - msg = _(f'value "{value.name}" of variable "{variable.name}" is not in list ' - f'of all expected values ({values})') - raise DictConsistencyError(msg, 15) - - -class ConstrainteAnnotator: - """Annotate constrainte - """ - def __init__(self, - objectspace, - eosfunc_file, - ): - self.objectspace = objectspace - eosfunc = SourceFileLoader('eosfunc', eosfunc_file).load_module() - self.functions = dir(eosfunc) - self.functions.extend(INTERNAL_FUNCTIONS) - self.valid_enums = {} - if hasattr(objectspace.space, 'variables'): - self.convert_auto_freeze() - if not hasattr(objectspace.space, 'constraints'): - return - if hasattr(self.objectspace.space.constraints, 'check'): - self.check_check() - self.check_valid_enum() - self.check_change_warning() - self.convert_check() - if hasattr(self.objectspace.space.constraints, 'condition'): - self.convert_condition_target() - self.convert_xxxlist_to_variable() - self.check_condition_fallback() - self.convert_condition_source() - self.check_choice_option_condition() - self.remove_condition_with_empty_target() - self.convert_condition() - if hasattr(self.objectspace.space.constraints, 'fill'): - self.convert_fill() - del self.objectspace.space.constraints - - def convert_auto_freeze(self): - """convert auto_freeze - only if FREEZE_AUTOFREEZE_VARIABLE == 'oui' this variable is frozen - """ - def _convert_auto_freeze(variable): - if not variable.auto_freeze: - return - if variable.namespace != Config['variable_namespace']: - xmlfiles = self.objectspace.display_xmlfiles(variable.xmlfiles) - msg = _(f'auto_freeze is not allowed in extra "{variable.namespace}" in {xmlfiles}') - raise DictConsistencyError(msg, 49) - 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) - for variables in self.objectspace.space.variables.values(): - for family in variables.family.values(): - if not hasattr(family, 'variable'): - continue - for variable in family.variable.values(): - if isinstance(variable, self.objectspace.leadership): - for follower in variable.variable: - _convert_auto_freeze(follower) - else: - _convert_auto_freeze(variable) - - def check_check(self): - """valid and manage - """ - remove_indexes = [] - for check_idx, check in enumerate(self.objectspace.space.constraints.check): - if not check.name in self.functions: - xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) - msg = _(f'cannot find check function "{check.name}" in {xmlfiles}') - raise DictConsistencyError(msg, 1) - check.is_in_leadership = self.objectspace.paths.is_in_leadership(check.target) - # let's replace the target by the path - try: - check.target = self.objectspace.paths.get_variable_obj(check.target) - except DictConsistencyError as err: - if err.errno == 36: - xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) - msg = _(f'the target "{check.target}" in check cannot be a dynamic ' - f'variable in {xmlfiles}') - raise DictConsistencyError(msg, 22) - raise err - if not hasattr(check, 'param'): - continue - param_option_indexes = [] - for idx, param in enumerate(check.param): - if param.type == 'variable': - if not self.objectspace.paths.path_is_defined(param.text): - if not param.optional: - xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) - msg = _(f'cannot find check param "{param.text}" in {xmlfiles}') - raise DictConsistencyError(msg, 2) - param_option_indexes.append(idx) - else: - # let's replace params by the path - param.text = self.objectspace.paths.get_variable_obj(param.text) - param_option_indexes.sort(reverse=True) - for idx in param_option_indexes: - check.param.pop(idx) - if check.param == []: - remove_indexes.append(check_idx) - remove_indexes.sort(reverse=True) - for idx in remove_indexes: - del self.objectspace.space.constraints.check[idx] - - def check_valid_enum(self): - """verify valid_enum - """ - remove_indexes = [] - for idx, check in enumerate(self.objectspace.space.constraints.check): - if check.name != 'valid_enum': - continue - if check.target.path in self.valid_enums: - check_xmlfiles = self.valid_enums[check.target.path]['xmlfiles'] - old_xmlfiles = self.objectspace.display_xmlfiles(check_xmlfiles) - xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) - msg = _(f'valid_enum define in {xmlfiles} but already set in {old_xmlfiles} ' - f'for "{check.target.name}", did you forget remove_check?') - raise DictConsistencyError(msg, 3) - if not hasattr(check, 'param'): - xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) - msg = _(f'param is mandatory for a valid_enum of variable "{check.target.name}" ' - f'in {xmlfiles}') - raise DictConsistencyError(msg, 4) - variable_type = check.target.type - values = self._set_valid_enum(check.target, - check, - ) - if values: - if hasattr(check.target, 'value'): - # check value - check_valid_enum_value(check.target, values) - else: - # no value, set the first choice has default value - new_value = self.objectspace.value(check.xmlfiles) - new_value.name = values[0] - new_value.type = variable_type - check.target.value = [new_value] - remove_indexes.append(idx) - remove_indexes.sort(reverse=True) - for idx in remove_indexes: - del self.objectspace.space.constraints.check[idx] - - def _set_valid_enum(self, - variable, - check, - ) -> List[Any]: - # value for choice's variable is mandatory - variable.mandatory = True - # build choice - variable.choice = [] - variable_type = variable.type - variable.type = 'choice' - - has_variable = False - values = [] - for param in check.param: - if has_variable: - xmlfiles = self.objectspace.display_xmlfiles(param.xmlfiles) - msg = _(f'only one "variable" parameter is allowed for valid_enum ' - f'of variable "{variable.name}" in {xmlfiles}') - raise DictConsistencyError(msg, 5) - param_type = variable_type - if param.type == 'variable': - has_variable = True - if param.optional is True: - xmlfiles = self.objectspace.display_xmlfiles(param.xmlfiles) - msg = _(f'optional parameter in valid_enum for variable "{variable.name}" ' - f'is not allowed in {xmlfiles}') - raise DictConsistencyError(msg, 14) - param_variable = param.text - if not param_variable.multi: - xmlfiles = self.objectspace.display_xmlfiles(param.xmlfiles) - msg = _(f'only multi "variable" parameter is allowed for valid_enum ' - f'of variable "{variable.name}" in {xmlfiles}') - raise DictConsistencyError(msg, 6) - param_type = 'calculation' - value = param.text - else: - if 'type' in vars(param) and variable_type != param.type: - xmlfiles = self.objectspace.display_xmlfiles(param.xmlfiles) - msg = _(f'parameter in valid_enum has incompatible type "{param.type}" ' - f'with type of the variable "{variable.name}" ("{variable_type}") ' - f'in {xmlfiles}') - raise DictConsistencyError(msg, 7) - if hasattr(param, 'text'): - try: - value = CONVERT_OPTION[variable_type].get('func', str)(param.text) - except ValueError as err: - msg = _(f'unable to change type of a valid_enum entry "{param.text}" ' - f'is not a valid "{variable_type}" for "{variable.name}"') - raise DictConsistencyError(msg, 13) from err - else: - if param.type == 'number': - xmlfiles = self.objectspace.display_xmlfiles(param.xmlfiles) - msg = _('param type is number, so value is mandatory for valid_enum ' - f'of variable "{variable.name}" in {xmlfiles}') - raise DictConsistencyError(msg, 8) - value = None - values.append(value) - choice = self.objectspace.choice(variable.xmlfiles) - choice.name = value - choice.type = param_type - variable.choice.append(choice) - - if has_variable: - return None - - self.valid_enums[check.target.path] = {'type': variable_type, - 'values': values, - 'xmlfiles': check.xmlfiles, - } - return values - - def check_change_warning(self): - """convert level to "warnings_only" - """ - for check in self.objectspace.space.constraints.check: - check.warnings_only = check.level == 'warning' - check.level = None - - 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, - ) - return variable, list(variable.variable.values()) - - def convert_condition_target(self): - """verify and manage target in condition - """ - for condition in self.objectspace.space.constraints.condition: - if not hasattr(condition, 'target'): - xmlfiles = self.objectspace.display_xmlfiles(condition.xmlfiles) - msg = _(f'target is mandatory in a condition for source "{condition.source}" ' - f'in {xmlfiles}') - raise DictConsistencyError(msg, 9) - remove_targets = [] - for index, target in enumerate(condition.target): - try: - if target.type == 'variable': - if condition.source == target.name: - msg = f'target name and source name must be different: {condition.source}' - raise DictConsistencyError(_(msg), 11) - target.name = self.objectspace.paths.get_variable_obj(target.name) - elif target.type == 'family': - target.name = self.objectspace.paths.get_family(target.name, - condition.namespace, - ) - elif target.type.endswith('list') and \ - condition.name not in ['disabled_if_in', 'disabled_if_not_in']: - xmlfiles = self.objectspace.display_xmlfiles(target.xmlfiles) - msg = _(f'target "{target.type}" not allow in condition "{condition.name}" ' - f'in {xmlfiles}') - raise DictConsistencyError(msg, 10) - except DictConsistencyError as err: - if err.errno != 42: - raise err - # for optional variable - if not target.optional: - xmlfiles = self.objectspace.display_xmlfiles(condition.xmlfiles) - raise DictConsistencyError(_(f'cannot found target "{target.name}" in the condition in {xmlfiles}'), 12) - remove_targets.append(index) - remove_targets.sort(reverse=True) - for index in remove_targets: - condition.target.pop(index) - - - def convert_xxxlist_to_variable(self): - """transform *list to variable or family - """ - for condition in self.objectspace.space.constraints.condition: - new_targets = [] - remove_targets = [] - for target_idx, target in enumerate(condition.target): - if target.type.endswith('list'): - listname = target.type - listvars = self.objectspace.list_conditions.get(listname, - {}).get(target.name) - if listvars: - for listvar in listvars: - type_ = 'variable' - new_target = self.objectspace.target(listvar.xmlfiles) - new_target.type = type_ - new_target.name = listvar - new_targets.append(new_target) - remove_targets.append(target_idx) - remove_targets.sort(reverse=True) - for target_idx in remove_targets: - condition.target.pop(target_idx) - condition.target.extend(new_targets) - - 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 True and \ - not self.objectspace.paths.path_is_defined(condition.source): - apply_action = False - 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 = list(set(remove_conditions)) - 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 = get_actions_from_condition(condition.name) - for target in condition.target: - leader_or_var, variables = self._get_family_variables_from_target(target) - main_action = actions[0] - setattr(leader_or_var, main_action, True) - for action in actions[1:]: - for variable in variables: - setattr(variable, action, True) - - def convert_condition_source(self): - """remove condition for ChoiceOption that don't have param - """ - remove_conditions = [] - for condition_idx, condition in enumerate(self.objectspace.space.constraints.condition): - try: - condition.source = self.objectspace.paths.get_variable_obj(condition.source) - except DictConsistencyError as err: - if err.errno == 36: - xmlfiles = self.objectspace.display_xmlfiles(condition.xmlfiles) - msg = _(f'the source "{condition.source}" in condition cannot be a dynamic ' - f'variable in {xmlfiles}') - raise DictConsistencyError(msg, 20) - - def check_choice_option_condition(self): - """remove condition for ChoiceOption that don't have param - """ - remove_conditions = [] - for condition_idx, condition in enumerate(self.objectspace.space.constraints.condition): - namespace = condition.namespace - # FIXME only string? - if condition.source.path in self.valid_enums and \ - self.valid_enums[condition.source.path]['type'] == 'string': - valid_enum = self.valid_enums[condition.source.path]['values'] - remove_param = [param_idx for param_idx, param in enumerate(condition.param) \ - if param.text not in valid_enum] - 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 - """ - for condition in self.objectspace.space.constraints.condition: - actions = 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, 'property'): - obj.property = [] - obj.property.append(prop) - - def convert_check(self) -> None: - """valid and manage - """ - for check in self.objectspace.space.constraints.check: - if check.name == 'valid_entier': - if not hasattr(check, 'param'): - msg = _(f'{check.name} must have, at least, 1 param') - raise DictConsistencyError(msg, 17) - for param in check.param: - if param.type != 'number': - xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) - msg = _(f'param in "valid_entier" must be an "integer", not "{param.type}"' - f' in {xmlfiles}') - raise DictConsistencyError(msg, 18) - if param.name == 'mini': - check.target.min_number = int(param.text) - elif param.name == 'maxi': - check.target.max_number = int(param.text) - else: - xmlfiles = self.objectspace.display_xmlfiles(check.xmlfiles) - msg = _(f'unknown parameter "{param.name}" in check "valid_entier" ' - f'for variable "{check.target.name}" in {xmlfiles}') - raise DictConsistencyError(msg, 19) - else: - if not hasattr(check.target, 'check'): - check.target.check = [] - check.target.check.append(check) - - def convert_fill(self) -> None: - """valid and manage - """ - targets = [] - for fill in self.objectspace.space.constraints.fill: - # test if it's redefined calculation - if fill.target in targets: - xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) - msg = _(f'A fill already exists for the target of "{fill.target}" created ' - f'in {xmlfiles}') - raise DictConsistencyError(msg, 24) - targets.append(fill.target) - - # test if the function exists - if fill.name not in self.functions: - xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) - msg = _(f'cannot find fill function "{fill.name}" in {xmlfiles}') - raise DictConsistencyError(msg, 25) - - # let's replace the target by the path - fill.target, suffix = self.objectspace.paths.get_variable_path(fill.target, - fill.namespace, - ) - if suffix is not None: - xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) - msg = _(f'Cannot add fill function to "{fill.target}" only ' - f'for the suffix "{suffix}" in {xmlfiles}') - raise DictConsistencyError(msg, 26) - - # get the target variable - variable = self.objectspace.paths.get_variable_obj(fill.target) - - # create an object value - value = self.objectspace.value(fill.xmlfiles) - value.type = 'calculation' - value.name = fill.name - variable.value = [value] - - # manage params - if not hasattr(fill, 'param'): - continue - self.convert_fill_param(fill) - if fill.param: - value.param = fill.param - - def convert_fill_param(self, - fill: "self.objectspace.fill", - ) -> None: - """ valid and convert fill's param - """ - param_to_delete = [] - for param_idx, param in enumerate(fill.param): - if param.type == 'string' and not hasattr(param, 'text'): - param.text = None - if param.type == 'suffix': - if hasattr(param, 'text'): - xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) - msg = _(f'"{param.type}" variables must not have a value in order ' - f'to calculate "{fill.target}" in {xmlfiles}') - raise DictConsistencyError(msg, 28) - if not self.objectspace.paths.variable_is_dynamic(fill.target): - xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) - msg = _('Cannot set suffix target to the none dynamic variable ' - f'"{fill.target}" in {xmlfiles}') - raise DictConsistencyError(msg, 53) - elif not hasattr(param, 'text'): - xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) - msg = _(f'All "{param.type}" variables must have a value in order ' - f'to calculate "{fill.target}" in {xmlfiles}') - raise DictConsistencyError(msg, 27) - if param.type == 'variable': - try: - path, suffix = self.objectspace.paths.get_variable_path(param.text, - fill.namespace, - ) - param.text = self.objectspace.paths.get_variable_obj(path) - if suffix: - param.suffix = suffix - except DictConsistencyError as err: - if err.errno != 42 or not param.optional: - raise err - param_to_delete.append(param_idx) - param_to_delete.sort(reverse=True) - for param_idx in param_to_delete: - fill.param.pop(param_idx) diff --git a/src/rougail/annotator/fill.py b/src/rougail/annotator/fill.py new file mode 100644 index 00000000..8b23254b --- /dev/null +++ b/src/rougail/annotator/fill.py @@ -0,0 +1,109 @@ +"""Fill annotator +""" +from importlib.machinery import SourceFileLoader + +from ..i18n import _ + +from ..error import DictConsistencyError + + +class FillAnnotator: + """Fill annotator + """ + def __init__(self, + objectspace, + eosfunc_file, + ): + self.objectspace = objectspace + if not hasattr(objectspace.space, 'constraints') or \ + not hasattr(self.objectspace.space.constraints, 'fill'): + return + eosfunc = SourceFileLoader('eosfunc', eosfunc_file).load_module() + self.functions = dir(eosfunc) + self.convert_fill() + + def convert_fill(self) -> None: + """valid and manage + """ + targets = [] + for fill in self.objectspace.space.constraints.fill: + # test if it's redefined calculation + if fill.target in targets: + xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) + msg = _(f'A fill already exists for the target of "{fill.target}" created ' + f'in {xmlfiles}') + raise DictConsistencyError(msg, 24) + targets.append(fill.target) + + # test if the function exists + if fill.name not in self.functions: + xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) + msg = _(f'cannot find fill function "{fill.name}" in {xmlfiles}') + raise DictConsistencyError(msg, 25) + + # let's replace the target by the path + fill.target, suffix = self.objectspace.paths.get_variable_path(fill.target, + fill.namespace, + ) + if suffix is not None: + xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) + msg = _(f'Cannot add fill function to "{fill.target}" only ' + f'for the suffix "{suffix}" in {xmlfiles}') + raise DictConsistencyError(msg, 26) + + # get the target variable + variable = self.objectspace.paths.get_variable_obj(fill.target) + + # create an object value + value = self.objectspace.value(fill.xmlfiles) + value.type = 'calculation' + value.name = fill.name + variable.value = [value] + + # manage params + if not hasattr(fill, 'param'): + continue + self.convert_fill_param(fill) + if fill.param: + value.param = fill.param + + def convert_fill_param(self, + fill: "self.objectspace.fill", + ) -> None: + """ valid and convert fill's param + """ + param_to_delete = [] + for param_idx, param in enumerate(fill.param): + if param.type == 'string' and not hasattr(param, 'text'): + param.text = None + if param.type == 'suffix': + if hasattr(param, 'text'): + xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) + msg = _(f'"{param.type}" variables must not have a value in order ' + f'to calculate "{fill.target}" in {xmlfiles}') + raise DictConsistencyError(msg, 28) + if not self.objectspace.paths.variable_is_dynamic(fill.target): + xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) + msg = _('Cannot set suffix target to the none dynamic variable ' + f'"{fill.target}" in {xmlfiles}') + raise DictConsistencyError(msg, 53) + elif not hasattr(param, 'text'): + xmlfiles = self.objectspace.display_xmlfiles(fill.xmlfiles) + msg = _(f'All "{param.type}" variables must have a value in order ' + f'to calculate "{fill.target}" in {xmlfiles}') + raise DictConsistencyError(msg, 27) + if param.type == 'variable': + try: + path, suffix = self.objectspace.paths.get_variable_path(param.text, + fill.namespace, + ) + param.text = self.objectspace.paths.get_variable_obj(path) + if suffix: + param.suffix = suffix + except DictConsistencyError as err: + if err.errno != 42 or not param.optional: + raise err + param_to_delete.append(param_idx) + param_to_delete.sort(reverse=True) + for param_idx in param_to_delete: + fill.param.pop(param_idx) diff --git a/src/rougail/objspace.py b/src/rougail/objspace.py index 1741562e..ad6b7959 100644 --- a/src/rougail/objspace.py +++ b/src/rougail/objspace.py @@ -71,6 +71,7 @@ class RougailObjSpace: self.forced_text_elts_as_name = set(FORCED_TEXT_ELTS_AS_NAME) self.list_conditions = {} + self.valid_enums = {} self.booleans_attributs = [] self.make_object_space_classes(xmlreflector) diff --git a/tests/dictionaries/10check_base/00-base.xml b/tests/dictionaries/10check_base/00-base.xml index c5786883..69c8fede 100644 --- a/tests/dictionaries/10check_base/00-base.xml +++ b/tests/dictionaries/10check_base/00-base.xml @@ -8,7 +8,6 @@ - 0 diff --git a/tests/dictionaries/80family_dynamic_check/00-base.xml b/tests/dictionaries/80family_dynamic_check/00-base.xml new file mode 100644 index 00000000..571af0c3 --- /dev/null +++ b/tests/dictionaries/80family_dynamic_check/00-base.xml @@ -0,0 +1,21 @@ + + + + + + + val1 + val2 + + + + + + + + + 0 + 100 + + + diff --git a/tests/dictionaries/80family_dynamic_check/__init__.py b/tests/dictionaries/80family_dynamic_check/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/dictionaries/80family_dynamic_check/errno_22 b/tests/dictionaries/80family_dynamic_check/errno_22 new file mode 100644 index 00000000..e69de29b