diff --git a/tests/test_mandatory.py b/tests/test_mandatory.py index ae123b7..4422e49 100644 --- a/tests/test_mandatory.py +++ b/tests/test_mandatory.py @@ -565,7 +565,7 @@ def test_mandatory_warnings_validate_empty(): cfg = Config(descr) cfg.option('str').value.set('') cfg.property.read_only() - assert list(cfg.value.mandatory()) == ['str', 'str1', 'str3', 'unicode1'] + assert list(cfg.value.mandatory()) == ['str', 'str1', 'str3'] def test_mandatory_warnings_requires(): diff --git a/tests/test_option_consistency.py b/tests/test_option_consistency.py index aa78a78..8ed5107 100644 --- a/tests/test_option_consistency.py +++ b/tests/test_option_consistency.py @@ -180,10 +180,17 @@ def test_consistency_not_equal_many_opts(config_type): # cfg.option('a').value.set(1) raises(ValueError, "cfg.option('b').value.set(1)") + assert cfg.option('b').value.get() == None # cfg.option('b').value.set(2) raises(ValueError, "cfg.option('f').value.set(2)") + assert cfg.option('f').value.get() is None + assert cfg.option('a').value.get() == 1 + assert cfg.option('b').value.get() == 2 raises(ValueError, "cfg.option('f').value.set(1)") + assert cfg.option('f').value.get() is None + assert cfg.option('a').value.get() == 1 + assert cfg.option('b').value.get() == 2 # cfg.option('d').value.set(3) raises(ValueError, "cfg.option('f').value.set(3)") diff --git a/tests/test_option_validator.py b/tests/test_option_validator.py index bed2177..0dd0daf 100644 --- a/tests/test_option_validator.py +++ b/tests/test_option_validator.py @@ -1,4 +1,4 @@ -from .autopath import do_autopath +1rom .autopath import do_autopath do_autopath() from .config import config_type, get_config @@ -127,6 +127,14 @@ def test_validator(config_type): cfg.option('opt2').value.set('val') assert len(w) == 1 assert str(w[0].message) == msg + with warnings.catch_warnings(record=True) as w: + cfg.option('opt2').value.get() + assert len(w) == 1 + assert str(w[0].message) == msg + with warnings.catch_warnings(record=True) as w: + cfg.option('opt2').value.get() + assert len(w) == 1 + assert str(w[0].message) == msg def test_validator_params(config_type): diff --git a/tiramisu/api.py b/tiramisu/api.py index b5b6ac9..e235b41 100644 --- a/tiramisu/api.py +++ b/tiramisu/api.py @@ -19,7 +19,7 @@ from time import time from typing import List, Set, Any, Optional, Callable, Union, Dict -from .error import APIError, ConfigError, LeadershipError, PropertiesOptionError +from .error import APIError, ConfigError, LeadershipError, PropertiesOptionError, ValueErrorWarning from .i18n import _ from .setting import ConfigBag, OptionBag, owners, groups, Undefined, undefined, \ FORBIDDEN_SET_PROPERTIES, SPECIAL_PROPERTIES, EXPIRATION_TIME @@ -530,6 +530,17 @@ class _TiramisuOptionValueOption: else: return values.getdefaultvalue(self._option_bag) + def valid(self): + try: + with warnings.catch_warnings(record=True) as warns: + self.get() + for warn in warns: + if isinstance(warns.message, ValueErrorWarning): + return False + except ValueError: + return False + return True + class _TiramisuOptionValueLeader: def pop(self, index): @@ -739,52 +750,54 @@ class _TiramisuOptionDescription(_TiramisuOption): def _filter(self, opt, - subconfig): - if self._config_bag.properties: - name = opt.impl_getname() - path = subconfig._get_subpath(name) - option_bag = OptionBag() - option_bag.set_option(opt, - path, - None, - self._config_bag) - if opt.impl_is_optiondescription(): - self._subconfig.get_subconfig(option_bag) - else: - subconfig.getattr(name, - option_bag) + subconfig, + config_bag): + option_bag = OptionBag() + option_bag.set_option(opt, + opt.impl_getpath(), + None, + config_bag) + if opt.impl_is_optiondescription(): + config_bag.context.cfgimpl_get_settings().validate_properties(option_bag) + return subconfig.get_subconfig(option_bag) + subconfig.getattr(opt.impl_getname(), + option_bag) def list(self, type='option', group_type=None): - """List options in an optiondescription (only for optiondescription)""" + """List options (by default list only option)""" assert type in ('all', 'option', 'optiondescription'), _('unknown list type {}').format(type) assert group_type is None or isinstance(group_type, groups.GroupType), \ _("unknown group_type: {0}").format(group_type) + config_bag = self._config_bag + if config_bag.properties and 'warnings' in config_bag.properties: + config_bag = config_bag.copy() + config_bag.remove_warnings() option = self._get_option() - name = option.impl_getname() - path = self._subconfig._get_subpath(name) option_bag = OptionBag() option_bag.set_option(option, - path, + option.impl_getpath(), None, - self._config_bag) + config_bag) subconfig = self._subconfig.get_subconfig(option_bag) - for opt in option.get_children(self._config_bag): + for opt in option.get_children(config_bag): try: self._filter(opt, - subconfig) + subconfig, + config_bag) except PropertiesOptionError: continue if opt.impl_is_optiondescription(): - if type == 'option' or (type == 'optiondescription' and group_type and \ - opt.impl_get_group_type() != group_type): + if type == 'option' or (type == 'optiondescription' and \ + group_type and opt.impl_get_group_type() != group_type): continue elif type == 'optiondescription': continue name = opt.impl_getname() + path = opt.impl_getpath() yield TiramisuOption(name, - subconfig._get_subpath(name), + path, None, subconfig, self._config_bag) @@ -792,9 +805,10 @@ class _TiramisuOptionDescription(_TiramisuOption): def dict(self, clearable: str="all", remotable: str="minimum", - form: List=[]) -> Dict: + form: List=[], + force: bool=False) -> Dict: """convert config and option to tiramisu format""" - if self._tiramisu_dict is None: + if force or self._tiramisu_dict is None: option = self._get_option() name = option.impl_getname() root = self._subconfig._get_subpath(name) @@ -822,9 +836,13 @@ class TiramisuOption(CommonTiramisuOption): subconfig: Union[None, KernelConfig, SubConfig]=None, config_bag: Optional[ConfigBag]=None) -> None: if subconfig: + # not for groupconfig + if '.' in name: + subconfig, name = config_bag.context.cfgimpl_get_home_by_path(path, + config_bag) option = subconfig.cfgimpl_get_description().get_child(name, - config_bag, - subconfig.cfgimpl_get_path()) + config_bag, + subconfig.cfgimpl_get_path()) if option.impl_is_optiondescription(): return _TiramisuOptionDescription(name=name, path=path, @@ -1219,13 +1237,12 @@ class TiramisuContextOption(TiramisuContext): continue if opt.impl_is_optiondescription(): if recursive: - for toption in self._walk(opt, - recursive, - type_, - group_type, - config_bag, - subsubconfig): - yield toption + yield from self._walk(opt, + recursive, + type_, + group_type, + config_bag, + subsubconfig) if type_ == 'option' or (type_ == 'optiondescription' and \ group_type and opt.impl_get_group_type() != group_type): continue @@ -1252,20 +1269,20 @@ class TiramisuContextOption(TiramisuContext): config_bag = config_bag.copy() config_bag.remove_warnings() option = config_bag.context.cfgimpl_get_description() - for toption in self._walk(option, - recursive, - type, - group_type, - config_bag, - config_bag.context): - yield toption + yield from self._walk(option, + recursive, + type, + group_type, + config_bag, + config_bag.context) def dict(self, clearable="all", remotable="minimum", - form=[]): + form=[], + force=False): """convert config and option to tiramisu format""" - if self._tiramisu_dict is None: + if force or self._tiramisu_dict is None: self._tiramisu_dict = TiramisuDict(Config(self._config_bag.context), root=None, clearable=clearable, @@ -1499,14 +1516,16 @@ class Config(TiramisuAPI): descr: OptionDescription, session_id: str=None, persistent: bool=False, - storage=None) -> None: + storage=None, + display_name=None) -> None: if isinstance(descr, KernelConfig): config = descr else: config = KernelConfig(descr, session_id=session_id, persistent=persistent, - storage=storage) + storage=storage, + display_name=display_name) super().__init__(config) @@ -1516,7 +1535,8 @@ class MetaConfig(TiramisuAPI): children, session_id: Union[str, None]=None, persistent: bool=False, - optiondescription: Optional[OptionDescription]=None) -> None: + optiondescription: Optional[OptionDescription]=None, + display_name=None) -> None: if isinstance(children, KernelMetaConfig): config = children else: @@ -1530,7 +1550,8 @@ class MetaConfig(TiramisuAPI): config = KernelMetaConfig(_children, session_id=session_id, persistent=persistent, - optiondescription=optiondescription) + optiondescription=optiondescription, + display_name=display_name) super().__init__(config) @@ -1540,7 +1561,8 @@ class MixConfig(TiramisuAPI): optiondescription: OptionDescription, children: List[Config], session_id: Optional[str]=None, - persistent: bool=False) -> None: + persistent: bool=False, + display_name: Callable=None) -> None: if isinstance(children, KernelMixConfig): config = children else: @@ -1554,7 +1576,8 @@ class MixConfig(TiramisuAPI): config = KernelMixConfig(optiondescription, _children, session_id=session_id, - persistent=persistent) + persistent=persistent, + display_name=display_name) super().__init__(config) diff --git a/tiramisu/config.py b/tiramisu/config.py index 3b4cfdf..626d91b 100644 --- a/tiramisu/config.py +++ b/tiramisu/config.py @@ -615,7 +615,7 @@ class _CommonConfig(SubConfig): def _impl_build_all_caches(self): descr = self.cfgimpl_get_description() if not descr.impl_already_build_caches(): - descr._build_cache() + descr._build_cache(display_name=self._display_name) config_bag = ConfigBag(context=self) descr.impl_build_force_store_values(config_bag) @@ -652,7 +652,8 @@ class _CommonConfig(SubConfig): fake_config = KernelConfig(self._impl_descr, persistent=False, force_values=get_default_values_storages(), - force_settings=self.cfgimpl_get_settings()) + force_settings=self.cfgimpl_get_settings(), + display_name=self._display_name) fake_config.cfgimpl_get_values()._p_.importation(self.cfgimpl_get_values()._p_.exportation()) return fake_config @@ -673,7 +674,8 @@ class _CommonConfig(SubConfig): force_values=force_values, force_settings=force_settings, persistent=persistent, - storage=storage) + storage=storage, + display_name=self._display_name) else: if session_id is None and metaconfig_prefix is not None: session_id = metaconfig_prefix + self.impl_getname() @@ -682,7 +684,8 @@ class _CommonConfig(SubConfig): optiondescription=self._impl_descr, session_id=session_id, persistent=persistent, - storage=storage) + storage=storage, + display_name=self._display_name) duplicated_config.cfgimpl_get_values()._p_.importation(self.cfgimpl_get_values()._p_.exportation()) properties = self.cfgimpl_get_settings()._p_.exportation() duplicated_config.cfgimpl_get_settings()._p_.importation(properties) @@ -714,7 +717,8 @@ class _CommonConfig(SubConfig): class KernelConfig(_CommonConfig): "main configuration management entry" __slots__ = ('__weakref__', - '_impl_name') + '_impl_name', + '_display_name') impl_type = 'config' def __init__(self, @@ -723,6 +727,7 @@ class KernelConfig(_CommonConfig): persistent=False, force_values=None, force_settings=None, + display_name=None, _duplicate=False, storage=None): """ Configuration option management class @@ -738,6 +743,7 @@ class KernelConfig(_CommonConfig): :type persistent: `boolean` """ self._impl_meta = None + self._display_name = display_name if isinstance(descr, Leadership): raise ConfigError(_('cannot set leadership object has root optiondescription')) if isinstance(descr, DynOptionDescription): @@ -976,7 +982,7 @@ class KernelGroupConfig(_CommonConfig): class KernelMixConfig(KernelGroupConfig): - __slots__ = tuple() + __slots__ = ('_display_name',) impl_type = 'mix' def __init__(self, @@ -985,8 +991,10 @@ class KernelMixConfig(KernelGroupConfig): session_id=None, persistent=False, storage=None, + display_name=None, _duplicate=False): # FIXME _duplicate + self._display_name = display_name for child in children: if not isinstance(child, _CommonConfig): try: @@ -1210,8 +1218,10 @@ class KernelMetaConfig(KernelMixConfig): persistent=False, optiondescription=None, storage=None, + display_name=None, _duplicate=False): descr = None + self._display_name = display_name if optiondescription is not None: if not _duplicate: new_children = [] @@ -1221,7 +1231,8 @@ class KernelMetaConfig(KernelMixConfig): 'not {}').format(child_session_id) new_children.append(KernelConfig(optiondescription, persistent=persistent, - session_id=child_session_id)) + session_id=child_session_id, + display_name=self._display_name)) children = new_children descr = optiondescription for child in children: @@ -1259,17 +1270,20 @@ class KernelMetaConfig(KernelMixConfig): if type_ == 'config': config = KernelConfig(self._impl_descr, session_id=session_id, - persistent=persistent) + persistent=persistent, + display_name=self._display_name) elif type_ == 'metaconfig': config = KernelMetaConfig([], optiondescription=self._impl_descr, session_id=session_id, - persistent=persistent) + persistent=persistent, + display_name=self._display_name) elif type_ == 'mixconfig': config = KernelMixConfig(children=[], optiondescription=self._impl_descr, session_id=session_id, - persistent=persistent) + persistent=persistent, + display_name=self._display_name) # Copy context properties/permissives if new: config.cfgimpl_get_settings().set_context_properties(self.cfgimpl_get_settings().get_context_properties(), config) diff --git a/tiramisu/option/baseoption.py b/tiramisu/option/baseoption.py index 50e02b2..ba4b47c 100644 --- a/tiramisu/option/baseoption.py +++ b/tiramisu/option/baseoption.py @@ -373,7 +373,7 @@ class BaseOption(Base): in options that have to be set only once, it is of course done in the __setattr__ method """ - __slots__ = tuple() + __slots__ = ('_display_name_function',) def __getstate__(self): raise NotImplementedError() @@ -407,8 +407,8 @@ class BaseOption(Base): "to know if a callback has been defined or not" return self.impl_get_callback()[0] is not None - def impl_get_display_name(self, - dyn_name: Base=None) -> str: + def _impl_get_display_name(self, + dyn_name: Base=None) -> str: name = self.impl_get_information('doc') if name is None or name == '': if dyn_name is not None: @@ -417,6 +417,12 @@ class BaseOption(Base): name = self.impl_getname() return name + def impl_get_display_name(self, + dyn_name: Base=None) -> str: + if hasattr(self, '_display_name_function'): + return self._display_name_function(self, dyn_name) + return self._impl_get_display_name(dyn_name) + def reset_cache(self, path: str, values: Values, diff --git a/tiramisu/option/optiondescription.py b/tiramisu/option/optiondescription.py index e51d9d0..f746cb2 100644 --- a/tiramisu/option/optiondescription.py +++ b/tiramisu/option/optiondescription.py @@ -43,7 +43,8 @@ class CacheOptionDescription(BaseOption): _consistencies_id=0, currpath: List[str]=None, cache_option=None, - force_store_values=None) -> None: + force_store_values=None, + display_name=None) -> None: """validate options and set option has readonly option """ # _consistencies is None only when we start to build cache @@ -73,7 +74,8 @@ class CacheOptionDescription(BaseOption): _consistencies_id, sub_currpath, cache_option, - force_store_values) + force_store_values, + display_name) else: is_multi = option.impl_is_multi() if not option.impl_is_symlinkoption(): @@ -155,6 +157,8 @@ class CacheOptionDescription(BaseOption): require_opt.impl_getname(), option.impl_getname())) if option.impl_is_readonly(): raise ConflictError(_('duplicate option: {0}').format(option)) + if not self.impl_is_readonly() and display_name: + option._display_name_function = display_name option._path = subpath option._set_readonly() if init: diff --git a/tiramisu/todict.py b/tiramisu/todict.py index ca8d02a..c2c0a72 100644 --- a/tiramisu/todict.py +++ b/tiramisu/todict.py @@ -33,6 +33,7 @@ INPUTS = ['string', # return always warning (even if same warning is already returned) warnings.simplefilter("always", ValueWarning) +warnings.simplefilter("always", ValueErrorWarning) class Callbacks(object): @@ -621,27 +622,33 @@ class TiramisuDict: props = set(childapi.property.get()) obj = self.gen_properties(props, isfollower, - childapi.option.ismulti()) + childapi.option.ismulti(), + index) self.calc_raises_properties(obj, childapi) return obj def gen_properties(self, properties, - isfollower=False, - ismulti=False): + isfollower, + ismulti, + index): obj = {} if not isfollower and ismulti: if 'empty' in properties: - obj['required'] = True + if index is None: + obj['required'] = True properties.remove('empty') if 'mandatory' in properties: - obj['needs_len'] = True + if index is None: + obj['needs_len'] = True properties.remove('mandatory') elif 'mandatory' in properties: - obj['required'] = True + if index is None: + obj['required'] = True properties.remove('mandatory') if 'frozen' in properties: - obj['readOnly'] = True + if index is None: + obj['readOnly'] = True properties.remove('frozen') if 'hidden' in properties: properties.remove('hidden') @@ -735,7 +742,7 @@ class TiramisuDict: value = childapi.value.get() if value is not None and value != []: obj['value'] = value - obj['owner'] = childapi.owner.get() + obj['owner'] = childapi.owner.get() def _get_value_with_exception(self, obj, @@ -744,22 +751,24 @@ class TiramisuDict: for value in values: if isinstance(value, ValueError): obj.setdefault('error', []) - obj['error'].append(str(value)) - obj['invalid'] = True + msg = str(value) + if msg not in obj.get('error', []): + obj['error'].append(msg) + obj['invalid'] = True elif isinstance(value.message, ValueErrorWarning): value.message.prefix = '' - if childapi.option.isfollower(): - obj.setdefault('invalid', []) - obj['invalid'].append({'error': str(value.message), - 'index': value.message.index}) - else: - obj.setdefault('error', []) - obj['error'].append(str(value.message)) + obj.setdefault('error', []) + msg = str(value.message) + if msg not in obj.get('error', []): + obj['error'].append(msg) obj['invalid'] = True else: + value.message.prefix = '' obj.setdefault('warnings', []) - obj['warnings'].append(str(value.message)) - obj['hasWarnings'] = True + msg = str(value.message) + if msg not in obj.get('error', []): + obj['warnings'].append(msg) + obj['hasWarnings'] = True def gen_global(self): ret = {} diff --git a/tiramisu/value.py b/tiramisu/value.py index 8cbf64e..eda7a04 100644 --- a/tiramisu/value.py +++ b/tiramisu/value.py @@ -76,7 +76,8 @@ class Values(object): option_bag, check_error=True) # store value in cache - validator = 'validator' in option_bag.config_bag.properties + properties = option_bag.config_bag.properties + validator = 'validator' in properties and 'demoting_error_warning' not in properties if not option_bag.fromconsistency and (not is_cached or validator): self._p_.setcache(option_bag.path, option_bag.index, @@ -570,13 +571,12 @@ class Values(object): except PropertiesOptionError as err: pass else: - for path in self._mandatory_warnings(context, - config_bag, - option, - currpath + [name], - subsubconfig, - od_config_bag): - yield path + yield from self._mandatory_warnings(context, + config_bag, + option, + currpath + [name], + subsubconfig, + od_config_bag) elif not option.impl_is_symlinkoption(): # don't verifying symlink try: @@ -602,11 +602,8 @@ class Values(object): except PropertiesOptionError as err: if err.proptype == ['mandatory']: yield path - except RequirementError: + except (RequirementError, ConfigError): pass - except ConfigError as err: - #assume that uncalculated value is an empty value - yield path def mandatory_warnings(self, config_bag):