diff --git a/ChangeLog b/ChangeLog index 4142405..ade2d3c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,6 @@ +Sat May 20 16:27:09 2017 +0200 Emmanuel Garette + * add 'operator' to requirement + Wed May 17 22:11:55 2017 +0200 Emmanuel Garette * add 'remove' to Multi diff --git a/test/test_requires.py b/test/test_requires.py index 2ea010c..bd499b2 100644 --- a/test/test_requires.py +++ b/test/test_requires.py @@ -431,6 +431,122 @@ def test_requires_multi_disabled(): assert props == ['disabled'] +def test_requires_multi_disabled_new_format(): + a = BoolOption('activate_service', '') + b = IntOption('num_service', '') + c = IPOption('ip_address_service', '', + requires=[{'expected': [{'option': a, 'value': True}, {'option': b, 'value': 1}], 'action': 'disabled'}]) + od = OptionDescription('service', '', [a, b, c]) + c = Config(od) + c.read_write() + + c.ip_address_service + + c.activate_service = True + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert props == ['disabled'] + + c.activate_service = False + c.ip_address_service + + c.num_service = 1 + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert props == ['disabled'] + + c.activate_service = True + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert props == ['disabled'] + + +def test_requires_multi_disabled_new_format_and(): + a = BoolOption('activate_service', '') + b = IntOption('num_service', '') + c = IPOption('ip_address_service', '', + requires=[{'expected': [{'option': a, 'value': True}, {'option': b, 'value': 1}], 'action': 'disabled', 'operator': 'and'}]) + od = OptionDescription('service', '', [a, b, c]) + c = Config(od) + c.read_write() + + c.ip_address_service + + c.activate_service = True + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert props == [] + + c.activate_service = False + c.ip_address_service + + c.num_service = 1 + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert props == [] + + c.activate_service = True + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert props == ['disabled'] + + +def test_requires_multi_disabled_new_format_and_2(): + a = BoolOption('activate_service', '') + b = IntOption('num_service', '') + c = IPOption('ip_address_service', '', + requires=[{'expected': [{'option': a, 'value': True}, {'option': b, 'value': 1}], 'action': 'disabled', 'operator': 'and'}, + {'expected': [{'option': a, 'value': False}, {'option': b, 'value': 1}], 'action': 'expert'}]) + od = OptionDescription('service', '', [a, b, c]) + c = Config(od) + c.cfgimpl_get_settings().append('expert') + c.read_write() + c.ip_address_service + + c.activate_service = True + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert props == [] + + c.activate_service = False + c.num_service = 1 + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert props == ['expert'] + + c.activate_service = True + props = [] + try: + c.ip_address_service + except PropertiesOptionError as err: + props = err.proptype + assert props == ['disabled', 'expert'] + + def test_requires_multi_disabled_inverse(): a = BoolOption('activate_service', '') b = IntOption('num_service', '') diff --git a/test/test_state.py b/test/test_state.py index 75e23dd..dd56b63 100644 --- a/test/test_state.py +++ b/test/test_state.py @@ -78,8 +78,10 @@ def _diff_opt(opt1, opt2): if val1 == val2 == []: pass else: - assert val1[0][0][0]._name == val2[0][0][0]._name - assert val1[0][0][1:] == val2[0][0][1:] + for idx, req in enumerate(val1[0][0][0]): + assert val1[0][0][0][idx][0]._name == val2[0][0][0][idx][0]._name + assert val1[0][0][0][idx][1] == val2[0][0][0][idx][1] + assert val1[0][0][1:] == val2[0][0][1:] elif attr == '_opt': assert val1._name == val2._name elif attr == '_consistencies': diff --git a/tiramisu/option/baseoption.py b/tiramisu/option/baseoption.py index 55021b3..47d361b 100644 --- a/tiramisu/option/baseoption.py +++ b/tiramisu/option/baseoption.py @@ -258,20 +258,22 @@ class BaseOption(Base): new_value = [] for requires in _requires: new_requires = [] + new_req = [] for require in requires: - if load: - new_require = [descr.impl_get_opt_by_path(require[0])] - else: - new_require = [descr.impl_get_path_by_opt(require[0])] - new_require.extend(require[1:]) - new_requires.append(tuple(new_require)) + for req in require[0]: + if load: + new_req.append([(descr.impl_get_opt_by_path(req[0]), req[1])]) + else: + new_req.append([(descr.impl_get_path_by_opt(req[0]), req[1])]) + new_req.extend(require[1:]) + new_requires.append(tuple(new_req)) new_value.append(tuple(new_requires)) if load: del(self._state_requires) if new_value != []: - self._requires = new_value + self._requires = tuple(new_value) else: - self._state_requires = new_value + self._state_requires = tuple(new_value) # serialize def _impl_getstate(self, descr): @@ -932,6 +934,94 @@ def validate_requires_arg(multi, requires, name): know more about the description of the requires dictionary """ + def get_option(require): + option = require['option'] + if not isinstance(option, Option): + raise ValueError(_('malformed requirements ' + 'must be an option in option {0}').format(name)) + if not multi and option.impl_is_multi(): + raise ValueError(_('malformed requirements ' + 'multi option must not set ' + 'as requires of non multi option {0}').format(name)) + return option + + def _set_expected(action, inverse, transitive, same_action, option, expected, operator): + if inverse not in ret_requires[action]: + ret_requires[action][inverse] = ([(option, [expected])], action, inverse, transitive, same_action, operator) + else: + for exp in ret_requires[action][inverse][0]: + if exp[0] == option: + exp[1].append(expected) + break + else: + ret_requires[action][inverse][0].append((option, [expected])) + + def set_expected(require, ret_requires): + expected = require['expected'] + inverse = get_inverse(require) + transitive = get_transitive(require) + same_action = get_sameaction(require) + operator = get_operator(require) + if isinstance(expected, list): + for exp in expected: + if exp.keys() != ['option', 'value']: + raise ValueError(_('malformed requirements expected must have ' + 'option and value for option {0}').format(name)) + option = exp['option'] + if option is not None: + err = option._validate(exp['value']) + if err: + raise ValueError(_('malformed requirements expected value ' + 'must be valid for option {0}' + ': {1}').format(name, err)) + _set_expected(action, inverse, transitive, same_action, option, exp['value'], operator) + else: + option = get_option(require) + if expected is not None: + err = option._validate(expected) + if err: + raise ValueError(_('malformed requirements expected value ' + 'must be valid for option {0}' + ': {1}').format(name, err)) + _set_expected(action, inverse, transitive, same_action, option, expected, operator) + + def get_action(require): + action = require['action'] + if action == 'force_store_value': + raise ValueError(_("malformed requirements for option: {0}" + " action cannot be force_store_value" + ).format(name)) + return action + + def get_inverse(require): + inverse = require.get('inverse', False) + if inverse not in [True, False]: + raise ValueError(_('malformed requirements for option: {0}' + ' inverse must be boolean')) + return inverse + + def get_transitive(require): + transitive = require.get('transitive', True) + if transitive not in [True, False]: + raise ValueError(_('malformed requirements for option: {0}' + ' transitive must be boolean')) + return transitive + + def get_sameaction(require): + same_action = require.get('same_action', True) + if same_action not in [True, False]: + raise ValueError(_('malformed requirements for option: {0}' + ' same_action must be boolean')) + return same_action + + def get_operator(require): + operator = require.get('operator', 'or') + if operator not in ['and', 'or']: + raise ValueError(_('malformed requirements for option: {0}' + ' operator must be "or" or "and"')) + return operator + + ret_requires = {} config_action = set() @@ -942,7 +1032,7 @@ def validate_requires_arg(multi, requires, name): raise ValueError(_("malformed requirements type for option:" " {0}, must be a dict").format(name)) valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive', - 'same_action') + 'same_action', 'operator') unknown_keys = frozenset(require.keys()) - frozenset(valid_keys) if unknown_keys != frozenset(): raise ValueError(_('malformed requirements for option: {0}' @@ -951,62 +1041,25 @@ def validate_requires_arg(multi, requires, name): unknown_keys, valid_keys)) # prepare all attributes - if 'option' not in require or 'expected' not in require or \ + if not ('expected' in require and isinstance(require['expected'], list)) and \ + not ('option' in require and 'expected' in require) or \ 'action' not in require: raise ValueError(_("malformed requirements for option: {0}" " require must have option, expected and" " action keys").format(name)) - option = require['option'] - expected = require['expected'] - action = require['action'] - if action == 'force_store_value': - raise ValueError(_("malformed requirements for option: {0}" - " action cannot be force_store_value" - ).format(name)) - inverse = require.get('inverse', False) - if inverse not in [True, False]: - raise ValueError(_('malformed requirements for option: {0}' - ' inverse must be boolean')) - transitive = require.get('transitive', True) - if transitive not in [True, False]: - raise ValueError(_('malformed requirements for option: {0}' - ' transitive must be boolean')) - same_action = require.get('same_action', True) - if same_action not in [True, False]: - raise ValueError(_('malformed requirements for option: {0}' - ' same_action must be boolean')) - - if not isinstance(option, Option): - raise ValueError(_('malformed requirements ' - 'must be an option in option {0}').format(name)) - if not multi and option.impl_is_multi(): - raise ValueError(_('malformed requirements ' - 'multi option must not set ' - 'as requires of non multi option {0}').format(name)) - if expected is not None: - err = option._validate(expected) - if err: - raise ValueError(_('malformed requirements expected value ' - 'must be valid for option {0}' - ': {1}').format(name, err)) + action = get_action(require) config_action.add(action) if action not in ret_requires: ret_requires[action] = {} - if option not in ret_requires[action]: - ret_requires[action][option] = {} - if inverse not in ret_requires[action][option]: - ret_requires[action][option][inverse] = (option, [expected], action, - inverse, transitive, same_action) - else: - ret_requires[action][option][inverse][1].append(expected) + set_expected(require, ret_requires) + # transform dict to tuple ret = [] - for opt_requires in ret_requires.values(): + for requires in ret_requires.values(): ret_action = [] - for requires in opt_requires.values(): - for require in requires.values(): - ret_action.append((require[0], tuple(require[1]), require[2], - require[3], require[4], require[5])) + for require in requires.values(): + ret_action.append((tuple(require[0]), require[1], + require[2], require[3], require[4], require[5])) ret.append(tuple(ret_action)) return frozenset(config_action), tuple(ret) diff --git a/tiramisu/option/optiondescription.py b/tiramisu/option/optiondescription.py index e7550d6..991274b 100644 --- a/tiramisu/option/optiondescription.py +++ b/tiramisu/option/optiondescription.py @@ -162,21 +162,21 @@ class OptionDescription(BaseOption, StorageOptionDescription): # * option in require must be a master or a slave # * current option must be a slave (and only a slave) # * option in require and current option must be in same master/slaves - require_opt = require[0] - if require_opt.impl_is_multi(): - if is_slave is None: - is_slave = option.impl_is_master_slaves('slave') - if is_slave: - masterslaves = option.impl_get_master_slaves() - if is_slave and require_opt.impl_is_master_slaves(): - if masterslaves != require_opt.impl_get_master_slaves(): + for require_opt, values in require[0]: + if require_opt.impl_is_multi(): + if is_slave is None: + is_slave = option.impl_is_master_slaves('slave') + if is_slave: + masterslaves = option.impl_get_master_slaves() + if is_slave and require_opt.impl_is_master_slaves(): + if masterslaves != require_opt.impl_get_master_slaves(): + raise ValueError(_('malformed requirements option {0} ' + 'must be in same master/slaves for {1}').format( + require_opt.impl_getname(), option.impl_getname())) + else: raise ValueError(_('malformed requirements option {0} ' - 'must be in same master/slaves for {1}').format( + 'must not be a multi for {1}').format( require_opt.impl_getname(), option.impl_getname())) - else: - raise ValueError(_('malformed requirements option {0} ' - 'must not be a multi for {1}').format( - require_opt.impl_getname(), option.impl_getname())) if init: session = config._impl_values._p_.getsession() if len(cache_option) != len(set(cache_option)): diff --git a/tiramisu/setting.py b/tiramisu/setting.py index 5ed3a16..ce3d1e5 100644 --- a/tiramisu/setting.py +++ b/tiramisu/setting.py @@ -644,68 +644,85 @@ class Settings(object): all_properties = None for requires in current_requires: for require in requires: - option, expected, action, inverse, \ - transitive, same_action = require - reqpath = option.impl_getpath(context) - if reqpath == path or reqpath.startswith(path + '.'): # pragma: optional cover - raise RequirementError(_("malformed requirements " - "imbrication detected for option:" - " '{0}' with requirement on: " - "'{1}'").format(path, reqpath)) - if option.impl_is_multi(): - if index is None: - continue - idx = index - else: - idx = None - value = context.getattr(reqpath, force_permissive=True, - _setting_properties=setting_properties, - index=idx, returns_raise=True) - if isinstance(value, Exception): - if isinstance(value, PropertiesOptionError): - if not transitive: - if all_properties is None: - all_properties = [] - for requires in opt.impl_getrequires(): - for require in requires: - all_properties.append(require[2]) - if not set(value.proptype) - set(all_properties): - continue - properties = value.proptype - if same_action and action not in properties: # pragma: optional cover - if len(properties) == 1: - prop_msg = _('property') - else: - prop_msg = _('properties') - raise RequirementError(_('cannot access to option "{0}" because ' - 'required option "{1}" has {2} {3}' - '').format(opt.impl_get_display_name(), - option.impl_get_display_name(), - prop_msg, - display_list(properties))) - orig_value = value - # transitive action, force expected - value = expected[0] - inverse = False + exps, action, inverse, \ + transitive, same_action, operator = require + breaked = False + for exp in exps: + option, expected = exp + reqpath = option.impl_getpath(context) + if reqpath == path or reqpath.startswith(path + '.'): # pragma: optional cover + raise RequirementError(_("malformed requirements " + "imbrication detected for option:" + " '{0}' with requirement on: " + "'{1}'").format(path, reqpath)) + if option.impl_is_multi(): + if index is None: + # multi is allowed only for slaves + # so do not calculated requires if no index + continue + idx = index else: - raise value - else: - orig_value = value - if (not inverse and value in expected or - inverse and value not in expected): - if debug: - if isinstance(orig_value, PropertiesOptionError): - for act, msg in orig_value._settings.apply_requires(**orig_value._datas).items(): - calc_properties.setdefault(action, []).extend(msg) + idx = None + value = context.getattr(reqpath, force_permissive=True, + _setting_properties=setting_properties, + index=idx, returns_raise=True) + if isinstance(value, Exception): + if isinstance(value, PropertiesOptionError): + if not transitive: + if all_properties is None: + all_properties = [] + for requires in opt.impl_getrequires(): + for require in requires: + all_properties.append(require[1]) + if not set(value.proptype) - set(all_properties): + continue + properties = value.proptype + if same_action and action not in properties: # pragma: optional cover + if len(properties) == 1: + prop_msg = _('property') + else: + prop_msg = _('properties') + raise RequirementError(_('cannot access to option "{0}" because ' + 'required option "{1}" has {2} {3}' + '').format(opt.impl_get_display_name(), + option.impl_get_display_name(), + prop_msg, + display_list(properties))) + orig_value = value + # transitive action, force expected + value = expected[0] + inverse = False else: - if not inverse: - msg = _('the value of "{0}" is "{1}"') - else: - msg = _('the value of "{0}" is not "{1}"') - calc_properties.setdefault(action, []).append(msg.format(option.impl_get_display_name(), display_list(expected, 'or'))) + raise value else: - calc_properties.add(action) + orig_value = value + if (not inverse and value in expected or + inverse and value not in expected): + if operator != 'and': + if debug: + if isinstance(orig_value, PropertiesOptionError): + for msg in orig_value._settings.apply_requires(**orig_value._datas).values(): + calc_properties.setdefault(action, []).extend(msg) + else: + if not inverse: + msg = _('the value of "{0}" is "{1}"') + else: + msg = _('the value of "{0}" is not "{1}"') + calc_properties.setdefault(action, []).append( + msg.format(option.impl_get_display_name(), + display_list(expected, 'or'))) + else: + calc_properties.add(action) + breaked = True + break + elif operator == 'and': break + else: + if operator == 'and': + calc_properties.add(action) + continue + if breaked: + break return calc_properties def get_modified_properties(self):