13c99d593c5f9b5b090ce7cf6b101201f816560d
[tiramisu.git] / tiramisu / option / baseoption.py
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2014-2017 Team tiramisu (see AUTHORS for all contributors)
3 #
4 # This program is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Lesser General Public License as published by the
6 # Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
12 # details.
13 #
14 # You should have received a copy of the GNU Lesser General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 #
17 # The original `Config` design model is unproudly borrowed from
18 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
19 # the whole pypy projet is under MIT licence
20 # ____________________________________________________________
21 import re
22 from types import FunctionType
23 import warnings
24 import sys
25 from inspect import getargspec
26
27 from ..i18n import _
28 from ..setting import log, undefined, debug, groups
29 from ..autolib import carry_out_calculation
30 from ..error import (ConfigError, ValueWarning, PropertiesOptionError,
31                      display_list)
32 from ..storage import get_storages_option
33 from . import MasterSlaves
34
35 if sys.version_info[0] >= 3:  # pragma: no cover
36     xrange = range
37
38
39 StorageBase = get_storages_option('base')
40 submulti = 2
41 name_regexp = re.compile(r'^[a-z][a-zA-Z\d\-_]*$')
42 forbidden_names = frozenset(['iter_all', 'iter_group', 'find', 'find_first',
43                              'make_dict', 'unwrap_from_path', 'read_only',
44                              'read_write', 'getowner', 'set_contexts'])
45 allowed_const_list = ['_cons_not_equal']
46
47
48 def valid_name(name):
49     "an option's name is a str and does not start with 'impl' or 'cfgimpl'"
50     if not isinstance(name, str):
51         return False
52     if re.match(name_regexp, name) is not None and \
53             name not in forbidden_names and \
54             not name.startswith('impl_') and \
55             not name.startswith('cfgimpl_'):
56         return True
57     else:
58         return False
59
60
61 def validate_callback(callback, callback_params, type_, callbackoption):
62     if not isinstance(callback, FunctionType):
63         raise ValueError(_('{0} must be a function').format(type_))
64     if callback_params is not None:
65         if not isinstance(callback_params, dict):
66             raise ValueError(_('{0}_params must be a dict').format(type_))
67         for key, callbacks in callback_params.items():
68             if key != '' and len(callbacks) != 1:
69                 raise ValueError(_("{0}_params with key {1} mustn't have "
70                                    "length different to 1").format(type_,
71                                                                    key))
72             if not isinstance(callbacks, tuple):
73                 raise ValueError(_('{0}_params must be tuple for key "{1}"'
74                                   ).format(type_, key))
75             for callbk in callbacks:
76                 if isinstance(callbk, tuple):
77                     if len(callbk) == 1:
78                         if callbk not in  ((None,), ('index',)):
79                             raise ValueError(_('{0}_params with length of '
80                                                'tuple as 1 must only have '
81                                                'None as first value'))
82                     elif len(callbk) != 2:
83                         raise ValueError(_('{0}_params must only have 1 or 2 '
84                                            'as length'))
85                     else:
86                         option, force_permissive = callbk
87                         if not isinstance(option, Option) and not \
88                                 isinstance(option, SymLinkOption):
89                             raise ValueError(_('{}_params must have an option'
90                                                ' not a {} for first argument'
91                                               ).format(type_, type(option)))
92                         if force_permissive not in [True, False]:
93                             raise ValueError(_('{}_params must have a boolean'
94                                                ' not a {} for second argument'
95                                               ).format(type_, type(
96                                                   force_permissive)))
97                         if isinstance(option, SymLinkOption):
98                             cur_opt = option._impl_getopt()
99                         else:
100                             cur_opt = option
101                         if cur_opt != callbackoption:
102                             if not getattr(cur_opt, '_dependencies', None):
103                                 options = []
104                             else:
105                                 options = list(cur_opt._dependencies)
106                             options.append(callbackoption)
107                             cur_opt._dependencies = tuple(options)
108 #____________________________________________________________
109 #
110
111
112 class Base(StorageBase):
113     __slots__ = tuple()
114
115     def __init__(self, name, doc, default=None, default_multi=None,
116                  requires=None, multi=False, unique=undefined, callback=None,
117                  callback_params=None, validator=None, validator_params=None,
118                  properties=None, warnings_only=False, extra=None,
119                  allow_empty_list=undefined, session=None):
120         if not valid_name(name):
121             raise ValueError(_("invalid name: {0} for option").format(name))
122         if not multi and default_multi is not None:
123             raise ValueError(_("default_multi is set whereas multi is False"
124                              " in option: {0}").format(name))
125         if multi is True:
126             is_multi = True
127             _multi = 0
128         elif multi is False:
129             is_multi = False
130             _multi = 1
131         elif multi is submulti:
132             is_multi = True
133             _multi = submulti
134         else:
135             raise ValueError(_('invalid multi value'))
136         if unique != undefined and not isinstance(unique, bool):
137             raise ValueError(_('unique must be a boolean'))
138         if not is_multi and unique is True:
139             raise ValueError(_('unique must be set only with multi value'))
140         if requires is not None:
141             calc_properties, requires = validate_requires_arg(self, is_multi,
142                                                               requires, name)
143         else:
144             calc_properties = frozenset()
145             requires = undefined
146         if properties is None:
147             properties = tuple()
148         if not isinstance(properties, tuple):
149             raise TypeError(_('invalid properties type {0} for {1},'
150                               ' must be a tuple').format(
151                                   type(properties),
152                                   name))
153         if validator is not None:
154             if multi:  # and validator_params is None:
155                 validator_params = self._build_validator_params(validator, validator_params)
156
157             validate_callback(validator, validator_params, 'validator', self)
158             self._set_validator(validator, validator_params)
159             self._set_has_dependency()
160         if calc_properties != frozenset([]) and properties is not tuple():
161             set_forbidden_properties = calc_properties & set(properties)
162             if set_forbidden_properties != frozenset():
163                 raise ValueError('conflict: properties already set in '
164                                  'requirement {0}'.format(
165                                      list(set_forbidden_properties)))
166         if session is None:
167             session = self.getsession()
168         StorageBase.__init__(self, name, _multi, warnings_only, doc, extra,
169                              calc_properties, requires, properties,
170                              allow_empty_list, unique, session=session)
171         if multi is not False and default is None:
172             default = []
173         err = self.impl_validate(default, is_multi=is_multi)
174         if err:
175             raise err
176         self._set_default_values(default, default_multi, is_multi)
177         ##callback is False in optiondescription
178         if callback is not False:
179             self.impl_set_callback(callback, callback_params, _init=True)
180         self.commit(session)
181
182     def _build_validator_params(self, validator, validator_params):
183         func_args = getargspec(validator)
184         defaults = func_args.defaults
185         if defaults is None:
186             defaults = []
187         args = func_args.args[0:len(func_args.args)-len(defaults)]
188         if validator_params is not None:
189             kwargs = validator_params.keys()
190             if '' in kwargs:
191                 kwargs.remove('')
192             for kwarg in kwargs:
193                 if kwarg in args:
194                     args = args[0:args.index(kwarg)]
195             len_args = len(validator_params.get('', []))
196             if len_args != 0 and len(args) >= len_args:
197                 args = args[0:len(args)-len_args]
198         if len(args) >= 2:
199             if validator_params is not None and '' in validator_params:
200                 params = list(validator_params[''])
201                 params.append((self, False))
202                 validator_params[''] = tuple(params)
203             else:
204                 if validator_params is None:
205                     validator_params = {}
206                 validator_params[''] = ((self, False),)
207         if len(args) == 3 and args[2] not in validator_params:
208             params = list(validator_params[''])
209             params.append(('index',))
210             validator_params[''] = tuple(params)
211         return validator_params
212
213     def _set_has_dependency(self):
214         if not isinstance(self, SymLinkOption):
215             self._has_dependency = True
216
217     def impl_has_dependency(self):
218         return getattr(self, '_has_dependency', False)
219
220     def impl_set_callback(self, callback, callback_params=None, _init=False):
221         if callback is None and callback_params is not None:
222                 raise ValueError(_("params defined for a callback function but "
223                                    "no callback defined"
224                                    " yet for option {0}").format(
225                                        self.impl_getname()))
226         if not _init and self.impl_get_callback()[0] is not None:
227             raise ConfigError(_("a callback is already set for {0}, "
228                                 "cannot set another one's").format(self.impl_getname()))
229         self._validate_callback(callback, callback_params)
230         if callback is not None:
231             validate_callback(callback, callback_params, 'callback', self)
232             self._set_callback(callback, callback_params)
233
234     def impl_is_optiondescription(self):
235         return self.__class__.__name__ in ['OptionDescription',
236                                            'DynOptionDescription',
237                                            'SynDynOptionDescription']
238
239     def impl_is_dynoptiondescription(self):
240         return self.__class__.__name__ in ['DynOptionDescription',
241                                            'SynDynOptionDescription']
242
243
244 class BaseOption(Base):
245     """This abstract base class stands for attribute access
246     in options that have to be set only once, it is of course done in the
247     __setattr__ method
248     """
249     __slots__ = tuple()
250
251     # ____________________________________________________________
252     # serialize object
253     def _impl_convert_dependencies(self, descr, load=False):
254         """export of the requires during the serialization process
255
256         :type descr: :class:`tiramisu.option.OptionDescription`
257         :param load: `True` if we are at the init of the option description
258         :type load: bool
259         """
260         if not load and getattr(self, '_dependencies', None) is None:
261             self._state_dependencies = None
262         elif load and self._state_dependencies is None:
263             del(self._state_dependencies)
264         else:
265             if load:
266                 self._dependencies = []
267                 for dependency in self._state_dependencies:
268                     option = descr.impl_get_opt_by_path(dependency)
269                     if option.impl_is_optiondescription() and \
270                             option.impl_get_group_type() == groups.master:
271                         master_path = dependency + '.' + dependency.split('.')[-1]
272                         option = descr.impl_get_opt_by_path(master_path).impl_get_master_slaves()
273                     self._dependencies.append(option)
274                 del(self._state_dependencies)
275             else:
276                 self._state_dependencies = []
277                 for dependency in self._dependencies:
278                     if isinstance(dependency, MasterSlaves):
279                         self._state_dependencies.append('.'.join(descr.impl_get_path_by_opt(dependency._p_.master).split('.')[:-1]))
280                     else:
281                         self._state_dependencies.append(descr.impl_get_path_by_opt(dependency))
282
283     def _impl_convert_requires(self, descr, load=False):
284         """export of the requires during the serialization process
285
286         :type descr: :class:`tiramisu.option.OptionDescription`
287         :param load: `True` if we are at the init of the option description
288         :type load: bool
289         """
290         if not load and self.impl_getrequires() == []:
291             self._state_requires = None
292         elif load and self._state_requires is None:
293             del(self._state_requires)
294         else:
295             if load:
296                 _requires = self._state_requires
297             else:
298                 _requires = self.impl_getrequires()
299             new_value = []
300             for requires in _requires:
301                 new_requires = []
302                 new_req = []
303                 for require in requires:
304                     for req in require[0]:
305                         if load:
306                             new_req.append([(descr.impl_get_opt_by_path(req[0]), req[1])])
307                         else:
308                             new_req.append([(descr.impl_get_path_by_opt(req[0]), req[1])])
309                     new_req.extend(require[1:])
310                     new_requires.append(tuple(new_req))
311                 new_value.append(tuple(new_requires))
312             if load:
313                 del(self._state_requires)
314                 if new_value != []:
315                     self._requires = tuple(new_value)
316             else:
317                 self._state_requires = tuple(new_value)
318
319     # serialize
320     def _impl_getstate(self, descr):
321         """the under the hood stuff that need to be done
322         before the serialization.
323
324         :param descr: the parent :class:`tiramisu.option.OptionDescription`
325         """
326         self._stated = True
327         for func in dir(self):
328             if func.startswith('_impl_convert_'):
329                 getattr(self, func)(descr)
330
331     def __getstate__(self, stated=True):
332         """special method to enable the serialization with pickle
333         Usualy, a `__getstate__` method does'nt need any parameter,
334         but somme under the hood stuff need to be done before this action
335
336         :parameter stated: if stated is `True`, the serialization protocol
337                            can be performed, not ready yet otherwise
338         :parameter type: bool
339         """
340         try:
341             self._stated
342         except AttributeError:
343             raise SystemError(_('cannot serialize Option, '
344                                 'only in OptionDescription'))
345         if isinstance(self, SymLinkOption):
346             slots = frozenset(['_name', '_state_opt', '_stated'])
347         else:
348             slots = self._impl_getattributes()
349             slots -= frozenset(['_cache_paths', '_cache_consistencies',
350                                 '__weakref__'])
351         states = {}
352         for slot in slots:
353             # remove variable if save variable converted
354             # in _state_xxxx variable
355             if '_state' + slot not in slots:
356                 try:
357                     if slot.startswith('_state'):
358                         states[slot] = getattr(self, slot)
359                         # remove _state_xxx variable
360                         self.__delattr__(slot)
361                     else:
362                         states[slot] = getattr(self, slot)
363                 except AttributeError:
364                     pass
365         if not stated:
366             del(states['_stated'])
367         return states
368
369     # unserialize
370     def _impl_setstate(self, descr):
371         """the under the hood stuff that need to be done
372         before the serialization.
373
374         :type descr: :class:`tiramisu.option.OptionDescription`
375         """
376         for func in dir(self):
377             if func.startswith('_impl_convert_'):
378                 getattr(self, func)(descr, load=True)
379         try:
380             del(self._stated)
381         except AttributeError:
382             pass
383
384     def __setstate__(self, state):
385         """special method that enables us to serialize (pickle)
386
387         Usualy, a `__setstate__` method does'nt need any parameter,
388         but somme under the hood stuff need to be done before this action
389
390         :parameter state: a dict is passed to the loads, it is the attributes
391                           of the options object
392         :type state: dict
393         """
394         for key, value in state.items():
395             setattr(self, key, value)
396
397     def __setattr__(self, name, value):
398         """set once and only once some attributes in the option,
399         like `_name`. `_name` cannot be changed one the option and
400         pushed in the :class:`tiramisu.option.OptionDescription`.
401
402         if the attribute `_readonly` is set to `True`, the option is
403         "frozen" (which has noting to do with the high level "freeze"
404         propertie or "read_only" property)
405         """
406         if name != '_option' and \
407                 not isinstance(value, tuple) and \
408                 not name.startswith('_state') and \
409                 not name == '_sa_instance_state':
410             is_readonly = False
411             # never change _name dans _opt
412             if name == '_name':
413                 try:
414                     if self.impl_getname() is not None:
415                         #so _name is already set
416                         is_readonly = True
417                 except (KeyError, AttributeError):
418                     pass
419             elif name == '_opt':
420                 pass
421             elif name != '_readonly':
422                 is_readonly = self.impl_is_readonly()
423             if is_readonly:
424                 raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
425                                        " read-only").format(
426                                            self.__class__.__name__,
427                                            self,
428                                            #self.impl_getname(),
429                                            name))
430         super(BaseOption, self).__setattr__(name, value)
431
432     def impl_getpath(self, context):
433         return context.cfgimpl_get_description().impl_get_path_by_opt(self)
434
435     def impl_has_callback(self):
436         "to know if a callback has been defined or not"
437         return self.impl_get_callback()[0] is not None
438
439     def _is_subdyn(self):
440         return getattr(self, '_subdyn', None) is not None
441
442     def _impl_valid_unicode(self, value):
443         if sys.version_info[0] >= 3:  # pragma: no cover
444             if not isinstance(value, str):
445                 return ValueError(_('invalid string'))
446         else:
447             if not isinstance(value, unicode) and not isinstance(value, str):
448                 return ValueError(_('invalid unicode or string'))
449
450     def impl_get_display_name(self, dyn_name=None):
451         name = self.impl_getdoc()
452         if name is None or name == '':
453             if dyn_name is not None:
454                 name = dyn_name
455             else:
456                 name = self.impl_getname()
457         if isinstance(name, unicode):
458             name = name.encode('utf8')
459         return name
460
461     def reset_cache(self, opt, obj, type_):
462         context = obj._getcontext()
463         path = self.impl_getpath(context)
464         obj._p_.delcache(path)
465         context.cfgimpl_reset_cache(only=(type_,),
466                                     opt=self,
467                                     path=path)
468
469
470 class OnlyOption(BaseOption):
471     __slots__ = tuple()
472
473
474 class Option(OnlyOption):
475     """
476     Abstract base class for configuration option's.
477
478     Reminder: an Option object is **not** a container for the value.
479     """
480     __slots__ = tuple()
481     _empty = ''
482
483     def _launch_consistency(self, current_opt, func, option, value, context,
484                             index, submulti_index, opts, warnings_only,
485                             transitive):
486         """Launch consistency now
487
488         :param func: function name, this name should start with _cons_
489         :type func: `str`
490         :param option: option that value is changing
491         :type option: `tiramisu.option.Option`
492         :param value: new value of this option
493         :param context: Config's context, if None, check default value instead
494         :type context: `tiramisu.config.Config`
495         :param index: only for multi option, consistency should be launch for
496                       specified index
497         :type index: `int`
498         :param opts: all options concerne by this consistency
499         :type opts: `list` of `tiramisu.option.Option`
500         :param warnings_only: specific raise error for warning
501         :type warnings_only: `boolean`
502         :param transitive: propertyerror is transitive
503         :type transitive: `boolean`
504         """
505         if context is not undefined:
506             descr = context.cfgimpl_get_description()
507
508         all_cons_vals = []
509         all_cons_opts = []
510         val_consistencies = True
511         for opt in opts:
512             if (isinstance(opt, DynSymLinkOption) and option._dyn == opt._dyn) or \
513                     option == opt:
514                 # option is current option
515                 # we have already value, so use it
516                 all_cons_vals.append(value)
517                 all_cons_opts.append(opt)
518             else:
519                 #if context, calculate value, otherwise get default value
520                 path = None
521                 is_multi = opt.impl_is_multi() and not opt.impl_is_master_slaves()
522                 if context is not undefined:
523                     if isinstance(opt, DynSymLinkOption):
524                         path = opt.impl_getpath(context)
525                     else:
526                         path = descr.impl_get_path_by_opt(opt)
527                     if is_multi:
528                         _index = None
529                     else:
530                         _index = index
531                     opt_value = context.getattr(path, validate=False,
532                                                 index=_index,
533                                                 force_permissive=True,
534                                                 returns_raise=True)
535                     if isinstance(opt_value, Exception):
536                         if isinstance(opt_value, PropertiesOptionError):
537                             if debug:  # pragma: no cover
538                                 log.debug('propertyerror in _launch_consistency: {0}'.format(opt_value))
539                             if transitive:
540                                 opt_value.set_orig_opt(option)
541                                 return opt_value
542                             else:
543                                 opt_value = None
544                         else:
545                             return opt_value
546                 elif index is None:
547                     opt_value = opt.impl_getdefault()
548                 else:
549                     opt_value = opt.impl_getdefault()[index]
550
551                 if self.impl_is_multi() and index is None:
552                     # only check propertyerror for master/slaves is transitive
553                     val_consistencies = False
554                 if is_multi and isinstance(opt_value, list):
555                     all_cons_vals.extend(opt_value)
556                     for len_ in xrange(len(opt_value)):
557                         all_cons_opts.append(opt)
558                 else:
559                     all_cons_vals.append(opt_value)
560                     all_cons_opts.append(opt)
561
562         if val_consistencies:
563             err = getattr(self, func)(current_opt, all_cons_opts, all_cons_vals, warnings_only)
564             if err:
565                 if warnings_only:
566                     msg = _('attention, "{0}" could be an invalid {1} for "{2}", {3}').format(
567                         value, self._display_name, current_opt.impl_get_display_name(), err)
568                     warnings.warn_explicit(ValueWarning(msg, self),
569                                            ValueWarning,
570                                            self.__class__.__name__, 0)
571                 else:
572                     return err
573
574     def impl_validate(self, value, context=undefined, validate=True,
575                       force_index=None, force_submulti_index=None,
576                       current_opt=undefined, is_multi=None,
577                       display_error=True, display_warnings=True, multi=None,
578                       setting_properties=undefined):
579         """
580         :param value: the option's value
581         :param context: Config's context
582         :type context: :class:`tiramisu.config.Config`
583         :param validate: if true enables ``self._validator`` validation
584         :type validate: boolean
585         :param force_index: if multi, value has to be a list
586                             not if force_index is not None
587         :type force_index: integer
588         :param force_submulti_index: if submulti, value has to be a list
589                                      not if force_submulti_index is not None
590         :type force_submulti_index: integer
591         """
592         if not validate:
593             return
594         if current_opt is undefined:
595             current_opt = self
596
597         if display_warnings and setting_properties is undefined and context is not undefined:
598             setting_properties = context.cfgimpl_get_settings()._getproperties(read_write=False)
599         display_warnings = display_warnings and (setting_properties is undefined or 'warnings' in setting_properties)
600         def _is_not_unique(value):
601             if display_error and self.impl_is_unique() and len(set(value)) != len(value):
602                 for idx, val in enumerate(value):
603                     if val in value[idx+1:]:
604                         return ValueError(_('invalid value "{}", this value is already in "{}"').format(
605                                             val, self.impl_get_display_name()))
606
607         def calculation_validator(val, _index):
608             validator, validator_params = self.impl_get_validator()
609             if validator is not None:
610                 if validator_params != {}:
611                     validator_params_ = {}
612                     for val_param, values in validator_params.items():
613                         validator_params_[val_param] = values
614                     #inject value in calculation
615                     if '' in validator_params_:
616                         lst = list(validator_params_[''])
617                         lst.insert(0, val)
618                         validator_params_[''] = tuple(lst)
619                     else:
620                         validator_params_[''] = (val,)
621                 else:
622                     validator_params_ = {'': (val,)}
623                 # Raise ValueError if not valid
624                 value = carry_out_calculation(current_opt, context=context,
625                                               callback=validator,
626                                               callback_params=validator_params_,
627                                               index=_index,
628                                               is_validator=True)
629                 if isinstance(value, Exception):
630                     return value
631
632         def do_validation(_value, _index, submulti_index):
633             if _value is None:
634                 error = warning = None
635             else:
636                 if display_error:
637                     # option validation
638                     err = self._validate(_value, context, current_opt)
639                     if err:
640                         if debug:  # pragma: no cover
641                             log.debug('do_validation: value: {0}, index: {1}, '
642                                       'submulti_index: {2}'.format(_value, _index,
643                                                                    submulti_index),
644                                       exc_info=True)
645                         err_msg = '{0}'.format(err)
646                         if err_msg:
647                             msg = _('"{0}" is an invalid {1} for "{2}", {3}'
648                                     '').format(_value, self._display_name,
649                                                self.impl_get_display_name(), err_msg)
650                         else:
651                             msg = _('"{0}" is an invalid {1} for "{2}"'
652                                     '').format(_value, self._display_name,
653                                                self.impl_get_display_name())
654                         return ValueError(msg)
655                 error = None
656                 if ((display_error and not self._is_warnings_only()) or
657                         (display_warnings and self._is_warnings_only())):
658                     error = calculation_validator(_value, _index)
659                     if not error:
660                         error = self._second_level_validation(_value, self._is_warnings_only())
661                     if error:
662                         if debug:  # pragma: no cover
663                             log.debug(_('do_validation for {0}: error in value').format(
664                                 self.impl_getname()), exc_info=True)
665                         if self._is_warnings_only():
666                             msg = _('attention, "{0}" could be an invalid {1} for "{2}", {3}').format(
667                                 _value, self._display_name, self.impl_get_display_name(), error)
668                             warnings.warn_explicit(ValueWarning(msg, self),
669                                                    ValueWarning,
670                                                    self.__class__.__name__, 0)
671                             error = None
672             if error is None:
673                 # if context launch consistency validation
674                 #if context is not undefined:
675                 ret = self._valid_consistency(current_opt, _value, context,
676                                               _index, submulti_index, display_warnings,
677                                               display_error)
678                 if isinstance(ret, ValueError):
679                     error = ret
680                 elif ret:
681                     return ret
682             if error:
683                 err_msg = '{0}'.format(error)
684                 if err_msg:
685                     msg = _('"{0}" is an invalid {1} for "{2}", {3}'
686                             '').format(_value, self._display_name,
687                                        self.impl_get_display_name(), err_msg)
688                 else:
689                     msg = _('"{0}" is an invalid {1} for "{2}"'
690                             '').format(_value, self._display_name,
691                                        self.impl_get_display_name())
692                 return ValueError(msg)
693
694         if is_multi is None:
695             is_multi = self.impl_is_multi()
696
697         if not is_multi:
698             return do_validation(value, None, None)
699         elif force_index is not None:
700             if self.impl_is_submulti() and force_submulti_index is None:
701                 err = _is_not_unique(value)
702                 if err:
703                     return err
704                 if not isinstance(value, list):
705                     return ValueError(_('invalid value "{0}" for "{1}" which'
706                                         ' must be a list').format(
707                                            value, self.impl_get_display_name()))
708                 for idx, val in enumerate(value):
709                     if isinstance(val, list):
710                         return ValueError(_('invalid value "{}" for "{}" '
711                                             'which must not be a list').format(val,
712                                                                               self.impl_get_display_name()))
713                     err = do_validation(val, force_index, idx)
714                     if err:
715                         return err
716             else:
717                 if multi is not None and self.impl_is_unique() and value in multi:
718                     if not self.impl_is_submulti() and len(multi) - 1 >= force_index:
719                         lst = list(multi)
720                         lst.pop(force_index)
721                     else:
722                         lst = multi
723                     if value in lst:
724                         return ValueError(_('invalid value "{}", this value is already'
725                                             ' in "{}"').format(value,
726                                                                self.impl_get_display_name()))
727                 return do_validation(value, force_index, force_submulti_index)
728         elif not isinstance(value, list):
729             return ValueError(_('invalid value "{0}" for "{1}" which '
730                                 'must be a list').format(value,
731                                                          self.impl_getname()))
732         elif self.impl_is_submulti() and force_submulti_index is None:
733             for idx, val in enumerate(value):
734                 err = _is_not_unique(val)
735                 if err:
736                     return err
737                 if not isinstance(val, list):
738                     return ValueError(_('invalid value "{0}" for "{1}" '
739                                         'which must be a list of list'
740                                         '').format(val,
741                                                    self.impl_getname()))
742                 for slave_idx, slave_val in enumerate(val):
743                     err = do_validation(slave_val, idx, slave_idx)
744                     if err:
745                         return err
746         else:
747             err = _is_not_unique(value)
748             if err:
749                 return err
750             for idx, val in enumerate(value):
751                 err = do_validation(val, idx, force_submulti_index)
752                 if err:
753                     return err
754             return self._valid_consistency(current_opt, None, context,
755                                            None, None, display_warnings, display_error)
756
757     def impl_is_dynsymlinkoption(self):
758         return False
759
760     def impl_is_master_slaves(self, type_='both'):
761         """FIXME
762         """
763         master_slaves = self.impl_get_master_slaves()
764         if master_slaves is not None:
765             if type_ in ('both', 'master') and \
766                     master_slaves.is_master(self):
767                 return True
768             if type_ in ('both', 'slave') and \
769                     not master_slaves.is_master(self):
770                 return True
771         return False
772
773     def impl_get_master_slaves(self):
774         masterslaves = self._get_master_slave()
775         if masterslaves is None:
776             return None
777         if not isinstance(masterslaves, MasterSlaves):
778             return MasterSlaves(masterslaves)
779         return masterslaves
780
781     def impl_getdoc(self):
782         "accesses the Option's doc"
783         return self.impl_get_information('doc')
784
785     def _valid_consistencies(self, other_opts, init=True, func=None):
786         if self._is_subdyn():
787             dynod = self._impl_getsubdyn()
788         else:
789             dynod = None
790         if self.impl_is_submulti():
791             raise ConfigError(_('cannot add consistency with submulti option'))
792         is_multi = self.impl_is_multi()
793         for opt in other_opts:
794             if opt.impl_is_submulti():
795                 raise ConfigError(_('cannot add consistency with submulti option'))
796             if not isinstance(opt, Option):
797                 raise ConfigError(_('consistency must be set with an option'))
798             if opt._is_subdyn():
799                 if dynod is None:
800                     raise ConfigError(_('almost one option in consistency is '
801                                         'in a dynoptiondescription but not all'))
802                 if dynod != opt._impl_getsubdyn():
803                     raise ConfigError(_('option in consistency must be in same'
804                                         ' dynoptiondescription'))
805                 dynod = opt._impl_getsubdyn()
806             elif dynod is not None:
807                 raise ConfigError(_('almost one option in consistency is in a '
808                                     'dynoptiondescription but not all'))
809             if self is opt:
810                 raise ConfigError(_('cannot add consistency with itself'))
811             if is_multi != opt.impl_is_multi():
812                 raise ConfigError(_('every options in consistency must be '
813                                     'multi or none'))
814             if init:
815                 # FIXME
816                 if func != 'not_equal':
817                     opt._set_has_dependency()
818
819     def impl_add_consistency(self, func, *other_opts, **params):
820         """Add consistency means that value will be validate with other_opts
821         option's values.
822
823         :param func: function's name
824         :type func: `str`
825         :param other_opts: options used to validate value
826         :type other_opts: `list` of `tiramisu.option.Option`
827         :param params: extra params (warnings_only and transitive are allowed)
828         """
829         if self.impl_is_readonly(): 
830             raise AttributeError(_("'{0}' ({1}) cannot add consistency, option is"
831                                    " read-only").format(
832                                        self.__class__.__name__,
833                                        self.impl_getname()))
834         self._valid_consistencies(other_opts, func=func)
835         func = '_cons_{0}'.format(func)
836         if func not in dir(self):
837             raise ConfigError(_('consistency {0} not available for this option').format(func))
838         all_cons_opts = tuple([self] + list(other_opts))
839         unknown_params = set(params.keys()) - set(['warnings_only', 'transitive'])
840         if unknown_params != set():
841             raise ValueError(_('unknow parameter {0} in consistency').format(unknown_params))
842         self._add_consistency(func, all_cons_opts, params)
843         #validate default value when add consistency
844         err = self.impl_validate(self.impl_getdefault())
845         if err:
846             self._del_consistency()
847             raise err
848         if func in allowed_const_list:
849             for opt in all_cons_opts:
850                 if getattr(opt, '_unique', undefined) == undefined:
851                     opt._unique = True
852         if func != '_cons_not_equal':
853             #consistency could generate warnings or errors
854             self._set_has_dependency()
855
856     def _valid_consistency(self, option, value, context, index, submulti_idx,
857                            display_warnings, display_error):
858         if context is not undefined:
859             descr = context.cfgimpl_get_description()
860             if descr._cache_consistencies is None:
861                 return
862             #consistencies is something like [('_cons_not_equal', (opt1, opt2))]
863             if isinstance(option, DynSymLinkOption):
864                 consistencies = descr._cache_consistencies.get(option._impl_getopt())
865             else:
866                 consistencies = descr._cache_consistencies.get(option)
867         else:
868             consistencies = option._get_consistencies()
869         if consistencies is not None:
870             for func, all_cons_opts, params in consistencies:
871                 warnings_only = params.get('warnings_only', False)
872                 if (warnings_only and display_warnings) or (not warnings_only and display_error):
873                     transitive = params.get('transitive', True)
874                     #all_cons_opts[0] is the option where func is set
875                     if isinstance(option, DynSymLinkOption):
876                         subpath = '.'.join(option._dyn.split('.')[:-1])
877                         namelen = len(option._impl_getopt().impl_getname())
878                         suffix = option.impl_getname()[namelen:]
879                         opts = []
880                         for opt in all_cons_opts:
881                             name = opt.impl_getname() + suffix
882                             path = subpath + '.' + name
883                             opts.append(opt._impl_to_dyn(name, path))
884                     else:
885                         opts = all_cons_opts
886                     err = opts[0]._launch_consistency(self, func, option, value,
887                                                       context, index, submulti_idx,
888                                                       opts, warnings_only,
889                                                       transitive)
890                     if err:
891                         return err
892
893     def _cons_not_equal(self, current_opt, opts, vals, warnings_only):
894         equal = set()
895         is_current = False
896         for idx_inf, val_inf in enumerate(vals):
897             for idx_sup, val_sup in enumerate(vals[idx_inf + 1:]):
898                 if val_inf == val_sup is not None:
899                     for opt_ in [opts[idx_inf], opts[idx_inf + idx_sup + 1]]:
900                         if opt_ == current_opt:
901                             is_current = True
902                         else:
903                             equal.add(opt_)
904         if equal:
905             if debug:  # pragma: no cover
906                 log.debug(_('_cons_not_equal: {} are not different').format(display_list(list(equal))))
907             if is_current:
908                 if warnings_only:
909                     msg = _('should be different from the value of {}')
910                 else:
911                     msg = _('must be different from the value of {}')
912             else:
913                 if warnings_only:
914                     msg = _('value for {} should be different')
915                 else:
916                     msg = _('value for {} must be different')
917             equal_name = []
918             for opt in equal:
919                 equal_name.append(opt.impl_get_display_name())
920             return ValueError(msg.format(display_list(list(equal_name))))
921
922     # serialize/unserialize
923     def _impl_convert_consistencies(self, descr, load=False):
924         """during serialization process, many things have to be done.
925         one of them is the localisation of the options.
926         The paths are set once for all.
927
928         :type descr: :class:`tiramisu.option.OptionDescription`
929         :param load: `True` if we are at the init of the option description
930         :type load: bool
931         """
932         if not load and self._get_consistencies() == ():
933             self._state_consistencies = None
934         elif load and self._state_consistencies is None:
935             del(self._state_consistencies)
936         else:
937             if load:
938                 consistencies = self._state_consistencies
939             else:
940                 consistencies = self._get_consistencies()
941             new_value = []
942             for consistency in consistencies:
943                 values = []
944                 for obj in consistency[1]:
945                     if load:
946                         values.append(descr.impl_get_opt_by_path(obj))
947                     else:
948                         values.append(descr.impl_get_path_by_opt(obj))
949                 new_value.append((consistency[0], tuple(values), consistency[2]))
950             if load:
951                 del(self._state_consistencies)
952                 for new_val in new_value:
953                     self._add_consistency(new_val[0], new_val[1], new_val[2])
954             else:
955                 self._state_consistencies = new_value
956
957     def _second_level_validation(self, value, warnings_only):
958         pass
959
960     def _impl_to_dyn(self, name, path):
961         return DynSymLinkOption(name, self, dyn=path)
962
963     def _validate_callback(self, callback, callback_params):
964         """callback_params:
965         * None
966         * {'': ((option, permissive),), 'ip': ((None,), (option, permissive))
967         """
968         if callback is None:
969             return
970         default_multi = self.impl_getdefault_multi()
971         is_multi = self.impl_is_multi()
972         default = self.impl_getdefault()
973         if (not is_multi and (default is not None or default_multi is not None)) or \
974                 (is_multi and (default != [] or default_multi is not None)):
975             raise ValueError(_("default value not allowed if option: {0} "
976                              "is calculated").format(self.impl_getname()))
977
978
979 def validate_requires_arg(new_option, multi, requires, name):
980     """check malformed requirements
981     and tranform dict to internal tuple
982
983     :param requires: have a look at the
984                      :meth:`tiramisu.setting.Settings.apply_requires` method to
985                      know more about
986                      the description of the requires dictionary
987     """
988     def set_dependency(option):
989         if not getattr(option, '_dependencies', None):
990             options = []
991         else:
992             options = list(option._dependencies)
993         options.append(new_option)
994         option._dependencies = tuple(options)
995
996     def get_option(require):
997         option = require['option']
998         if not isinstance(option, Option):
999             raise ValueError(_('malformed requirements '
1000                                'must be an option in option {0}').format(name))
1001         if not multi and option.impl_is_multi():
1002             raise ValueError(_('malformed requirements '
1003                                'multi option must not set '
1004                                'as requires of non multi option {0}').format(name))
1005         set_dependency(option)
1006         return option
1007
1008     def _set_expected(action, inverse, transitive, same_action, option, expected, operator):
1009         if inverse not in ret_requires[action]:
1010             ret_requires[action][inverse] = ([(option, [expected])], action, inverse, transitive, same_action, operator)
1011         else:
1012             for exp in ret_requires[action][inverse][0]:
1013                 if exp[0] == option:
1014                     exp[1].append(expected)
1015                     break
1016             else:
1017                 ret_requires[action][inverse][0].append((option, [expected]))
1018
1019     def set_expected(require, ret_requires):
1020         expected = require['expected']
1021         inverse = get_inverse(require)
1022         transitive = get_transitive(require)
1023         same_action = get_sameaction(require)
1024         operator = get_operator(require)
1025         if isinstance(expected, list):
1026             for exp in expected:
1027                 if exp.keys() != ['option', 'value']:
1028                     raise ValueError(_('malformed requirements expected must have '
1029                                        'option and value for option {0}').format(name))
1030                 option = exp['option']
1031                 set_dependency(option)
1032                 if option is not None:
1033                     err = option._validate(exp['value'])
1034                     if err:
1035                         raise ValueError(_('malformed requirements expected value '
1036                                            'must be valid for option {0}'
1037                                            ': {1}').format(name, err))
1038                 _set_expected(action, inverse, transitive, same_action, option, exp['value'], operator)
1039         else:
1040             option = get_option(require)
1041             if expected is not None:
1042                 err = option._validate(expected)
1043                 if err:
1044                     raise ValueError(_('malformed requirements expected value '
1045                                        'must be valid for option {0}'
1046                                        ': {1}').format(name, err))
1047             _set_expected(action, inverse, transitive, same_action, option, expected, operator)
1048
1049     def get_action(require):
1050         action = require['action']
1051         if action == 'force_store_value':
1052             raise ValueError(_("malformed requirements for option: {0}"
1053                                " action cannot be force_store_value"
1054                                ).format(name))
1055         return action
1056
1057     def get_inverse(require):
1058         inverse = require.get('inverse', False)
1059         if inverse not in [True, False]:
1060             raise ValueError(_('malformed requirements for option: {0}'
1061                                ' inverse must be boolean'))
1062         return inverse
1063
1064     def get_transitive(require):
1065         transitive = require.get('transitive', True)
1066         if transitive not in [True, False]:
1067             raise ValueError(_('malformed requirements for option: {0}'
1068                                ' transitive must be boolean'))
1069         return transitive
1070
1071     def get_sameaction(require):
1072         same_action = require.get('same_action', True)
1073         if same_action not in [True, False]:
1074             raise ValueError(_('malformed requirements for option: {0}'
1075                                ' same_action must be boolean'))
1076         return same_action
1077
1078     def get_operator(require):
1079         operator = require.get('operator', 'or')
1080         if operator not in ['and', 'or']:
1081             raise ValueError(_('malformed requirements for option: {0}'
1082                                ' operator must be "or" or "and"'))
1083         return operator
1084
1085
1086     ret_requires = {}
1087     config_action = set()
1088
1089     # start parsing all requires given by user (has dict)
1090     # transforme it to a tuple
1091     for require in requires:
1092         if not isinstance(require, dict):
1093             raise ValueError(_("malformed requirements type for option:"
1094                                " {0}, must be a dict").format(name))
1095         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
1096                       'same_action', 'operator')
1097         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
1098         if unknown_keys != frozenset():
1099             raise ValueError(_('malformed requirements for option: {0}'
1100                              ' unknown keys {1}, must only '
1101                              '{2}').format(name,
1102                                            unknown_keys,
1103                                            valid_keys))
1104         # prepare all attributes
1105         if not ('expected' in require and isinstance(require['expected'], list)) and \
1106                 not ('option' in require and 'expected' in require) or \
1107                 'action' not in require:
1108             raise ValueError(_("malformed requirements for option: {0}"
1109                                " require must have option, expected and"
1110                                " action keys").format(name))
1111         action = get_action(require)
1112         config_action.add(action)
1113         if action not in ret_requires:
1114             ret_requires[action] = {}
1115         set_expected(require, ret_requires)
1116
1117     # transform dict to tuple
1118     ret = []
1119     for requires in ret_requires.values():
1120         ret_action = []
1121         for require in requires.values():
1122             ret_action.append((tuple(require[0]), require[1],
1123                                require[2], require[3], require[4], require[5]))
1124         ret.append(tuple(ret_action))
1125     return frozenset(config_action), tuple(ret)
1126
1127
1128 class SymLinkOption(OnlyOption):
1129 #    __slots__ = ('_opt', '_state_opt')
1130
1131     def __init__(self, name, opt):
1132         if not isinstance(opt, Option):
1133             raise ValueError(_('malformed symlinkoption '
1134                                'must be an option '
1135                                'for symlink {0}').format(name))
1136         session = self.getsession()
1137         super(Base, self).__init__(name, undefined, undefined, undefined,
1138                                    undefined, undefined, undefined, undefined,
1139                                    undefined, undefined, opt=opt, session=session)
1140         opt._set_has_dependency()
1141         self.commit(session)
1142
1143     def __getattr__(self, name, context=undefined):
1144         if name in ('_opt', '_readonly', 'impl_getpath', '_name',
1145                     '_state_opt', '_impl_setopt'):
1146             return object.__getattr__(self, name)
1147         else:
1148             return getattr(self._impl_getopt(), name)
1149
1150     def _impl_getstate(self, descr):
1151         self._stated = True
1152         self._state_opt = descr.impl_get_path_by_opt(self._impl_getopt())
1153
1154     def _impl_setstate(self, descr):
1155         self._impl_setopt(descr.impl_get_opt_by_path(self._state_opt))
1156         del(self._state_opt)
1157         del(self._stated)
1158         self._set_readonly(True)
1159
1160     def impl_get_information(self, key, default=undefined):
1161         return self._impl_getopt().impl_get_information(key, default)
1162
1163     def impl_is_readonly(self):
1164         return True
1165
1166     def impl_getproperties(self):
1167         return self._impl_getopt()._properties
1168
1169     def impl_get_callback(self):
1170         return self._impl_getopt().impl_get_callback()
1171
1172     def impl_has_callback(self):
1173         "to know if a callback has been defined or not"
1174         return self._impl_getopt().impl_has_callback()
1175
1176     def impl_is_multi(self):
1177         return self._impl_getopt().impl_is_multi()
1178
1179     def _is_subdyn(self):
1180         return getattr(self._impl_getopt(), '_subdyn', None) is not None
1181
1182     def _get_consistencies(self):
1183         return ()
1184
1185
1186 class DynSymLinkOption(object):
1187     __slots__ = ('_dyn', '_opt', '_name')
1188
1189     def __init__(self, name, opt, dyn):
1190         self._name = name
1191         self._dyn = dyn
1192         self._opt = opt
1193
1194     def __getattr__(self, name, context=undefined):
1195         if name in ('_opt', '_readonly', 'impl_getpath', '_name', '_state_opt'):
1196             return object.__getattr__(self, name)
1197         else:
1198             return getattr(self._impl_getopt(), name)
1199
1200     def impl_getname(self):
1201         return self._name
1202
1203     def impl_get_display_name(self):
1204         return self._impl_getopt().impl_get_display_name(dyn_name=self.impl_getname())
1205
1206     def _impl_getopt(self):
1207         return self._opt
1208
1209     def impl_getsuffix(self):
1210         return self._dyn.split('.')[-1][len(self._impl_getopt().impl_getname()):]
1211
1212     def impl_getpath(self, context):
1213         path = self._impl_getopt().impl_getpath(context)
1214         base_path = '.'.join(path.split('.')[:-2])
1215         if self.impl_is_master_slaves() and base_path is not '':
1216             base_path = base_path + self.impl_getsuffix()
1217         if base_path == '':
1218             return self._dyn
1219         else:
1220             return base_path + '.' + self._dyn
1221
1222     def impl_validate(self, value, context=undefined, validate=True,
1223                       force_index=None, force_submulti_index=None, is_multi=None,
1224                       display_error=True, display_warnings=True, multi=None,
1225                       setting_properties=undefined):
1226         return self._impl_getopt().impl_validate(value, context, validate,
1227                                                  force_index,
1228                                                  force_submulti_index,
1229                                                  current_opt=self,
1230                                                  is_multi=is_multi,
1231                                                  display_error=display_error,
1232                                                  display_warnings=display_warnings,
1233                                                  multi=multi,
1234                                                  setting_properties=setting_properties)
1235
1236     def impl_is_dynsymlinkoption(self):
1237         return True