simplify tiramisu/option/baseoption.py

This commit is contained in:
Emmanuel Garette 2018-11-15 16:17:39 +01:00
parent 8b3da4d37b
commit 92b469bd43
17 changed files with 115 additions and 163 deletions

View File

@ -25,7 +25,6 @@ def test_option_valid_name():
raises(ValueError, 'IntOption(1, "")') raises(ValueError, 'IntOption(1, "")')
raises(ValueError, 'IntOption("1test", "")') raises(ValueError, 'IntOption("1test", "")')
IntOption("test1", "") IntOption("test1", "")
raises(ValueError, 'IntOption("impl_test", "")')
raises(ValueError, 'IntOption("_test", "")') raises(ValueError, 'IntOption("_test", "")')
raises(ValueError, 'IntOption(" ", "")') raises(ValueError, 'IntOption(" ", "")')

View File

@ -264,15 +264,15 @@ def test_param_option():
def test_callback_invalid(): def test_callback_invalid():
raises(ValueError, 'val1 = StrOption("val1", "", callback="string")') raises(AssertionError, 'val1 = StrOption("val1", "", callback="string")')
raises(ValueError, 'val1 = StrOption("val1", "", callback=return_val, callback_params="string")') raises(AssertionError, 'val1 = StrOption("val1", "", callback=return_val, callback_params="string")')
val1 = StrOption('val1', "", 'val') val1 = StrOption('val1', "", 'val')
val1 val1
raises(ValueError, "StrOption('val2', '', callback=return_value, callback_params={'': 'string'})") raises(AssertionError, "StrOption('val2', '', callback=return_value, callback_params={'': 'string'})")
raises(ValueError, "StrOption('val4', '', callback=return_value, callback_params={'value': (('string', False),)})") raises(AssertionError, "StrOption('val4', '', callback=return_value, callback_params={'value': (('string', False),)})")
raises(ValueError, "StrOption('val4', '', callback=return_value, callback_params={'value': ((val1, 'string'),)})") raises(AssertionError, "StrOption('val4', '', callback=return_value, callback_params={'value': ((val1, 'string'),)})")
raises(ValueError, "StrOption('val4', '', callback=return_value, callback_params={'value': ((val1, False, 'unknown'),)})") raises(AssertionError, "StrOption('val4', '', callback=return_value, callback_params={'value': ((val1, False, 'unknown'),)})")
raises(ValueError, "StrOption('val4', '', callback=return_value, callback_params={'value': ((val1,),)})") raises(AssertionError, "StrOption('val4', '', callback=return_value, callback_params={'value': ((val1,),)})")
def test_callback_with_context(): def test_callback_with_context():

View File

@ -55,9 +55,8 @@ def test_optname_shall_not_start_with_numbers():
def test_option_has_an_api_name(): def test_option_has_an_api_name():
raises(ValueError, "BoolOption('cfgimpl_get_settings', 'dummy', default=False)") b = BoolOption('impl_has_dependency', 'dummy', default=True)
raises(ValueError, "BoolOption('impl_getdoc', 'dummy', default=False)") descr = OptionDescription('tiramisu', '', [b])
raises(ValueError, "BoolOption('_unvalid', 'dummy', default=False)") api = Config(descr)
raises(ValueError, "BoolOption('6unvalid', 'dummy', default=False)") assert api.option('impl_has_dependency').value.get() is True
BoolOption('unvalid6', 'dummy', default=False) assert b.impl_has_dependency() is False
BoolOption('unvalid_', 'dummy', default=False)

View File

@ -121,42 +121,6 @@ def test_slots_option_readonly():
raises(AttributeError, "q._requires = 'q'") raises(AttributeError, "q._requires = 'q'")
def test_slots_option_readonly_name():
a = ChoiceOption('a', '', ('a',))
b = BoolOption('b', '')
c = IntOption('c', '')
d = FloatOption('d', '')
e = StrOption('e', '')
f = SymLinkOption('f', c)
g = UnicodeOption('g', '')
h = IPOption('h', '')
i = PortOption('i', '')
j = NetworkOption('j', '')
k = NetmaskOption('k', '')
l = DomainnameOption('l', '')
o = DomainnameOption('o', '')
p = DomainnameOption('p', '')
q = DomainnameOption('q', '')
m = OptionDescription('m', '', [a, b, c, d, e, f, g, h, i, j, k, l, o, p, q])
m
raises(AttributeError, "a._name = 'a'")
raises(AttributeError, "b._name = 'b'")
raises(AttributeError, "c._name = 'c'")
raises(AttributeError, "d._name = 'd'")
raises(AttributeError, "e._name = 'e'")
raises(AttributeError, "f._name = 'f'")
raises(AttributeError, "g._name = 'g'")
raises(AttributeError, "h._name = 'h'")
raises(AttributeError, "i._name = 'i'")
raises(AttributeError, "j._name = 'j'")
raises(AttributeError, "k._name = 'k'")
raises(AttributeError, "l._name = 'l'")
raises(AttributeError, "m._name = 'm'")
raises(AttributeError, "o._name = 'o'")
raises(AttributeError, "p._name = 'p'")
raises(AttributeError, "q._name = 'q'")
#def test_slots_description(): #def test_slots_description():
# # __slots__ for OptionDescription should be complete for __getattr__ # # __slots__ for OptionDescription should be complete for __getattr__
# slots = set() # slots = set()

View File

@ -113,7 +113,7 @@ class SubConfig(object):
if option_bag.path in resetted_opts: if option_bag.path in resetted_opts:
return return
resetted_opts.append(option_bag.path) resetted_opts.append(option_bag.path)
for woption in option_bag.option._get_dependencies(self): for woption in option_bag.option._get_dependencies(self.cfgimpl_get_description()):
option = woption() option = woption()
if option.impl_is_dynoptiondescription(): if option.impl_is_dynoptiondescription():
subpath = option.impl_getpath().rsplit('.', 1)[0] subpath = option.impl_getpath().rsplit('.', 1)[0]

View File

@ -23,6 +23,8 @@ def display_list(lst, separator='and', add_quote=False):
separator = _('and') separator = _('and')
elif separator == 'or': elif separator == 'or':
separator = _('or') separator = _('or')
if isinstance(lst, tuple) or isinstance(lst, frozenset):
lst = list(lst)
if len(lst) == 1: if len(lst) == 1:
ret = lst[0] ret = lst[0]
if not isinstance(ret, str): if not isinstance(ret, str):
@ -31,8 +33,6 @@ def display_list(lst, separator='and', add_quote=False):
ret = '"{}"'.format(ret) ret = '"{}"'.format(ret)
return ret return ret
else: else:
if isinstance(lst, tuple):
lst = list(lst)
lst.sort() lst.sort()
lst_ = [] lst_ = []
for l in lst[:-1]: for l in lst[:-1]:

View File

@ -20,13 +20,15 @@
# ____________________________________________________________ # ____________________________________________________________
import re import re
from types import FunctionType from types import FunctionType
from typing import FrozenSet, Callable, Tuple, Set, Optional, Union, Any, List
import weakref import weakref
from inspect import signature from inspect import signature
from itertools import chain from itertools import chain
from ..i18n import _ from ..i18n import _
from ..setting import undefined from ..setting import undefined, Settings
from ..value import Values
from ..error import ConfigError, display_list from ..error import ConfigError, display_list
from ..function import Params, ParamContext, ParamOption, ParamIndex from ..function import Params, ParamContext, ParamOption, ParamIndex
@ -38,18 +40,14 @@ NAME_REGEXP = re.compile(r'^[a-zA-Z][a-zA-Z\d_-]*$')
def valid_name(name): def valid_name(name):
"""an option's name is a str and does not start with 'impl' or 'cfgimpl'
and name is not a function name"""
if not isinstance(name, str): if not isinstance(name, str):
return False return False
return re.match(NAME_REGEXP, name) is not None and \ return re.match(NAME_REGEXP, name) is not None
not name.startswith('impl_') and \
not name.startswith('cfgimpl_')
#____________________________________________________________ #____________________________________________________________
# #
class Base(object): class Base:
"""Base use by all *Option* classes (Option, OptionDescription, SymLinkOption, ...) """Base use by all *Option* classes (Option, OptionDescription, SymLinkOption, ...)
""" """
__slots__ = ('_name', __slots__ = ('_name',
@ -70,13 +68,13 @@ class Base(object):
) )
def __init__(self, def __init__(self,
name, name: str,
doc, doc: str,
requires=None, requires=None,
properties=None, properties=None,
is_multi=False): is_multi: bool=False) -> None:
if not valid_name(name): if not valid_name(name):
raise ValueError(_('invalid name: "{0}" for option').format(name)) raise ValueError(_('"{0}" is an invalid name for an option').format(name))
if requires is not None: if requires is not None:
calc_properties, requires = validate_requires_arg(self, calc_properties, requires = validate_requires_arg(self,
is_multi, is_multi,
@ -90,12 +88,16 @@ class Base(object):
if isinstance(properties, tuple): if isinstance(properties, tuple):
properties = frozenset(properties) properties = frozenset(properties)
if is_multi and 'empty' not in properties: if is_multi and 'empty' not in properties:
# if option is a multi, it cannot be "empty" (None not allowed in the list)
# "empty" is removed for slave's option
properties = properties | {'empty'} properties = properties | {'empty'}
if not isinstance(properties, frozenset): if not isinstance(properties, frozenset):
raise TypeError(_('invalid properties type {0} for {1},' raise TypeError(_('invalid properties type {0} for {1},'
' must be a frozenset').format(type(properties), ' must be a frozenset').format(type(properties),
name)) name))
self.validate_properties(name, calc_properties, properties) self.validate_properties(name,
calc_properties,
properties)
_setattr = object.__setattr__ _setattr = object.__setattr__
_setattr(self, '_name', name) _setattr(self, '_name', name)
_setattr(self, '_informations', {'doc': doc}) _setattr(self, '_informations', {'doc': doc})
@ -106,15 +108,18 @@ class Base(object):
if properties: if properties:
_setattr(self, '_properties', properties) _setattr(self, '_properties', properties)
def validate_properties(self, name, calc_properties, properties): def validate_properties(self,
name: str,
calc_properties: FrozenSet[str],
properties: FrozenSet[str]) -> None:
set_forbidden_properties = calc_properties & properties set_forbidden_properties = calc_properties & properties
if set_forbidden_properties != frozenset(): if set_forbidden_properties != frozenset():
raise ValueError(_('conflict: properties already set in requirement {0} for {1}' raise ValueError(_('conflict: properties already set in requirement {0} for {1}'
'').format(display_list(list(set_forbidden_properties)), '').format(display_list(set_forbidden_properties),
name)) name))
def _get_function_args(self, def _get_function_args(self,
function): function: Callable) -> Tuple[Set[str], Set[str], bool, bool]:
args = set() args = set()
kwargs = set() kwargs = set()
positional = False positional = False
@ -131,8 +136,8 @@ class Base(object):
return args, kwargs, positional, keyword return args, kwargs, positional, keyword
def _get_parameters_args(self, def _get_parameters_args(self,
calculator_params, calculator_params: Optional[Params],
add_value): add_value: bool) -> Tuple[Set[str], Set[str]]:
args = set() args = set()
kwargs = set() kwargs = set()
# add value as first argument # add value as first argument
@ -149,18 +154,17 @@ class Base(object):
return args, kwargs return args, kwargs
def _build_calculator_params(self, def _build_calculator_params(self,
calculator, calculator: Callable,
calculator_params, calculator_params: Optional[Params],
type_, type_: str,
add_value=False): add_value: bool=False) -> Union[None, Params]:
""" """
:add_value: add value as first argument for validator :add_value: add value as first argument for validator
""" """
if not isinstance(calculator, FunctionType): assert isinstance(calculator, FunctionType), _('{0} must be a function').format(type_)
raise ValueError(_('{0} must be a function').format(type_))
if calculator_params is not None: if calculator_params is not None:
if not isinstance(calculator_params, Params): assert isinstance(calculator_params, Params), _('{0}_params must be a params'
raise ValueError(_('{0}_params must be a params').format(type_)) '').format(type_)
for param in chain(calculator_params.args, calculator_params.kwargs.values()): for param in chain(calculator_params.args, calculator_params.kwargs.values()):
if isinstance(param, ParamContext): if isinstance(param, ParamContext):
self._has_calc_context = True self._has_calc_context = True
@ -229,34 +233,28 @@ class Base(object):
return calculator_params return calculator_params
def impl_has_dependency(self, def impl_has_dependency(self,
self_is_dep=True): self_is_dep: bool=True) -> bool:
if self_is_dep is True: if self_is_dep is True:
#if self.impl_is_master_slaves():
# return True
return getattr(self, '_has_dependency', False) return getattr(self, '_has_dependency', False)
else: return hasattr(self, '_dependencies')
return hasattr(self, '_dependencies')
def _get_dependencies(self, def _get_dependencies(self,
context): context_od) -> Set[str]:
if context:
od = context.cfgimpl_get_description()
ret = set(getattr(self, '_dependencies', STATIC_TUPLE)) ret = set(getattr(self, '_dependencies', STATIC_TUPLE))
if context and hasattr(od, '_dependencies'): if context_od and hasattr(context_od, '_dependencies'):
return set(od._dependencies) | ret # if context is set in options, add those options
else: return set(context_od._dependencies) | ret
return ret return ret
def _add_dependency(self, def _add_dependency(self,
option): option) -> None:
options = self._get_dependencies(None) options = self._get_dependencies(None)
options.add(weakref.ref(option)) options.add(weakref.ref(option))
self._dependencies = tuple(options) self._dependencies = tuple(options)
def _impl_set_callback(self, def _impl_set_callback(self,
callback, callback: Callable,
callback_params=None): callback_params: Optional[Params]=None) -> None:
if callback is None and callback_params is not None: if callback is None and callback_params is not None:
raise ValueError(_("params defined for a callback function but " raise ValueError(_("params defined for a callback function but "
"no callback defined" "no callback defined"
@ -276,22 +274,16 @@ class Base(object):
val_call = (callback, callback_params) val_call = (callback, callback_params)
self._val_call = (val, val_call) self._val_call = (val, val_call)
def impl_is_optiondescription(self): def impl_is_optiondescription(self) -> bool:
return False return False
def impl_is_dynoptiondescription(self): def impl_is_dynoptiondescription(self) -> bool:
return False return False
def impl_getname(self): def impl_getname(self) -> str:
return self._name return self._name
def impl_is_readonly(self): def _set_readonly(self) -> None:
return not isinstance(getattr(self, '_informations', dict()), dict)
def impl_getproperties(self):
return getattr(self, '_properties', frozenset())
def _set_readonly(self):
if not self.impl_is_readonly(): if not self.impl_is_readonly():
_setattr = object.__setattr__ _setattr = object.__setattr__
dico = self._informations dico = self._informations
@ -305,11 +297,17 @@ class Base(object):
if extra is not None: if extra is not None:
_setattr(self, '_extra', tuple([tuple(extra.keys()), tuple(extra.values())])) _setattr(self, '_extra', tuple([tuple(extra.keys()), tuple(extra.values())]))
def impl_is_readonly(self) -> str:
return not isinstance(getattr(self, '_informations', dict()), dict)
def impl_getproperties(self) -> FrozenSet[str]:
return getattr(self, '_properties', frozenset())
def _setsubdyn(self, def _setsubdyn(self,
subdyn): subdyn) -> None:
self._subdyn = weakref.ref(subdyn) self._subdyn = weakref.ref(subdyn)
def issubdyn(self): def issubdyn(self) -> bool:
return getattr(self, '_subdyn', None) is not None return getattr(self, '_subdyn', None) is not None
def getsubdyn(self): def getsubdyn(self):
@ -331,20 +329,17 @@ class Base(object):
# ____________________________________________________________ # ____________________________________________________________
# information # information
def impl_get_information(self, def impl_get_information(self,
key, key: str,
default=undefined): default: Any=undefined) -> Any:
"""retrieves one information's item """retrieves one information's item
:param key: the item string (ex: "help") :param key: the item string (ex: "help")
""" """
def _is_string(infos):
return isinstance(infos, str)
dico = self._informations dico = self._informations
if isinstance(dico, tuple): if isinstance(dico, tuple):
if key in dico[0]: if key in dico[0]:
return dico[1][dico[0].index(key)] return dico[1][dico[0].index(key)]
elif _is_string(dico): elif isinstance(dico, str):
if key == 'doc': if key == 'doc':
return dico return dico
elif isinstance(dico, dict): elif isinstance(dico, dict):
@ -356,8 +351,8 @@ class Base(object):
key)) key))
def impl_set_information(self, def impl_set_information(self,
key, key: str,
value): value: Any) -> None:
"""updates the information's attribute """updates the information's attribute
(which is a dictionary) (which is a dictionary)
@ -384,47 +379,33 @@ class BaseOption(Base):
raise NotImplementedError() raise NotImplementedError()
def __setattr__(self, def __setattr__(self,
name, name: str,
value): value: Any) -> Any:
"""set once and only once some attributes in the option, """set once and only once some attributes in the option,
like `_name`. `_name` cannot be changed one the option and like `_name`. `_name` cannot be changed once the option is
pushed in the :class:`tiramisu.option.OptionDescription`. pushed in the :class:`tiramisu.option.OptionDescription`.
if the attribute `_readonly` is set to `True`, the option is if the attribute `_readonly` is set to `True`, the option is
"frozen" (which has noting to do with the high level "freeze" "frozen" (which has nothing to do with the high level "freeze"
propertie or "read_only" property) propertie or "read_only" property)
""" """
if name != '_option' and \ # never change _name in an option or attribute when object is readonly
not isinstance(value, tuple): if self.impl_is_readonly():
is_readonly = False raise AttributeError(_('"{}" ({}) object attribute "{}" is'
# never change _name dans _opt ' read-only').format(self.__class__.__name__,
if name == '_name': self.impl_get_display_name(),
if self.impl_getname() is not None: name))
#so _name is already set
is_readonly = True
elif name != '_readonly':
is_readonly = self.impl_is_readonly()
if is_readonly and (name != '_path' or value != getattr(self, '_path', value)):
raise AttributeError(_('"{}" ({}) object attribute "{}" is'
' read-only').format(self.__class__.__name__,
self.impl_get_display_name(),
name))
super(BaseOption, self).__setattr__(name, value) super(BaseOption, self).__setattr__(name, value)
def impl_getpath(self): def impl_getpath(self) -> str:
return self._path return self._path
def impl_has_callback(self): def impl_has_callback(self) -> bool:
"to know if a callback has been defined or not" "to know if a callback has been defined or not"
return self.impl_get_callback()[0] is not None return self.impl_get_callback()[0] is not None
def _impl_valid_string(self,
value):
if not isinstance(value, str):
raise ValueError(_('invalid string'))
def impl_get_display_name(self, def impl_get_display_name(self,
dyn_name=None): dyn_name: Base=None) -> str:
name = self.impl_get_information('doc') name = self.impl_get_information('doc')
if name is None or name == '': if name is None or name == '':
if dyn_name is not None: if dyn_name is not None:
@ -434,23 +415,23 @@ class BaseOption(Base):
return name return name
def reset_cache(self, def reset_cache(self,
path, path: str,
values, values: Values,
settings, settings: Settings,
resetted_opts): resetted_opts: List[Base]) -> None:
settings._p_.delcache(path) settings._p_.delcache(path)
settings._pp_.delcache(path) settings._pp_.delcache(path)
if not self.impl_is_optiondescription(): if not self.impl_is_optiondescription():
values._p_.delcache(path) values._p_.delcache(path)
def impl_is_symlinkoption(self): def impl_is_symlinkoption(self) -> bool:
return False return False
def validate_requires_arg(new_option, def validate_requires_arg(new_option: BaseOption,
multi, multi: bool,
requires, requires: List[dict],
name): name: str) -> Tuple[FrozenSet, Tuple]:
"""check malformed requirements """check malformed requirements
and tranform dict to internal tuple and tranform dict to internal tuple

View File

@ -34,7 +34,8 @@ class BroadcastOption(Option):
value, value,
*args, *args,
**kwargs): **kwargs):
self._impl_valid_string(value) if not isinstance(value, str):
raise ValueError(_('invalid string'))
if value.count('.') != 3: if value.count('.') != 3:
raise ValueError() raise ValueError()
for val in value.split('.'): for val in value.split('.'):

View File

@ -33,7 +33,8 @@ class DateOption(Option):
value, value,
*args, *args,
**kwargs): **kwargs):
self._impl_valid_string(value) if not isinstance(value, str):
raise ValueError(_('invalid string'))
try: try:
datetime.strptime(value, "%Y-%m-%d") datetime.strptime(value, "%Y-%m-%d")
except ValueError: except ValueError:

View File

@ -101,8 +101,6 @@ class DomainnameOption(Option):
value, value,
*args, *args,
**kwargs): **kwargs):
self._impl_valid_string(value)
def _valid_length(val): def _valid_length(val):
if len(val) < 1: if len(val) < 1:
raise ValueError(_("invalid length (min 1)")) raise ValueError(_("invalid length (min 1)"))
@ -110,6 +108,8 @@ class DomainnameOption(Option):
raise ValueError(_("invalid length (max {0})" raise ValueError(_("invalid length (max {0})"
"").format(part_name_length)) "").format(part_name_length))
if not isinstance(value, str):
raise ValueError(_('invalid string'))
if self.impl_get_extra('_allow_ip') is True: if self.impl_get_extra('_allow_ip') is True:
try: try:
IP('{0}/32'.format(value)) IP('{0}/32'.format(value))

View File

@ -68,7 +68,8 @@ class IPOption(Option):
**kwargs): **kwargs):
# sometimes an ip term starts with a zero # sometimes an ip term starts with a zero
# but this does not fit in some case, for example bind does not like it # but this does not fit in some case, for example bind does not like it
self._impl_valid_string(value) if not isinstance(value, str):
raise ValueError(_('invalid string'))
if value.count('.') != 3: if value.count('.') != 3:
raise ValueError() raise ValueError()
for val in value.split('.'): for val in value.split('.'):

View File

@ -32,10 +32,11 @@ class NetmaskOption(Option):
_display_name = _('netmask address') _display_name = _('netmask address')
def _validate(self, def _validate(self,
value, value: str,
*args, *args,
**kwargs): **kwargs) -> None:
self._impl_valid_string(value) if not isinstance(value, str):
raise ValueError(_('invalid string'))
if value.count('.') != 3: if value.count('.') != 3:
raise ValueError() raise ValueError()
for val in value.split('.'): for val in value.split('.'):

View File

@ -34,7 +34,8 @@ class NetworkOption(Option):
value, value,
*args, *args,
**kwargs): **kwargs):
self._impl_valid_string(value) if not isinstance(value, str):
raise ValueError(_('invalid string'))
if value.count('.') != 3: if value.count('.') != 3:
raise ValueError() raise ValueError()
for val in value.split('.'): for val in value.split('.'):

View File

@ -729,7 +729,8 @@ class RegexpOption(Option):
value, value,
*args, *args,
**kwargs): **kwargs):
self._impl_valid_string(value) if not isinstance(value, str):
raise ValueError(_('invalid string'))
match = self._regexp.search(value) match = self._regexp.search(value)
if not match: if not match:
raise ValueError() raise ValueError()

View File

@ -33,4 +33,5 @@ class PasswordOption(Option):
value, value,
*args, *args,
**kwargs): **kwargs):
self._impl_valid_string(value) if not isinstance(value, str):
raise ValueError(_('invalid string'))

View File

@ -102,7 +102,8 @@ class PortOption(Option):
**kwargs): **kwargs):
if isinstance(value, int): if isinstance(value, int):
value = str(value) value = str(value)
self._impl_valid_string(value) if not isinstance(value, str):
raise ValueError(_('invalid string'))
if self.impl_get_extra('_allow_range') and ":" in str(value): if self.impl_get_extra('_allow_range') and ":" in str(value):
value = str(value).split(':') value = str(value).split(':')
if len(value) != 2: if len(value) != 2:

View File

@ -36,7 +36,8 @@ class URLOption(DomainnameOption):
value, value,
*args, *args,
**kwargs): **kwargs):
self._impl_valid_string(value) if not isinstance(value, str):
raise ValueError(_('invalid string'))
match = self.proto_re.search(value) match = self.proto_re.search(value)
if not match: if not match:
raise ValueError(_('must start with http:// or ' raise ValueError(_('must start with http:// or '