add 'operator' to requirement
authorEmmanuel Garette <egarette@cadoles.com>
Sat, 20 May 2017 14:28:19 +0000 (16:28 +0200)
committerEmmanuel Garette <egarette@cadoles.com>
Sat, 20 May 2017 14:28:19 +0000 (16:28 +0200)
ChangeLog
test/test_requires.py
test/test_state.py
tiramisu/option/baseoption.py
tiramisu/option/optiondescription.py
tiramisu/setting.py

index 4142405..ade2d3c 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,6 @@
+Sat May 20 16:27:09 2017 +0200 Emmanuel Garette <egarette@cadoles.com>
+       * add 'operator' to requirement
+
 Wed May 17 22:11:55 2017 +0200 Emmanuel Garette <egarette@cadoles.com>
        * add 'remove' to Multi
 
index 2ea010c..bd499b2 100644 (file)
@@ -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', '')
index 75e23dd..dd56b63 100644 (file)
@@ -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':
index 55021b3..47d361b 100644 (file)
@@ -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)
 
index e7550d6..991274b 100644 (file)
@@ -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)):
index 5ed3a16..ce3d1e5 100644 (file)
@@ -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):