tiramisu/tiramisu/autolib.py

534 lines
23 KiB
Python
Raw Normal View History

2021-02-24 20:30:04 +01:00
# Copyright (C) 2012-2021 Team tiramisu (see AUTHORS for all contributors)
#
2013-09-22 22:33:09 +02:00
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
2013-09-22 22:33:09 +02:00
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
2013-09-22 22:33:09 +02:00
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
2012-09-18 09:48:41 +02:00
# The original `Config` design model is unproudly borrowed from
# the rough gus of pypy: pypy: http://codespeak.net/svn/pypy/dist/pypy/config/
# the whole pypy projet is under MIT licence
# ____________________________________________________________
"enables us to carry out a calculation and return an option's value"
2018-04-28 08:39:07 +02:00
from typing import Any, Optional, Union, Callable, Dict, List
from types import CoroutineType
2019-09-01 09:41:53 +02:00
from itertools import chain
2018-04-28 08:39:07 +02:00
2019-11-20 08:28:26 +01:00
from .error import PropertiesOptionError, ConfigError, LeadershipError, ValueWarning
from .i18n import _
2018-08-01 08:37:58 +02:00
from .setting import undefined, ConfigBag, OptionBag, Undefined
# ____________________________________________________________
2013-04-03 12:20:26 +02:00
2019-09-01 09:41:53 +02:00
class Params:
__slots__ = ('args', 'kwargs')
def __init__(self, args=None, kwargs=None, **kwgs):
if args is None:
args = tuple()
if kwargs is None:
kwargs = {}
if kwgs:
kwargs.update(kwgs)
if isinstance(args, Param):
args = (args,)
else:
if not isinstance(args, tuple):
raise ValueError(_('args in params must be a tuple'))
for arg in args:
if not isinstance(arg, Param):
raise ValueError(_('arg in params must be a Param'))
if not isinstance(kwargs, dict):
raise ValueError(_('kwargs in params must be a dict'))
for arg in kwargs.values():
if not isinstance(arg, Param):
raise ValueError(_('arg in params must be a Param'))
self.args = args
self.kwargs = kwargs
class Param:
pass
class ParamOption(Param):
__slots__ = ('todict',
'option',
'notraisepropertyerror',
'raisepropertyerror')
def __init__(self,
option: 'Option',
notraisepropertyerror: bool=False,
raisepropertyerror: bool=False,
todict: bool=False) -> None:
if __debug__ and not hasattr(option, 'impl_is_symlinkoption'):
raise ValueError(_('paramoption needs an option not {}').format(type(option)))
if option.impl_is_symlinkoption():
cur_opt = option.impl_getopt()
else:
cur_opt = option
assert isinstance(notraisepropertyerror, bool), _('param must have a boolean not a {} for notraisepropertyerror').format(type(notraisepropertyerror))
assert isinstance(raisepropertyerror, bool), _('param must have a boolean not a {} for raisepropertyerror').format(type(raisepropertyerror))
self.todict = todict
self.option = cur_opt
self.notraisepropertyerror = notraisepropertyerror
self.raisepropertyerror = raisepropertyerror
class ParamDynOption(ParamOption):
__slots__ = ('suffix',)
def __init__(self,
option: 'Option',
suffix: str,
dynoptiondescription: 'DynOptionDescription',
notraisepropertyerror: bool=False,
raisepropertyerror: bool=False,
todict: bool=False,
) -> None:
super().__init__(option,
notraisepropertyerror,
raisepropertyerror,
todict,
)
self.suffix = suffix
self.dynoptiondescription = dynoptiondescription
2019-09-01 09:41:53 +02:00
class ParamSelfOption(Param):
2019-10-27 11:09:15 +01:00
__slots__ = ('todict', 'whole')
2019-09-01 09:41:53 +02:00
def __init__(self,
2019-10-27 11:09:15 +01:00
todict: bool=False,
whole: bool=undefined) -> None:
"""whole: send all value for a multi, not only indexed value"""
2019-09-01 09:41:53 +02:00
self.todict = todict
2019-10-27 11:09:15 +01:00
if whole is not undefined:
self.whole = whole
2019-09-01 09:41:53 +02:00
class ParamValue(Param):
__slots__ = ('value',)
def __init__(self, value):
self.value = value
class ParamInformation(Param):
__slots__ = ('information_name',)
def __init__(self,
information_name: str,
default_value: Any=undefined,
) -> None:
self.information_name = information_name
self.default_value = default_value
2021-03-06 19:23:35 +01:00
class ParamSelfInformation(ParamInformation):
__slots__ = tuple()
2019-09-01 09:41:53 +02:00
class ParamIndex(Param):
__slots__ = tuple()
2019-10-27 11:09:15 +01:00
class ParamSuffix(Param):
__slots__ = tuple()
2019-09-01 09:41:53 +02:00
class Calculation:
__slots__ = ('function',
'params',
'help_function',
2019-11-20 08:28:26 +01:00
'_has_index',
2019-10-27 11:09:15 +01:00
'warnings_only')
2019-09-01 09:41:53 +02:00
def __init__(self,
function: Callable,
2019-09-28 16:32:48 +02:00
params: Params=Params(),
2019-10-27 11:09:15 +01:00
help_function: Optional[Callable]=None,
warnings_only: bool=False):
2019-09-01 09:41:53 +02:00
assert isinstance(function, Callable), _('first argument ({0}) must be a function').format(function)
if help_function:
assert isinstance(help_function, Callable), _('help_function ({0}) must be a function').format(help_function)
self.help_function = help_function
else:
self.help_function = None
self.function = function
2019-09-28 16:32:48 +02:00
self.params = params
for arg in chain(self.params.args, self.params.kwargs.values()):
if isinstance(arg, ParamIndex):
2019-11-20 08:28:26 +01:00
self._has_index = True
2019-09-28 16:32:48 +02:00
break
2019-10-27 11:09:15 +01:00
if warnings_only is True:
self.warnings_only = warnings_only
2019-09-01 09:41:53 +02:00
2019-12-24 15:24:20 +01:00
async def execute(self,
2019-09-01 09:41:53 +02:00
option_bag: OptionBag,
2019-10-27 11:09:15 +01:00
leadership_must_have_index: bool=False,
orig_value: Any=undefined,
2021-03-08 06:39:40 +01:00
allow_value_error: bool=False,
force_value_warning: bool=False,
for_settings: bool=False,
2020-08-04 16:49:21 +02:00
) -> Any:
2019-12-24 15:24:20 +01:00
return await carry_out_calculation(option_bag.option,
callback=self.function,
callback_params=self.params,
index=option_bag.index,
config_bag=option_bag.config_bag,
leadership_must_have_index=leadership_must_have_index,
orig_value=orig_value,
2020-08-04 16:49:21 +02:00
allow_value_error=allow_value_error,
force_value_warning=force_value_warning,
2021-03-08 06:39:40 +01:00
for_settings=for_settings,
2020-08-04 16:49:21 +02:00
)
2019-12-24 15:24:20 +01:00
async def help(self,
option_bag: OptionBag,
2021-03-08 06:39:40 +01:00
leadership_must_have_index: bool=False,
for_settings: bool=False,
) -> str:
2019-09-01 09:41:53 +02:00
if not self.help_function:
2020-01-22 20:46:18 +01:00
return await self.execute(option_bag,
2021-03-08 06:39:40 +01:00
leadership_must_have_index=leadership_must_have_index,
for_settings=for_settings,
)
2019-12-24 15:24:20 +01:00
return await carry_out_calculation(option_bag.option,
callback=self.help_function,
callback_params=self.params,
index=option_bag.index,
config_bag=option_bag.config_bag,
2021-03-08 06:39:40 +01:00
leadership_must_have_index=leadership_must_have_index,
for_settings=for_settings,
)
2019-09-01 09:41:53 +02:00
2019-11-20 08:28:26 +01:00
def has_index(self, current_option):
if hasattr(self, '_has_index'):
return self._has_index
self._has_index = False
for arg in chain(self.params.args, self.params.kwargs.values()):
if isinstance(arg, ParamOption) and arg.option.impl_get_leadership() and \
arg.option.impl_get_leadership().in_same_group(current_option):
self._has_index = True
break
return self._has_index
2019-09-01 09:41:53 +02:00
class Break(Exception):
pass
async def manager_callback(callbk: Param,
2019-12-24 15:24:20 +01:00
option,
index: Optional[int],
orig_value,
config_bag: ConfigBag,
2021-03-08 06:39:40 +01:00
leadership_must_have_index: bool,
for_settings: bool,
) -> Any:
"""replace Param by true value"""
2019-10-27 11:09:15 +01:00
def calc_index(callbk, index, same_leadership):
if index is not None:
if hasattr(callbk, 'whole'):
whole = callbk.whole
else:
# if value is same_leadership, follower are isolate by default
# otherwise option is a whole option
whole = not same_leadership
if not whole:
return index
return None
2019-12-24 15:24:20 +01:00
async def calc_self(callbk, option, index, value, config_bag):
2019-10-27 11:09:15 +01:00
# index must be apply only if follower
is_follower = option.impl_is_follower()
apply_index = calc_index(callbk, index, is_follower)
if value is undefined or (apply_index is None and is_follower):
if config_bag is undefined:
return undefined
path = option.impl_getpath()
2019-12-25 20:44:56 +01:00
option_bag = await get_option_bag(config_bag,
option,
2020-01-22 20:46:18 +01:00
apply_index,
True)
2019-12-24 15:24:20 +01:00
new_value = await get_value(callbk, option_bag, path)
2019-10-27 11:09:15 +01:00
if apply_index is None and is_follower:
new_value[index] = value
value = new_value
elif apply_index is not None and not is_follower:
value = value[apply_index]
return value
async def get_value(callbk,
option_bag,
path,
):
2019-10-27 11:09:15 +01:00
try:
# get value
2019-12-24 15:24:20 +01:00
value = await config_bag.context.getattr(path,
option_bag)
2019-10-27 11:09:15 +01:00
except PropertiesOptionError as err:
# raise PropertiesOptionError (which is catched) because must not add value None in carry_out_calculation
if callbk.notraisepropertyerror or callbk.raisepropertyerror:
raise err
raise ConfigError(_('unable to carry out a calculation for "{}"'
', {}').format(option.impl_get_display_name(), err), err)
except ValueError as err:
raise ValueError(_('the option "{0}" is used in a calculation but is invalid ({1})').format(option_bag.option.impl_get_display_name(), err))
except AttributeError as err:
raise ConfigError(_('impossible to calculate "{0}", {1}').format(option_bag.option.impl_get_display_name(),
err,
))
2019-10-27 11:09:15 +01:00
return value
2019-12-24 15:24:20 +01:00
async def get_option_bag(config_bag,
opt,
2020-01-22 20:46:18 +01:00
index_,
self_calc):
2019-10-27 11:09:15 +01:00
# don't validate if option is option that we tried to validate
config_bag = config_bag.copy()
2021-03-08 06:39:40 +01:00
if for_settings:
config_bag.properties = config_bag.true_properties - {'warnings'}
2019-10-27 11:09:15 +01:00
config_bag.set_permissive()
2021-03-08 06:39:40 +01:00
if not for_settings:
config_bag.properties -= {'warnings'}
2019-10-27 11:09:15 +01:00
option_bag = OptionBag()
option_bag.set_option(opt,
index_,
config_bag)
2020-01-22 20:46:18 +01:00
if not self_calc:
option_bag.properties = await config_bag.context.cfgimpl_get_settings().getproperties(option_bag)
else:
option_bag.config_bag.unrestraint()
option_bag.config_bag.remove_validation()
# if we are in properties calculation, cannot calculated properties
option_bag.properties = await config_bag.context.cfgimpl_get_settings().getproperties(option_bag,
apply_requires=False)
2019-10-27 11:09:15 +01:00
return option_bag
if isinstance(callbk, ParamValue):
return callbk.value
2019-10-27 11:09:15 +01:00
if isinstance(callbk, ParamInformation):
2021-03-06 19:23:35 +01:00
if isinstance(callbk, ParamSelfInformation):
option_bag = OptionBag()
option_bag.set_option(option,
index,
config_bag,
)
else:
option_bag = None
try:
2021-03-06 19:23:35 +01:00
return await config_bag.context.impl_get_information(config_bag,
option_bag,
callbk.information_name,
callbk.default_value,
)
except ValueError as err:
raise ConfigError(_('option "{}" cannot be calculated: {}').format(option.impl_get_display_name(),
str(err),
))
if isinstance(callbk, ParamIndex):
return index
2019-10-27 11:09:15 +01:00
if isinstance(callbk, ParamSuffix):
if not option.issubdyn():
raise ConfigError(_('option "{}" is not in a dynoptiondescription').format(option.impl_get_display_name()))
2019-10-27 11:09:15 +01:00
return option.impl_getsuffix()
2019-09-01 09:41:53 +02:00
if isinstance(callbk, ParamSelfOption):
2019-10-27 11:09:15 +01:00
if leadership_must_have_index and option.impl_get_leadership() and index is None:
raise Break()
2019-12-24 15:24:20 +01:00
value = await calc_self(callbk, option, index, orig_value, config_bag)
2019-10-27 11:09:15 +01:00
if not callbk.todict:
return value
return {'name': option.impl_get_display_name(),
'value': value}
if isinstance(callbk, ParamOption):
callbk_option = callbk.option
if callbk_option.issubdyn():
if isinstance(callbk, ParamDynOption):
subdyn = callbk.dynoptiondescription
rootpath = subdyn.impl_getpath() + callbk.suffix
suffix = callbk.suffix
else:
if not option.impl_is_dynsymlinkoption():
2021-05-18 18:53:14 +02:00
msg = 'option "{}" is not dynamic but is an argument of the dynamic option "{}" in a callback'
raise ConfigError(_(msg).format(option.impl_get_display_name(),
callbk_option.impl_get_display_name(),
))
2021-05-18 18:53:14 +02:00
#FIXME in same dynamic option?
rootpath = option.rootpath
suffix = option.impl_getsuffix()
subdyn = callbk_option.getsubdyn()
callbk_option = callbk_option.to_dynoption(rootpath,
suffix,
subdyn)
if leadership_must_have_index and callbk_option.impl_get_leadership() and index is None:
raise Break()
if config_bag is undefined:
return undefined
if index is not None and callbk_option.impl_get_leadership() and \
callbk_option.impl_get_leadership().in_same_group(option):
if not callbk_option.impl_is_follower():
# leader
index_ = None
with_index = True
else:
# follower
index_ = index
with_index = False
2019-10-27 11:09:15 +01:00
else:
index_ = None
with_index = False
path = callbk_option.impl_getpath()
option_bag = await get_option_bag(config_bag,
callbk_option,
index_,
False)
value = await get_value(callbk,
option_bag,
path,
)
if with_index:
value = value[index]
if not callbk.todict:
return value
return {'name': callbk_option.impl_get_display_name(),
'value': value}
raise ConfigError(_('unknown callback type {} in option {}').format(callbk,
option.impl_get_display_name()))
2019-12-24 15:24:20 +01:00
async def carry_out_calculation(option,
callback: Callable,
callback_params: Optional[Params],
index: Optional[int],
config_bag: Optional[ConfigBag],
orig_value=undefined,
leadership_must_have_index: bool=False,
2020-08-04 16:49:21 +02:00
allow_value_error: bool=False,
force_value_warning: bool=False,
2021-03-08 06:39:40 +01:00
for_settings: bool=False,
2020-08-04 16:49:21 +02:00
):
2013-08-21 14:52:48 +02:00
"""a function that carries out a calculation for an option's value
2013-04-03 12:20:26 +02:00
2014-01-30 22:55:15 +01:00
:param option: the option
2013-08-21 14:52:48 +02:00
:param callback: the name of the callback function
:param callback_params: the callback's parameters
(only keyword parameters are allowed)
:param index: if an option is multi, only calculates the nth value
2020-08-04 16:49:21 +02:00
:param allow_value_error: to know if carry_out_calculation can return ValueError or ValueWarning (for example if it's a validation)
:param force_value_warning: transform valueError to ValueWarning object
The callback_params is a dict. Key is used to build args (if key is '')
and kwargs (otherwise). Values are tuple of:
- values
- tuple with option and boolean's force_permissive (True when don't raise
if PropertiesOptionError)
2019-10-27 11:09:15 +01:00
Values could have multiple values only when key is ''."""
2019-11-20 08:28:26 +01:00
def fake_items(iterator):
return ((None, i) for i in iterator)
2017-12-19 23:11:45 +01:00
args = []
kwargs = {}
if callback_params:
2019-11-20 08:28:26 +01:00
for key, callbk in chain(fake_items(callback_params.args), callback_params.kwargs.items()):
try:
2019-12-24 15:24:20 +01:00
value = await manager_callback(callbk,
option,
index,
orig_value,
config_bag,
2021-03-08 06:39:40 +01:00
leadership_must_have_index,
for_settings,
)
if value is undefined:
return undefined
2019-11-20 08:28:26 +01:00
if key is None:
args.append(value)
else:
kwargs[key] = value
2019-09-01 09:41:53 +02:00
except PropertiesOptionError as err:
if callbk.raisepropertyerror:
raise err
if callbk.todict:
2019-11-20 08:28:26 +01:00
if key is None:
args.append({'propertyerror': str(err)})
else:
kwargs[key] = {'propertyerror': str(err)}
2019-09-01 09:41:53 +02:00
except Break:
continue
ret = await calculate(option,
callback,
allow_value_error,
force_value_warning,
args,
kwargs)
2017-12-19 23:11:45 +01:00
if isinstance(ret, list) and not option.impl_is_dynoptiondescription() and \
2020-04-21 17:13:25 +02:00
option.impl_is_follower() and not option.impl_is_submulti():
2017-12-19 23:11:45 +01:00
if args or kwargs:
2019-06-12 08:45:56 +02:00
raise LeadershipError(_('the "{}" function with positional arguments "{}" '
'and keyword arguments "{}" must not return '
'a list ("{}") for the follower option "{}"'
2019-02-23 19:06:23 +01:00
'').format(callback.__name__,
args,
kwargs,
ret,
option.impl_get_display_name()))
2018-04-09 21:37:49 +02:00
else:
2019-06-12 08:45:56 +02:00
raise LeadershipError(_('the "{}" function must not return a list ("{}") '
'for the follower option "{}"'
2019-02-23 19:06:23 +01:00
'').format(callback.__name__,
ret,
2019-06-12 08:45:56 +02:00
option.impl_get_display_name()))
2017-12-19 23:11:45 +01:00
return ret
2012-09-18 09:48:41 +02:00
2013-04-03 12:20:26 +02:00
async def calculate(option,
callback: Callable,
allow_value_error: bool,
force_value_warning: bool,
args,
kwargs,
):
2013-05-23 14:55:52 +02:00
"""wrapper that launches the 'callback'
2013-05-10 16:02:27 +02:00
:param callback: callback function
:param args: in the callback's arity, the unnamed parameters
:param kwargs: in the callback's arity, the named parameters
2013-05-23 14:55:52 +02:00
2013-05-10 16:02:27 +02:00
"""
2016-10-14 22:20:14 +02:00
try:
ret = callback(*args, **kwargs)
if isinstance(ret, CoroutineType):
ret = await ret
return ret
2020-08-04 16:49:21 +02:00
except (ValueError, ValueWarning) as err:
if allow_value_error:
if force_value_warning:
raise ValueWarning(str(err))
2017-11-28 22:42:30 +01:00
raise err
error = err
except Exception as err:
2019-11-20 08:28:26 +01:00
# import traceback
# traceback.print_exc()
error = err
2017-12-19 23:11:45 +01:00
if args or kwargs:
msg = _('unexpected error "{0}" in function "{1}" with arguments "{3}" and "{4}" '
'for option "{2}"').format(str(error),
2017-07-09 09:49:03 +02:00
callback.__name__,
option.impl_get_display_name(),
args,
kwargs)
else:
msg = _('unexpected error "{0}" in function "{1}" for option "{2}"'
'').format(str(error),
2017-07-09 09:49:03 +02:00
callback.__name__,
option.impl_get_display_name())
2021-02-06 15:18:28 +01:00
raise ConfigError(msg) from error