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