add 'operator' to requirement

This commit is contained in:
Emmanuel Garette 2017-05-20 16:28:19 +02:00
parent f522fd0d53
commit 9b78f46e9d
6 changed files with 319 additions and 128 deletions

View file

@ -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> Wed May 17 22:11:55 2017 +0200 Emmanuel Garette <egarette@cadoles.com>
* add 'remove' to Multi * add 'remove' to Multi

View file

@ -431,6 +431,122 @@ def test_requires_multi_disabled():
assert props == ['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(): def test_requires_multi_disabled_inverse():
a = BoolOption('activate_service', '') a = BoolOption('activate_service', '')
b = IntOption('num_service', '') b = IntOption('num_service', '')

View file

@ -78,8 +78,10 @@ def _diff_opt(opt1, opt2):
if val1 == val2 == []: if val1 == val2 == []:
pass pass
else: else:
assert val1[0][0][0]._name == val2[0][0][0]._name for idx, req in enumerate(val1[0][0][0]):
assert val1[0][0][1:] == val2[0][0][1:] 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': elif attr == '_opt':
assert val1._name == val2._name assert val1._name == val2._name
elif attr == '_consistencies': elif attr == '_consistencies':

View file

@ -258,20 +258,22 @@ class BaseOption(Base):
new_value = [] new_value = []
for requires in _requires: for requires in _requires:
new_requires = [] new_requires = []
new_req = []
for require in requires: for require in requires:
if load: for req in require[0]:
new_require = [descr.impl_get_opt_by_path(require[0])] if load:
else: new_req.append([(descr.impl_get_opt_by_path(req[0]), req[1])])
new_require = [descr.impl_get_path_by_opt(require[0])] else:
new_require.extend(require[1:]) new_req.append([(descr.impl_get_path_by_opt(req[0]), req[1])])
new_requires.append(tuple(new_require)) new_req.extend(require[1:])
new_requires.append(tuple(new_req))
new_value.append(tuple(new_requires)) new_value.append(tuple(new_requires))
if load: if load:
del(self._state_requires) del(self._state_requires)
if new_value != []: if new_value != []:
self._requires = new_value self._requires = tuple(new_value)
else: else:
self._state_requires = new_value self._state_requires = tuple(new_value)
# serialize # serialize
def _impl_getstate(self, descr): def _impl_getstate(self, descr):
@ -932,6 +934,94 @@ def validate_requires_arg(multi, requires, name):
know more about know more about
the description of the requires dictionary 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 = {} ret_requires = {}
config_action = set() config_action = set()
@ -942,7 +1032,7 @@ def validate_requires_arg(multi, requires, name):
raise ValueError(_("malformed requirements type for option:" raise ValueError(_("malformed requirements type for option:"
" {0}, must be a dict").format(name)) " {0}, must be a dict").format(name))
valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive', valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
'same_action') 'same_action', 'operator')
unknown_keys = frozenset(require.keys()) - frozenset(valid_keys) unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
if unknown_keys != frozenset(): if unknown_keys != frozenset():
raise ValueError(_('malformed requirements for option: {0}' raise ValueError(_('malformed requirements for option: {0}'
@ -951,62 +1041,25 @@ def validate_requires_arg(multi, requires, name):
unknown_keys, unknown_keys,
valid_keys)) valid_keys))
# prepare all attributes # 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: 'action' not in require:
raise ValueError(_("malformed requirements for option: {0}" raise ValueError(_("malformed requirements for option: {0}"
" require must have option, expected and" " require must have option, expected and"
" action keys").format(name)) " action keys").format(name))
option = require['option'] action = get_action(require)
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))
config_action.add(action) config_action.add(action)
if action not in ret_requires: if action not in ret_requires:
ret_requires[action] = {} ret_requires[action] = {}
if option not in ret_requires[action]: set_expected(require, ret_requires)
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)
# transform dict to tuple # transform dict to tuple
ret = [] ret = []
for opt_requires in ret_requires.values(): for requires in ret_requires.values():
ret_action = [] ret_action = []
for requires in opt_requires.values(): for require in requires.values():
for require in requires.values(): ret_action.append((tuple(require[0]), require[1],
ret_action.append((require[0], tuple(require[1]), require[2], require[2], require[3], require[4], require[5]))
require[3], require[4], require[5]))
ret.append(tuple(ret_action)) ret.append(tuple(ret_action))
return frozenset(config_action), tuple(ret) return frozenset(config_action), tuple(ret)

View file

@ -162,21 +162,21 @@ class OptionDescription(BaseOption, StorageOptionDescription):
# * option in require must be a master or a slave # * option in require must be a master or a slave
# * current option must be a slave (and only a slave) # * current option must be a slave (and only a slave)
# * option in require and current option must be in same master/slaves # * option in require and current option must be in same master/slaves
require_opt = require[0] for require_opt, values in require[0]:
if require_opt.impl_is_multi(): if require_opt.impl_is_multi():
if is_slave is None: if is_slave is None:
is_slave = option.impl_is_master_slaves('slave') is_slave = option.impl_is_master_slaves('slave')
if is_slave: if is_slave:
masterslaves = option.impl_get_master_slaves() masterslaves = option.impl_get_master_slaves()
if is_slave and require_opt.impl_is_master_slaves(): if is_slave and require_opt.impl_is_master_slaves():
if masterslaves != require_opt.impl_get_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} ' 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())) 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: if init:
session = config._impl_values._p_.getsession() session = config._impl_values._p_.getsession()
if len(cache_option) != len(set(cache_option)): if len(cache_option) != len(set(cache_option)):

View file

@ -644,68 +644,85 @@ class Settings(object):
all_properties = None all_properties = None
for requires in current_requires: for requires in current_requires:
for require in requires: for require in requires:
option, expected, action, inverse, \ exps, action, inverse, \
transitive, same_action = require transitive, same_action, operator = require
reqpath = option.impl_getpath(context) breaked = False
if reqpath == path or reqpath.startswith(path + '.'): # pragma: optional cover for exp in exps:
raise RequirementError(_("malformed requirements " option, expected = exp
"imbrication detected for option:" reqpath = option.impl_getpath(context)
" '{0}' with requirement on: " if reqpath == path or reqpath.startswith(path + '.'): # pragma: optional cover
"'{1}'").format(path, reqpath)) raise RequirementError(_("malformed requirements "
if option.impl_is_multi(): "imbrication detected for option:"
if index is None: " '{0}' with requirement on: "
continue "'{1}'").format(path, reqpath))
idx = index if option.impl_is_multi():
else: if index is None:
idx = None # multi is allowed only for slaves
value = context.getattr(reqpath, force_permissive=True, # so do not calculated requires if no index
_setting_properties=setting_properties, continue
index=idx, returns_raise=True) idx = index
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
else: else:
raise value idx = None
else: value = context.getattr(reqpath, force_permissive=True,
orig_value = value _setting_properties=setting_properties,
if (not inverse and value in expected or index=idx, returns_raise=True)
inverse and value not in expected): if isinstance(value, Exception):
if debug: if isinstance(value, PropertiesOptionError):
if isinstance(orig_value, PropertiesOptionError): if not transitive:
for act, msg in orig_value._settings.apply_requires(**orig_value._datas).items(): if all_properties is None:
calc_properties.setdefault(action, []).extend(msg) 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: else:
if not inverse: raise value
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: 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 break
else:
if operator == 'and':
calc_properties.add(action)
continue
if breaked:
break
return calc_properties return calc_properties
def get_modified_properties(self): def get_modified_properties(self):