fix: better permissive support

This commit is contained in:
egarette@silique.fr 2024-10-22 11:04:57 +02:00
parent 7761758096
commit 7ae1b48f4a
3 changed files with 179 additions and 123 deletions

View file

@ -21,7 +21,7 @@ from functools import wraps
from copy import deepcopy
from .error import ConfigError, LeadershipError, ValueErrorWarning
from .error import ConfigError, LeadershipError, ValueErrorWarning, PropertiesOptionError
from .i18n import _
from .setting import ConfigBag, owners, groups, undefined, \
FORBIDDEN_SET_PROPERTIES, SPECIAL_PROPERTIES, \
@ -92,12 +92,13 @@ class CommonTiramisu(TiramisuHelp):
_allow_dynoption = False
def _set_subconfig(self) -> None:
self._subconfig = self._config_bag.context.get_sub_config(self._config_bag,
self._path,
self._index,
validate_properties=False,
allow_dynoption=self._allow_dynoption,
)
if not self._subconfig:
self._subconfig = self._config_bag.context.get_sub_config(self._config_bag,
self._path,
self._index,
validate_properties=False,
allow_dynoption=self._allow_dynoption,
)
def option_type(typ):
@ -119,6 +120,7 @@ def option_type(typ):
)]
kwargs['is_group'] = True
return func(self, options_bag, *args[1:], **kwargs)
self._set_subconfig()
option = self._subconfig.option
error_type = None
if 'dynamic' in types:
@ -194,6 +196,7 @@ class CommonTiramisuOption(CommonTiramisu):
self._path = path
self._index = index
self._config_bag = config_bag
self._subconfig = None
self._set_subconfig()
@ -292,6 +295,11 @@ class _TiramisuOptionOptionDescription:
return 'optiondescription'
return option.get_type()
@option_type(['option', 'symlink', 'with_or_without_index'])
def extra(self, extra):
"""Get de option extra"""
return self._subconfig.option.impl_get_extra(extra)
@option_type(['option', 'optiondescription', 'symlink', 'with_or_without_index'])
def isdynamic(self,
*,
@ -342,19 +350,19 @@ class _TiramisuOptionOptionDescription:
return ret
@option_type(['dynamic', 'with_or_without_index'])
def suffixes(self,
only_self: bool=False,
uncalculated: bool=False,
):
"""Get suffixes for dynamic option"""
def identifiers(self,
only_self: bool=False,
uncalculated: bool=False,
):
"""Get identifiers for dynamic option"""
if not only_self:
return self._subconfig.suffixes
return self._subconfig.identifiers
if not self._subconfig.option.impl_is_optiondescription() or \
not self._subconfig.option.impl_is_dynoptiondescription():
raise ConfigError(_(f'the option {self._subconfig.path} is not a dynamic option, cannot get suffixes with only_self parameter to True'))
return self._subconfig.option.get_suffixes(self._subconfig.parent,
uncalculated=uncalculated,
)
raise ConfigError(_(f'the option {self._subconfig.path} is not a dynamic option, cannot get identifiers with only_self parameter to True'))
return self._subconfig.option.get_identifiers(self._subconfig.parent,
uncalculated=uncalculated,
)
class _TiramisuOptionOption(_TiramisuOptionOptionDescription):
@ -571,6 +579,7 @@ class TiramisuOptionPermissive(CommonTiramisuOption):
class TiramisuOptionInformation(CommonTiramisuOption):
"""Manage option's informations"""
_validate_properties = False
_allow_dynoption = True
@option_type(['option', 'optiondescription', 'with_or_without_index', 'symlink'])
def get(self,
@ -670,8 +679,8 @@ class TiramisuOptionValue(CommonTiramisuOption, _TiramisuODGet):
option = self._subconfig.option
if not isinstance(value, Calculation) and option.impl_is_leader() and \
len(value) < self._subconfig.parent.get_length_leadership():
raise LeadershipError(_('cannot reduce length of the leader "{}"'
'').format(option.impl_get_display_name(self._subconfig)))
raise LeadershipError(_('cannot reduce length of the leader {}'
'').format(option.impl_get_display_name(self._subconfig, with_quote=True)))
values = self._config_bag.context.get_values()
return values.set_value(self._subconfig,
value
@ -753,6 +762,32 @@ class TiramisuOptionValue(CommonTiramisuOption, _TiramisuODGet):
"""Length for a leadership"""
return self._subconfig.parent.get_length_leadership()
def mandatory(self):
"""Return path of options with mandatory property without any value"""
subconfig = self._subconfig
if subconfig.option.impl_is_optiondescription():
ori_config_bag = self._subconfig.config_bag
config_bag = ori_config_bag.copy()
config_bag.properties -= {'mandatory', 'empty', 'warnings'}
config_bag.set_permissive()
self._subconfig.config_bag = config_bag
options = []
for subconfig in self._config_bag.context.walk(self._subconfig,
only_mandatory=True,
):
options.append(TiramisuOption(subconfig.path,
subconfig.index,
ori_config_bag,
subconfig=subconfig,
))
self._subconfig.config_bag = ori_config_bag
return options
try:
self._config_bag.context.walk_valid_value(self._subconfig, only_mandatory=True)
except PropertiesOptionError as err:
return err.proptype == ['mandatory'] or err.proptype == ['empty']
return False
def _registers(_registers: Dict[str, type],
prefix: str,
@ -808,10 +843,7 @@ class TiramisuOption(CommonTiramisu,
self._index = index
self._config_bag = config_bag
self._allow_dynoption = allow_dynoption
if subconfig is None:
self._set_subconfig()
else:
self._subconfig = subconfig
self._subconfig = subconfig
if not self._registers:
_registers(self._registers, 'TiramisuOption')
@ -831,6 +863,7 @@ class TiramisuOption(CommonTiramisu,
raise ConfigError(_(f'please specify a valid sub function ({self.__class__.__name__}.{subfunc}) for {self._path}'))
#
def __iter__(self):
self._set_subconfig()
for sub_subconfig in self._subconfig.get_children(True):
yield TiramisuOption(sub_subconfig.path,
sub_subconfig.index,
@ -841,6 +874,7 @@ class TiramisuOption(CommonTiramisu,
@option_type('optiondescription')
def group_type(self):
"""Get type for an optiondescription (only for optiondescription)"""
self._set_subconfig()
return self._subconfig.option.impl_get_group_type()
@option_type('optiondescription')
@ -850,6 +884,7 @@ class TiramisuOption(CommonTiramisu,
uncalculated: bool=False,
):
"""List options inside an option description (by default list only option)"""
self._set_subconfig()
return self._list(self._subconfig,
validate_properties,
uncalculated=uncalculated,
@ -1335,12 +1370,15 @@ class TiramisuContextOption(TiramisuConfig, _TiramisuOptionWalk):
return 'optiondescription'
def list(self,
*,
validate_properties: bool=True,
uncalculated: bool=False,
):
"""List options (by default list only option)"""
root = self._config_bag.context.get_root(self._config_bag)
return self._list(root,
validate_properties,
uncalculated=uncalculated,
)
def _load_dict(self,

View file

@ -115,12 +115,12 @@ class ParamOption(Param):
class ParamDynOption(ParamOption):
__slots__ = ('suffixes',
__slots__ = ('identifiers',
'optional',
)
def __init__(self,
option: 'Option',
suffixes: list[str],
identifiers: list[str],
notraisepropertyerror: bool=False,
raisepropertyerror: bool=False,
optional: bool=False,
@ -129,7 +129,11 @@ class ParamDynOption(ParamOption):
notraisepropertyerror,
raisepropertyerror,
)
self.suffixes = suffixes
if not isinstance(identifiers, list):
raise Exception(f'identifiers in ParamDynOption must be a list, not {identifiers}')
if not isinstance(optional, bool):
raise Exception(f'optional in ParamDynOption must be a boolean, not {optional}')
self.identifiers = identifiers
self.optional = optional
@ -212,12 +216,12 @@ class ParamIndex(Param):
__slots__ = tuple()
class ParamSuffix(Param):
__slots__ = ('suffix_index',)
class ParamIdentifier(Param):
__slots__ = ('identifier_index',)
def __init__(self,
suffix_index: int=-1,
identifier_index: int=-1,
) -> None:
self.suffix_index = suffix_index
self.identifier_index = identifier_index
class Calculation:
@ -345,7 +349,7 @@ def manager_callback(callback: Callable,
properties=undefined,
):
option = subconfig.option
if option.impl_is_follower() and apply_index is False:
if option.impl_is_follower() and (subconfig.index is None or apply_index is False):
value = []
for idx in range(subconfig.parent.get_length_leadership()):
subconfig = get_option_bag(config_bag,
@ -374,10 +378,11 @@ def manager_callback(callback: Callable,
# raise PropertiesOptionError (which is catched) because must not add value None in carry_out_calculation
if isinstance(param, ParamSelfOption) or param.notraisepropertyerror or param.raisepropertyerror:
raise err from err
raise ConfigError(_('unable to carry out a calculation for "{}", {}').format(display_name, err)) from err
display_name = subconfig.option.impl_get_display_name(subconfig, with_quote=True)
raise ConfigError(_('unable to carry out a calculation for {}, {}').format(display_name, err)) from err
except ValueError as err:
display_name = subconfig.option.impl_get_display_name(subconfig)
raise ValueError(_('the option "{0}" is used in a calculation but is invalid ({1})').format(display_name, err)) from err
display_name = subconfig.option.impl_get_display_name(subconfig, with_quote=True)
raise ValueError(_('the option {0} is used in a calculation but is invalid ({1})').format(display_name, err)) from err
except AttributeError as err:
if isinstance(param, ParamDynOption) and param.optional:
# cannot acces, simulate a propertyerror
@ -385,8 +390,8 @@ def manager_callback(callback: Callable,
['configerror'],
config_bag.context.get_settings(),
)
display_name = subconfig.option.impl_get_display_name(subconfig)
raise ConfigError(_(f'unable to get value for calculating "{display_name}", {err}')) from err
display_name = subconfig.option.impl_get_display_name(subconfig, with_quote=True)
raise ConfigError(_(f'unable to get value for calculating {display_name}, {err}')) from err
return value
def get_option_bag(config_bag,
@ -398,16 +403,13 @@ def manager_callback(callback: Callable,
properties=undefined,
):
# don't validate if option is option that we tried to validate
config_bag = config_bag.copy()
if for_settings:
config_bag.properties = config_bag.properties - {'warnings'}
config_bag.set_permissive()
if not for_settings:
config_bag.properties -= {'warnings'}
if self_calc:
config_bag.unrestraint()
config_bag.remove_validation()
# root = config_bag.context.get_root(config_bag)
try:
subsubconfig = config_bag.context.get_sub_config(config_bag,
opt.impl_getpath(),
@ -419,10 +421,11 @@ def manager_callback(callback: Callable,
# raise PropertiesOptionError (which is catched) because must not add value None in carry_out_calculation
if param.notraisepropertyerror or param.raisepropertyerror:
raise err from err
display_name = option.impl_get_display_name(subconfig)
raise ConfigError(_('unable to carry out a calculation for "{}", {}').format(display_name, err)) from err
display_name = option.impl_get_display_name(subconfig, with_quote=True)
raise ConfigError(_('unable to carry out a calculation for {}, {}').format(display_name, err)) from err
except ValueError as err:
raise ValueError(_('the option "{0}" is used in a calculation but is invalid ({1})').format(option.impl_get_display_name(subconfig), err)) from err
display_name = option.impl_get_display_name(subconfig, with_quote=True)
raise ValueError(_('the option {0} is used in a calculation but is invalid ({1})').format(display_name, err)) from err
except AttributeError as err:
if isinstance(param, ParamDynOption) and param.optional:
# cannot acces, simulate a propertyerror
@ -430,8 +433,8 @@ def manager_callback(callback: Callable,
['configerror'],
config_bag.context.get_settings(),
)
display_name = option.impl_get_display_name(subconfig)
raise ConfigError(_(f'unable to get value for calculating "{display_name}", {err}')) from err
display_name = option.impl_get_display_name(subconfig, with_quote=True)
raise ConfigError(_(f'unable to get value for calculating {display_name}, {err}')) from err
return subsubconfig
if isinstance(param, ParamValue):
@ -447,7 +450,9 @@ def manager_callback(callback: Callable,
true_path=subconfig.path,
)
if isinstance(isubconfig, list):
raise ConfigError(f'cannot find information for "{option.impl_get_display_name(subconfig)}", "{search_option.impl_get_display_name(None)}" is a dynamic option')
display_name = option.impl_get_display_name(subconfig, with_quote=True)
search_name = search_option.impl_get_display_name(None, with_quote=True)
raise ConfigError(f'cannot find information for {display_name}, {search_name} is a dynamic option')
else:
isubconfig = get_option_bag(config_bag,
param.option,
@ -464,17 +469,17 @@ def manager_callback(callback: Callable,
param.default_value,
)
except ValueError as err:
display_name = option.impl_get_display_name(subconfig)
raise ConfigError(_(f'unable to get value for calculating "{display_name}", {err}')) from err
display_name = option.impl_get_display_name(subconfig, with_quote=True)
raise ConfigError(_(f'unable to get value for calculating {display_name}, {err}')) from err
if isinstance(param, ParamIndex):
return index
if isinstance(param, ParamSuffix):
if isinstance(param, ParamIdentifier):
if not option.issubdyn() and (not option.impl_is_optiondescription() or not option.impl_is_dynoptiondescription()):
display_name = subconfig.option.impl_get_display_name(subconfig)
raise ConfigError(_(f'option "{display_name}" is not a dynoptiondescription or in a dynoptiondescription'))
return subconfig.suffixes[param.suffix_index]
display_name = subconfig.option.impl_get_display_name(subconfig, with_quote=True)
raise ConfigError(_(f'option {display_name} is not a dynoptiondescription or in a dynoptiondescription'))
return subconfig.identifiers[param.identifier_index]
if isinstance(param, ParamSelfOption):
value = calc_self(param,
@ -490,7 +495,6 @@ def manager_callback(callback: Callable,
if isinstance(param, ParamOption):
callbk_option = param.option
config_bag = subconfig.config_bag
if index is not None and callbk_option.impl_get_leadership() and \
callbk_option.impl_get_leadership().in_same_leadership(option):
if not callbk_option.impl_is_follower():
@ -506,8 +510,7 @@ def manager_callback(callback: Callable,
with_index = False
if callbk_option.issubdyn():
if isinstance(param, ParamDynOption):
#callbk_option = callbk_option.to_sub_dyoption(param.suffixes)
suffixes = param.suffixes.copy()
identifiers = param.identifiers.copy()
paths = callbk_option.impl_getpath().split('.')
parents = [config_bag.context.get_root(config_bag)]
subconfigs_is_a_list = False
@ -520,9 +523,18 @@ def manager_callback(callback: Callable,
allow_dynoption=True,
)
if doption.impl_is_dynoptiondescription():
if suffixes:
suffix = suffixes.pop(0)
name = doption.impl_getname(suffix)
if not identifiers:
identifier = None
else:
identifier = identifiers.pop(0)
if not identifier:
subconfigs_is_a_list = True
new_parents.extend(parent.dyn_to_subconfig(doption,
True,
)
)
else:
name = doption.impl_getname(identifier)
try:
doption = parent.option.get_child(name,
config_bag,
@ -534,14 +546,8 @@ def manager_callback(callback: Callable,
None,
True,
name=name,
suffix=suffix,
identifier=identifier,
))
else:
subconfigs_is_a_list = True
new_parents.extend(parent.dyn_to_subconfig(doption,
True,
)
)
else:
new_parents.append(parent.get_child(doption,
None,
@ -625,11 +631,13 @@ def carry_out_calculation(subconfig: 'SubConfig',
Values could have multiple values only when key is ''."""
option = subconfig.option
if not option.impl_is_optiondescription() and option.impl_is_follower() and index is None:
raise ConfigError(f'the follower "{option.impl_get_display_name(subconfig)}" must have index in carry_out_calculation!')
raise ConfigError(f'the follower {option.impl_get_display_name(subconfig, with_quote=True)} must have index in carry_out_calculation!')
def fake_items(iterator):
return ((None, i) for i in iterator)
args = []
kwargs = {}
config_bag = config_bag.copy()
config_bag.set_permissive()
if callback_params:
for key, param in chain(fake_items(callback_params.args), callback_params.kwargs.items()):
try:
@ -667,18 +675,18 @@ def carry_out_calculation(subconfig: 'SubConfig',
if args or kwargs:
raise LeadershipError(_('the "{}" function with positional arguments "{}" '
'and keyword arguments "{}" must not return '
'a list ("{}") for the follower option "{}"'
'a list ("{}") for the follower option {}'
'').format(callback.__name__,
args,
kwargs,
ret,
option.impl_get_display_name(subconfig)))
option.impl_get_display_name(subconfig, with_quote=True)))
else:
raise LeadershipError(_('the "{}" function must not return a list ("{}") '
'for the follower option "{}"'
'for the follower option {}'
'').format(callback.__name__,
ret,
option.impl_get_display_name(subconfig)))
option.impl_get_display_name(subconfig, with_quote=True)))
return ret
@ -702,22 +710,22 @@ def calculate(subconfig,
if allow_value_error:
if force_value_warning:
raise ValueWarning(str(err))
raise err
raise err from err
error = err
except ConfigError as err:
raise err from err
except Exception as err:
import traceback
traceback.print_exc()
error = err
if args or kwargs:
msg = _('unexpected error "{0}" in function "{1}" with arguments "{3}" and "{4}" '
'for option "{2}"').format(str(error),
'for option {2}').format(str(error),
callback.__name__,
subconfig.option.impl_get_display_name(subconfig),
subconfig.option.impl_get_display_name(subconfig, with_quote=True),
args,
kwargs)
else:
msg = _('unexpected error "{0}" in function "{1}" for option "{2}"'
msg = _('unexpected error "{0}" in function "{1}" for option {2}'
'').format(str(error),
callback.__name__,
subconfig.option.impl_get_display_name(subconfig))
subconfig.option.impl_get_display_name(subconfig, with_quote=True))
raise ConfigError(msg) from error

View file

@ -182,10 +182,11 @@ class SubConfig:
'index',
'path',
'true_path',
'properties',
'_properties',
'apply_requires',
'transitive_properties',
'is_dynamic',
'suffixes',
'identifiers',
'_length',
)
def __init__(self,
@ -194,14 +195,14 @@ class SubConfig:
path: str,
config_bag: ConfigBag,
parent: Optional['SubConfig'],
suffixes: Optional[list[str]],
identifiers: Optional[list[str]],
*,
true_path: Optional[str]=None,
properties: Union[list[str], undefined]=undefined,
validate_properties: bool=True,
) -> None:
self.index = index
self.suffixes = suffixes
self.identifiers = identifiers
self.option = option
self.config_bag = config_bag
self.parent = parent
@ -210,38 +211,47 @@ class SubConfig:
if true_path is None:
true_path = path
is_follower = not option.impl_is_optiondescription() and option.impl_is_follower()
apply_requires = not is_follower or index is not None
self.apply_requires = not is_follower or index is not None
self.true_path = true_path
settings = config_bag.context.get_settings()
if parent and parent.is_dynamic or self.option.impl_is_dynoptiondescription():
self.is_dynamic = True
else:
self.is_dynamic = False
if properties is undefined:
if path is None:
self.properties = frozenset()
else:
self.properties = frozenset()
if validate_properties:
self.properties = settings.getproperties(self,
apply_requires=False,
)
self.config_bag.context.get_settings().validate_properties(self)
self.properties = settings.getproperties(self,
apply_requires=apply_requires,
)
else:
self.properties = properties
self._properties = properties
if validate_properties:
if self.path and self._properties is undefined:
settings = config_bag.context.get_settings()
self._properties = settings.getproperties(self,
apply_requires=False,
)
self.config_bag.context.get_settings().validate_properties(self)
self._properties = undefined
self.config_bag.context.get_settings().validate_properties(self)
if apply_requires and self.option.impl_is_optiondescription():
if self.apply_requires and self.option.impl_is_optiondescription():
if self.path and self.properties is not None:
settings = config_bag.context.get_settings()
self.transitive_properties = settings.calc_transitive_properties(self,
self.properties,
)
else:
self.transitive_properties = frozenset()
@property
def properties(self):
if self._properties is undefined:
if self.path is None:
self._properties = frozenset()
else:
settings = self.config_bag.context.get_settings()
self._properties = frozenset()
self._properties = settings.getproperties(self,
apply_requires=self.apply_requires,
)
return self._properties
@properties.setter
def properties(self, properties):
self._properties = properties
def __repr__(self):
return f'<SubConfig path={self.path}, index={self.index}>'
@ -253,9 +263,9 @@ class SubConfig:
true_path: Optional[str]=None,
) -> List['SubConfig']:
config_bag = self.config_bag
for suffix in child.get_suffixes(self):
for identifier in child.get_identifiers(self):
try:
name = child.impl_getname(suffix)
name = child.impl_getname(identifier)
if not validate_properties:
properties = None
else:
@ -263,7 +273,7 @@ class SubConfig:
yield self.get_child(child,
None,
validate_properties,
suffix=suffix,
identifier=identifier,
name=name,
properties=properties,
true_path=true_path,
@ -323,7 +333,7 @@ class SubConfig:
*,
properties=undefined,
allow_dynoption: bool=False,
suffix: Optional[str]=None,
identifier: Optional[str]=None,
name: Optional[str]=None,
check_index: bool=True,
config_bag: ConfigBag=None,
@ -338,19 +348,19 @@ class SubConfig:
path = self.get_path(name,
option,
)
if suffix is None:
suffixes = self.suffixes
if identifier is None:
identifiers = self.identifiers
else:
if self.suffixes:
suffixes = self.suffixes + [suffix]
if self.identifiers:
identifiers = self.identifiers + [identifier]
else:
suffixes = [suffix]
identifiers = [identifier]
subsubconfig = SubConfig(option,
index,
path,
self.config_bag,
self,
suffixes,
identifiers,
properties=properties,
validate_properties=validate_properties,
true_path=true_path,
@ -363,7 +373,7 @@ class SubConfig:
if index >= length:
raise LeadershipError(_(f'index "{index}" is greater than the leadership '
f'length "{length}" for option '
f'"{option.impl_get_display_name(subsubconfig)}"'))
f'{option.impl_get_display_name(subsubconfig, with_quote=True)}'))
return subsubconfig
def get_path(self,
@ -393,7 +403,7 @@ class SubConfig:
path,
cconfig_bag,
self,
self.suffixes,
self.identifiers,
validate_properties=False,
)
self._length = len(cconfig_bag.context.get_value(subconfig))
@ -587,10 +597,10 @@ class _Config(CCache):
raise AttributeError(_("no option found in config"
" with these criteria"))
def _walk_valid_value(self,
subconfig,
only_mandatory,
):
def walk_valid_value(self,
subconfig,
only_mandatory,
):
value = self.get_value(subconfig,
need_help=False,
)
@ -649,19 +659,19 @@ class _Config(CCache):
option = subconfig.option.get_child(name,
config_bag,
subconfig,
with_suffix=True,
with_identifier=True,
allow_dynoption=allow_dynoption,
)
if isinstance(option, tuple):
suffix, option = option
identifier, option = option
else:
suffix = None
identifier = None
subconfig = subconfig.get_child(option,
index_,
validate_properties,
properties=properties,
name=name,
suffix=suffix,
identifier=identifier,
true_path=true_path_,
)
return subconfig
@ -710,9 +720,9 @@ class _Config(CCache):
only_mandatory: bool,
):
try:
value = self._walk_valid_value(subconfig,
only_mandatory,
)
value = self.walk_valid_value(subconfig,
only_mandatory,
)
except PropertiesOptionError as err:
if err.proptype in (['mandatory'], ['empty']):
if only_mandatory:
@ -764,8 +774,8 @@ class _Config(CCache):
length = subconfig.parent.get_length_leadership()
follower_len = self.get_values().get_max_length(subconfig.path)
if follower_len > length:
option_name = subconfig.option.impl_get_display_name(subconfig)
raise LeadershipError(_(f'the follower option "{option_name}" '
option_name = subconfig.option.impl_get_display_name(subconfig, with_quote=True)
raise LeadershipError(_(f'the follower option {option_name} '
f'has greater length ({follower_len}) than the leader '
f'length ({length})'))
self.get_settings().validate_mandatory(subconfig,