Compare commits
8 commits
477b0a2da1
...
d2a7c2b31c
| Author | SHA1 | Date | |
|---|---|---|---|
| d2a7c2b31c | |||
| 8b4ceddb81 | |||
| 4b41fd89fb | |||
| 7fad2b5d7d | |||
| a36d3cb9bf | |||
| add176331e | |||
| 245d5c02fb | |||
| ba0634f163 |
16 changed files with 269 additions and 42 deletions
|
|
@ -42,7 +42,6 @@ def calc_disk_usage(path, size='bytes'):
|
|||
# do not calc if path is None
|
||||
if path is None:
|
||||
return None
|
||||
|
||||
if size == 'bytes':
|
||||
div = 1
|
||||
else:
|
||||
|
|
@ -60,12 +59,13 @@ usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage,
|
|||
|
||||
Finally add those options in option description and a Config:
|
||||
|
||||
```
|
||||
```python
|
||||
disk = OptionDescription('disk', 'Verify disk usage', [filename, usage])
|
||||
root = OptionDescription('root', 'root', [disk])
|
||||
async def main():
|
||||
config = await Config(root)
|
||||
await config.property.read_write()
|
||||
return config
|
||||
|
||||
config = run(main())
|
||||
```
|
||||
|
|
@ -109,26 +109,29 @@ returns:
|
|||
|
||||
When you enter a value it is validated:
|
||||
|
||||
>>> try:
|
||||
>>> config.option('disk.path').value.set('/unknown')
|
||||
>>> except ValueError as err:
|
||||
>>> print(err)
|
||||
```python
|
||||
async def main():
|
||||
try:
|
||||
await config.option('disk.path').value.set('/unknown')
|
||||
except ValueError as err:
|
||||
print(err)
|
||||
|
||||
run(main())
|
||||
```
|
||||
|
||||
returns:
|
||||
|
||||
```
|
||||
"/unknown" is an invalid file name for "Path", this directory does not exist
|
||||
|
||||
We can also set a :doc:`calculation` as value. For example, we want to launch previous function but with in_gb to True as second argument:
|
||||
|
||||
>>> calc = Calculation(calc_disk_usage, Params((ParamOption(filename),
|
||||
... ParamValue('gigabytes'))))
|
||||
>>> config.option('disk.usage').value.set(calc)
|
||||
>>> config.option('disk.usage').value.get()
|
||||
622.6080360412598
|
||||
```
|
||||
|
||||
#### Is value is valid?
|
||||
|
||||
To check is a value is valid:
|
||||
|
||||
>>> config.option('disk.path').value.valid()
|
||||
True
|
||||
```python
|
||||
await config.option('disk.path').value.valid()
|
||||
```
|
||||
|
||||
#### Display the default value
|
||||
|
||||
|
|
|
|||
|
|
@ -471,6 +471,22 @@ from tiramisu import FilenameOption
|
|||
FilenameOption('file', 'file', '/etc/tiramisu/tiramisu.conf')
|
||||
```
|
||||
|
||||
## Unix file permissions: PermissionsOption
|
||||
|
||||
Valid the representing Unix permissions is an octal (base-8) notation.
|
||||
|
||||
```python
|
||||
from tiramisu import PermissionsOption
|
||||
PermissionsOption('perms', 'perms', 755)
|
||||
PermissionsOption('perms', 'perms', 1755)
|
||||
```
|
||||
|
||||
This option doesn't allow (or display a warning with warnings_only):
|
||||
|
||||
- 777 (two weak value)
|
||||
- others have more right than group
|
||||
- group has more right than user
|
||||
|
||||
# Date option
|
||||
|
||||
## Date option: DateOption
|
||||
|
|
|
|||
|
|
@ -436,6 +436,7 @@ async def test_config_od_type(config_type):
|
|||
o2 = OptionDescription('val', '', [o])
|
||||
async with await Config(o2) as cfg:
|
||||
cfg = await get_config(cfg, config_type)
|
||||
assert await cfg.option('val').option.type() == 'optiondescription'
|
||||
assert await cfg.option('val.i').option.type() == 'integer'
|
||||
assert not await list_sessions()
|
||||
|
||||
|
|
|
|||
|
|
@ -881,6 +881,51 @@ async def test_requires_dyndescription_in_dyn():
|
|||
assert not await list_sessions()
|
||||
|
||||
|
||||
def calc_value_not_same(param, condition, expected, default, suffix):
|
||||
if suffix == 'val1':
|
||||
index = 0
|
||||
else:
|
||||
index = 1
|
||||
return calc_value(param, condition=condition[index], expected=expected, default=default)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_dyndescription_in_dyn_not_same():
|
||||
boolean = BoolOption('boolean', '', True)
|
||||
disabled_property = Calculation(calc_value_not_same,
|
||||
Params(ParamValue('disabled'),
|
||||
kwargs={'condition': ParamOption(boolean, raisepropertyerror=True),
|
||||
'expected': ParamValue(False),
|
||||
'default': ParamValue(None),
|
||||
'suffix': ParamSuffix()}))
|
||||
st = StrOption('st', '', properties=(disabled_property,))
|
||||
dod1 = DynOptionDescription('dod1', '', [boolean], suffixes=Calculation(return_list))
|
||||
dod2 = DynOptionDescription('dod2', '', [st], suffixes=Calculation(return_list))
|
||||
od = OptionDescription('od', '', [dod1, dod2])
|
||||
od2 = OptionDescription('od', '', [od])
|
||||
async with await Config(od2) as cfg:
|
||||
await cfg.property.read_write()
|
||||
|
||||
assert await cfg.option('od.dod2val1.stval1').value.get() is None
|
||||
assert await cfg.option('od.dod2val2.stval2').value.get() is None
|
||||
#
|
||||
await cfg.option('od.dod1val1.booleanval1').value.set(False)
|
||||
|
||||
props = []
|
||||
try:
|
||||
await cfg.option('od.dod2val1.stval1').value.get()
|
||||
except PropertiesOptionError as err:
|
||||
props = err.proptype
|
||||
assert props == frozenset(['disabled'])
|
||||
props = []
|
||||
await cfg.option('od.dod2val2.stval2').value.get()
|
||||
#
|
||||
await cfg.option('od.dod1val1.booleanval1').value.set(True)
|
||||
assert await cfg.option('od.dod2val1.stval1').value.get() is None
|
||||
assert await cfg.option('od.dod2val2.stval2').value.get() is None
|
||||
assert not await list_sessions()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_dyndescription2():
|
||||
boolean = BoolOption('boolean', '', True)
|
||||
|
|
|
|||
|
|
@ -1074,3 +1074,16 @@ async def test_follower_properties():
|
|||
await cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).property.get() == ('aproperty', 'newproperty', 'newproperty1')
|
||||
await cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).property.get() == ('aproperty', 'newproperty1')
|
||||
assert not await list_sessions()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_get_leader(config_type):
|
||||
ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
|
||||
netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=True)
|
||||
interface1 = Leadership('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
|
||||
maconfig = OptionDescription('conf', '', [interface1])
|
||||
async with await Config(maconfig) as cfg:
|
||||
option = await cfg.option('ip_admin_eth0.netmask_admin_eth0').option.leader()
|
||||
assert await option.option.get() == ip_admin_eth0
|
||||
|
||||
assert not await list_sessions()
|
||||
|
|
|
|||
|
|
@ -1514,6 +1514,18 @@ async def test_calc_value_remove_duplicate(config_type):
|
|||
assert not await list_sessions()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calc_value_remove_duplicate2(config_type):
|
||||
val1 = StrOption('val1', "", ['val1', 'val1'], multi=True, properties=('notunique',))
|
||||
val2 = StrOption('val2', "", ['val1', 'val1'], multi=True, properties=('notunique',))
|
||||
val3 = StrOption('val3', "", Calculation(calc_value, Params((ParamOption(val1), ParamOption(val2)), multi=ParamValue(True), remove_duplicate_value=ParamValue(True), join=ParamValue('-'))), multi=True)
|
||||
od = OptionDescription('root', '', [val1, val2, val3])
|
||||
async with await Config(od) as cfg:
|
||||
cfg = await get_config(cfg, config_type)
|
||||
assert await cfg.value.dict() == {'val1': ['val1', 'val1'], 'val2': ['val1', 'val1'], 'val3': ['val1-val1']}
|
||||
assert not await list_sessions()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calc_value_join(config_type):
|
||||
val1 = StrOption('val1', "", 'val1')
|
||||
|
|
|
|||
36
tests/test_option_permissions.py
Normal file
36
tests/test_option_permissions.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"configuration objects global API"
|
||||
from .autopath import do_autopath
|
||||
do_autopath()
|
||||
|
||||
import pytest
|
||||
|
||||
from tiramisu import PermissionsOption
|
||||
|
||||
|
||||
def test_permissions():
|
||||
PermissionsOption('a', '', 640)
|
||||
PermissionsOption('a', '', 642)
|
||||
PermissionsOption('a', '', 751)
|
||||
PermissionsOption('a', '', 753)
|
||||
PermissionsOption('a', '', 7555)
|
||||
PermissionsOption('a', '', 1755)
|
||||
with pytest.raises(ValueError):
|
||||
PermissionsOption('a', '', 800)
|
||||
with pytest.raises(ValueError):
|
||||
PermissionsOption('a', '', 75)
|
||||
with pytest.raises(ValueError):
|
||||
PermissionsOption('a', '', 77775)
|
||||
with pytest.raises(ValueError):
|
||||
PermissionsOption('a', '', '755')
|
||||
with pytest.raises(ValueError):
|
||||
PermissionsOption('a', '', 'string')
|
||||
with pytest.raises(ValueError):
|
||||
PermissionsOption('a', '', 800)
|
||||
with pytest.raises(ValueError):
|
||||
PermissionsOption('a', '', 1575)
|
||||
with pytest.raises(ValueError):
|
||||
PermissionsOption('a', '', 1557)
|
||||
with pytest.raises(ValueError):
|
||||
PermissionsOption('a', '', 777)
|
||||
with pytest.raises(ValueError):
|
||||
PermissionsOption('a', '', 1777)
|
||||
|
|
@ -407,6 +407,8 @@ class TiramisuOptionOption(_TiramisuOptionOptionDescription):
|
|||
|
||||
@option_and_connection
|
||||
async def type(self):
|
||||
if self._option_bag.option.impl_is_optiondescription():
|
||||
return 'optiondescription'
|
||||
return self._option_bag.option.get_type()
|
||||
|
||||
@option_and_connection
|
||||
|
|
@ -437,6 +439,12 @@ class TiramisuOptionOption(_TiramisuOptionOptionDescription):
|
|||
self._option_bag.config_bag,
|
||||
)
|
||||
|
||||
@option_and_connection
|
||||
async def leader(self):
|
||||
return TiramisuOption(self._option_bag.option.impl_get_leadership().get_leader().impl_getpath(),
|
||||
None,
|
||||
self._option_bag.config_bag)
|
||||
|
||||
|
||||
class TiramisuOptionOwner(CommonTiramisuOption):
|
||||
#FIXME optiondescription must not have Owner!
|
||||
|
|
@ -699,7 +707,8 @@ class TiramisuOptionValue(CommonTiramisuOption):
|
|||
flatten=False,
|
||||
withwarning: bool=False,
|
||||
fullpath=False,
|
||||
leader_to_list=False):
|
||||
leader_to_list=False,
|
||||
):
|
||||
"""Dict with path as key and value"""
|
||||
name = self._option_bag.option.impl_getname()
|
||||
subconfig = await self._subconfig.get_subconfig(self._option_bag)
|
||||
|
|
@ -768,6 +777,7 @@ class TiramisuOptionValue(CommonTiramisuOption):
|
|||
option = self._option_bag.option
|
||||
values = self._option_bag.config_bag.context.cfgimpl_get_values()
|
||||
if option.impl_is_follower() and self._option_bag.index is None:
|
||||
# IF OU PAS IF ?? if self._option_bag.option.impl_is_symlinkoption():
|
||||
value = []
|
||||
length = await self._subconfig.cfgimpl_get_length_leadership(self._option_bag)
|
||||
settings = self._option_bag.config_bag.context.cfgimpl_get_settings()
|
||||
|
|
@ -779,6 +789,7 @@ class TiramisuOptionValue(CommonTiramisuOption):
|
|||
soption_bag.properties = await settings.getproperties(soption_bag)
|
||||
value.append(await values.getdefaultvalue(soption_bag))
|
||||
return value
|
||||
# raise APIError('index must be set with a follower option')
|
||||
else:
|
||||
return await values.getdefaultvalue(self._option_bag)
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ class ParamDynOption(ParamOption):
|
|||
dynoptiondescription: 'DynOptionDescription',
|
||||
notraisepropertyerror: bool=False,
|
||||
raisepropertyerror: bool=False,
|
||||
optional: bool=False,
|
||||
todict: bool=False,
|
||||
) -> None:
|
||||
super().__init__(option,
|
||||
|
|
@ -99,6 +100,7 @@ class ParamDynOption(ParamOption):
|
|||
)
|
||||
self.suffix = suffix
|
||||
self.dynoptiondescription = dynoptiondescription
|
||||
self.optional = optional
|
||||
|
||||
|
||||
class ParamSelfOption(Param):
|
||||
|
|
@ -273,13 +275,19 @@ async def manager_callback(callbk: Param,
|
|||
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 err from err
|
||||
raise ConfigError(_('unable to carry out a calculation for "{}"'
|
||||
', {}').format(option.impl_get_display_name(), err), err)
|
||||
', {}').format(option.impl_get_display_name(), err), err) from 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))
|
||||
raise ValueError(_('the option "{0}" is used in a calculation but is invalid ({1})').format(option_bag.option.impl_get_display_name(), err)) from err
|
||||
except AttributeError as err:
|
||||
raise ConfigError(_(f'unable to get value for calculating "{option_bag.option.impl_get_display_name()}", {err}'))
|
||||
if isinstance(callbk, ParamDynOption) and callbk.optional:
|
||||
# cannot acces, simulate a propertyerror
|
||||
raise PropertiesOptionError(option_bag,
|
||||
['configerror'],
|
||||
config_bag.context.cfgimpl_get_settings(),
|
||||
)
|
||||
raise ConfigError(_(f'unable to get value for calculating "{option_bag.option.impl_get_display_name()}", {err}')) from err
|
||||
return value
|
||||
|
||||
async def get_option_bag(config_bag,
|
||||
|
|
@ -351,6 +359,7 @@ async def manager_callback(callbk: Param,
|
|||
callbk_option = callbk.option
|
||||
callbk_options = None
|
||||
if callbk_option.issubdyn():
|
||||
found = False
|
||||
if isinstance(callbk, ParamDynOption):
|
||||
subdyn = callbk.dynoptiondescription
|
||||
rootpath = subdyn.impl_getpath() + callbk.suffix
|
||||
|
|
@ -358,7 +367,22 @@ async def manager_callback(callbk: Param,
|
|||
callbk_option = callbk_option.to_dynoption(rootpath,
|
||||
suffix,
|
||||
subdyn)
|
||||
elif not option.impl_is_dynsymlinkoption():
|
||||
found = True
|
||||
elif option.impl_is_dynsymlinkoption():
|
||||
rootpath = option.rootpath
|
||||
call_path = callbk_option.impl_getpath()
|
||||
if call_path.startswith(option.opt.impl_getpath().rsplit('.', 1)[0]):
|
||||
# in same dynoption
|
||||
if len(callbk_option.impl_getpath().split('.')) == len(rootpath.split('.')):
|
||||
rootpath = rootpath.rsplit('.', 1)[0]
|
||||
suffix = option.impl_getsuffix()
|
||||
subdyn = callbk_option.getsubdyn()
|
||||
callbk_option = callbk_option.to_dynoption(rootpath,
|
||||
suffix,
|
||||
subdyn,
|
||||
)
|
||||
found = True
|
||||
if not found:
|
||||
callbk_options = []
|
||||
dynopt = callbk_option.getsubdyn()
|
||||
rootpath = dynopt.impl_getpath()
|
||||
|
|
@ -370,16 +394,6 @@ async def manager_callback(callbk: Param,
|
|||
suffix,
|
||||
dynopt)
|
||||
callbk_options.append(doption)
|
||||
else:
|
||||
#FIXME in same dynamic option?
|
||||
rootpath = option.rootpath
|
||||
if len(callbk_option.impl_getpath().split('.')) == len(rootpath.split('.')):
|
||||
rootpath = rootpath.rsplit('.', 1)[0]
|
||||
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_is_follower() and index is None:
|
||||
raise Break()
|
||||
if config_bag is undefined:
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ class CalcValue:
|
|||
value = []
|
||||
elif None in value and not allow_none:
|
||||
value = []
|
||||
elif remove_duplicate_value:
|
||||
if remove_duplicate_value:
|
||||
new_value = []
|
||||
for val in value:
|
||||
if val not in new_value:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from .dateoption import DateOption
|
|||
from .filenameoption import FilenameOption
|
||||
from .passwordoption import PasswordOption
|
||||
from .macoption import MACOption
|
||||
from .permissionsoption import PermissionsOption
|
||||
|
||||
|
||||
__all__ = ('Leadership', 'OptionDescription', 'DynOptionDescription',
|
||||
|
|
@ -33,4 +34,4 @@ __all__ = ('Leadership', 'OptionDescription', 'DynOptionDescription',
|
|||
'IPOption', 'PortOption', 'NetworkOption', 'NetmaskOption',
|
||||
'BroadcastOption', 'DomainnameOption', 'EmailOption', 'URLOption',
|
||||
'UsernameOption', 'GroupnameOption', 'FilenameOption', 'PasswordOption', 'submulti',
|
||||
'RegexpOption', 'MACOption')
|
||||
'RegexpOption', 'MACOption', 'PermissionsOption')
|
||||
|
|
|
|||
|
|
@ -74,12 +74,10 @@ class DomainnameOption(StrOption):
|
|||
else:
|
||||
min_time = 1
|
||||
regexp = r'((?!-)[a-z0-9-]{{{1},{0}}}\.){{{1},}}[a-z0-9-]{{1,{0}}}'.format(self._get_len(type), min_time)
|
||||
msg = _('only lowercase, number, "-" and "." characters are allowed')
|
||||
msg_warning = _('only lowercase, number, "-" and "." characters are recommanded')
|
||||
else:
|
||||
regexp = r'((?!-)[a-z0-9-]{{1,{0}}})'.format(self._get_len(type))
|
||||
msg = _('only lowercase, number and "-" characters are allowed')
|
||||
msg_warning = _('only lowercase, number and "-" characters are recommanded')
|
||||
msg = _('must start with lowercase characters followed by lowercase characters, number, "-" and "." characters are allowed')
|
||||
msg_warning = _('must start with lowercase characters followed by lowercase characters, number, "-" and "." characters are recommanded')
|
||||
if allow_ip:
|
||||
msg = _('could be a IP, otherwise {}').format(msg)
|
||||
msg_warning = _('could be a IP, otherwise {}').format(msg_warning)
|
||||
|
|
|
|||
75
tiramisu/option/permissionsoption.py
Normal file
75
tiramisu/option/permissionsoption.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2023 Team tiramisu (see AUTHORS for all contributors)
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# The original `Config` design model is unproudly borrowed from
|
||||
# the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
|
||||
# the whole pypy projet is under MIT licence
|
||||
# ____________________________________________________________
|
||||
import re
|
||||
import sys
|
||||
|
||||
from ..setting import undefined, Undefined, OptionBag
|
||||
from ..i18n import _
|
||||
from .option import Option
|
||||
from .intoption import IntOption
|
||||
|
||||
|
||||
class PermissionsOption(IntOption):
|
||||
"""Unix file permissions
|
||||
Valid the representing Unix permissions is an octal (base-8) notation.
|
||||
This notation consists of at least three digits (owner, group, and others).
|
||||
If a fourth digit is present to the setuid bit, the setgid bit and the sticky bit attributes.
|
||||
This option is an integer value.
|
||||
"""
|
||||
__slots__ = tuple()
|
||||
perm_re = re.compile(r"^[0-7]{3,4}$")
|
||||
_type = 'permissions'
|
||||
_display_name = _('unix file permissions')
|
||||
|
||||
def __init__(self,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
#do not display intoption attributs
|
||||
super().__init__(*args,
|
||||
**kwargs)
|
||||
|
||||
def validate(self,
|
||||
value: str) -> None:
|
||||
super().validate(value)
|
||||
if not self.perm_re.search(str(value)):
|
||||
raise ValueError(_('only 3 or 4 octal digits are allowed'))
|
||||
|
||||
def second_level_validation(self,
|
||||
value: str,
|
||||
warnings_only: bool) -> None:
|
||||
old_digit = 7
|
||||
str_value = str(value)
|
||||
if len(str_value) == 4:
|
||||
str_value = str_value[1:]
|
||||
for idx, digit in enumerate(str_value):
|
||||
new_digit = int(digit)
|
||||
if old_digit < new_digit:
|
||||
if idx == 1:
|
||||
old = _('user')
|
||||
new = _('group')
|
||||
else:
|
||||
old = _('group')
|
||||
new = _('other')
|
||||
raise ValueError(_(f'{new} has more right than {old}'))
|
||||
old_digit = new_digit
|
||||
if str_value == '777':
|
||||
raise ValueError(_(f'too weak'))
|
||||
|
|
@ -19,11 +19,8 @@
|
|||
# the whole pypy projet is under MIT licence
|
||||
# ____________________________________________________________
|
||||
import re
|
||||
import sys
|
||||
|
||||
from ..setting import undefined, Undefined, OptionBag
|
||||
from ..i18n import _
|
||||
from .option import Option
|
||||
from .stroption import StrOption
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ FORBIDDEN_SET_PERMISSIVES = frozenset(['force_default_on_freeze',
|
|||
'force_metaconfig_on_freeze',
|
||||
'force_store_value'])
|
||||
ALLOWED_LEADER_PROPERTIES = frozenset(['empty',
|
||||
'notempty',
|
||||
'notunique',
|
||||
'unique',
|
||||
'force_store_value',
|
||||
|
|
|
|||
|
|
@ -175,7 +175,11 @@ class Values:
|
|||
value,
|
||||
reset_cache=True):
|
||||
if isinstance(value, Calculation):
|
||||
value = await value.execute(option_bag)
|
||||
try:
|
||||
value = await value.execute(option_bag)
|
||||
except ConfigError as err:
|
||||
msg = _(f'error when calculating "{option_bag.option.impl_get_display_name()}": {err} : {option_bag.path}')
|
||||
raise ConfigError(msg) from err
|
||||
elif isinstance(value, (list, tuple)):
|
||||
value = await self._do_value_list(value, option_bag)
|
||||
if reset_cache:
|
||||
|
|
|
|||
Loading…
Reference in a new issue