c22cd5ba38ebd63f72b611087b2c8268fadce22c
[tiramisu.git] / tiramisu / option / baseoption.py
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2014 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 from ..storage import get_storages_option
32 from . import MasterSlaves
33
34
35 if sys.version_info[0] >= 3:  # pragma: optional 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):  # pragma: optional cover
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:  # pragma: optional cover
58         return False
59
60
61 def validate_callback(callback, callback_params, type_):
62     if type(callback) != FunctionType:  # pragma: optional cover
63         raise ValueError(_('{0} must be a function').format(type_))
64     if callback_params is not None:
65         if not isinstance(callback_params, dict):  # pragma: optional cover
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:  # pragma: optional cover
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):  # pragma: optional cover
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 != (None,):  # pragma: optional cover
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:  # pragma: optional cover
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):  # pragma: optional cover
89                             raise ValueError(_('{0}_params must have an option'
90                                                ' not a {0} for first argument'
91                                                ).format(type_, type(option)))
92                         if force_permissive not in [True, False]:  # pragma: optional cover
93                             raise ValueError(_('{0}_params must have a boolean'
94                                                ' not a {0} for second argument'
95                                                ).format(type_, type(
96                                                    force_permissive)))
97 #____________________________________________________________
98 #
99
100
101 class Base(StorageBase):
102     __slots__ = tuple()
103
104     def __init__(self, name, doc, default=None, default_multi=None,
105                  requires=None, multi=False, unique=undefined, callback=None,
106                  callback_params=None, validator=None, validator_params=None,
107                  properties=None, warnings_only=False, extra=None,
108                  allow_empty_list=undefined, session=None):
109         if not valid_name(name):  # pragma: optional cover
110             raise ValueError(_("invalid name: {0} for option").format(name))
111         if not multi and default_multi is not None:  # pragma: optional cover
112             raise ValueError(_("default_multi is set whereas multi is False"
113                              " in option: {0}").format(name))
114         if multi is True:
115             is_multi = True
116             _multi = 0
117         elif multi is False:
118             is_multi = False
119             _multi = 1
120         elif multi is submulti:
121             is_multi = True
122             _multi = submulti
123         else:
124             raise ValueError(_('invalid multi value'))
125         if unique != undefined and not isinstance(unique, bool):
126             raise ValueError(_('unique must be a boolean'))
127         if not is_multi and unique == True:
128             raise ValueError(_('unique must be set only with multi value'))
129         if requires is not None:
130             calc_properties, requires = validate_requires_arg(is_multi,
131                                                               requires, name)
132         else:
133             calc_properties = frozenset()
134             requires = undefined
135         if properties is None:
136             properties = tuple()
137         if not isinstance(properties, tuple):  # pragma: optional cover
138             raise TypeError(_('invalid properties type {0} for {1},'
139                             ' must be a tuple').format(
140                                 type(properties),
141                                 name))
142         if validator is not None:
143             validate_callback(validator, validator_params, 'validator')
144             self._set_validator(validator, validator_params)
145             self._set_has_dependency()
146         if calc_properties != frozenset([]) and properties is not tuple():  # pragma: optional cover
147             set_forbidden_properties = calc_properties & set(properties)
148             if set_forbidden_properties != frozenset():
149                 raise ValueError('conflict: properties already set in '
150                                  'requirement {0}'.format(
151                                      list(set_forbidden_properties)))
152         if session is None:
153             session = self.getsession()
154         StorageBase.__init__(self, name, _multi, warnings_only, doc, extra,
155                              calc_properties, requires, properties,
156                              allow_empty_list, unique, session=session)
157         if multi is not False and default is None:
158             default = []
159         err = self.impl_validate(default, is_multi=is_multi)
160         if err:
161             raise err
162         self._set_default_values(default, default_multi, is_multi)
163         ##callback is False in optiondescription
164         if callback is not False:
165             self.impl_set_callback(callback, callback_params, _init=True)
166         self.commit(session)
167
168     def _set_has_dependency(self):
169         if not isinstance(self, SymLinkOption):
170             self._has_dependency = True
171
172     def impl_has_dependency(self):
173         return getattr(self, '_has_dependency', False)
174
175     def impl_set_callback(self, callback, callback_params=None, _init=False):
176         if callback is None and callback_params is not None:  # pragma: optional cover
177                 raise ValueError(_("params defined for a callback function but "
178                                    "no callback defined"
179                                    " yet for option {0}").format(
180                                        self.impl_getname()))
181         if not _init and self.impl_get_callback()[0] is not None:
182             raise ConfigError(_("a callback is already set for {0}, "
183                                 "cannot set another one's").format(self.impl_getname()))
184         self._validate_callback(callback, callback_params)
185         if callback is not None:
186             validate_callback(callback, callback_params, 'callback')
187             self._set_callback(callback, callback_params)
188
189     def impl_is_optiondescription(self):
190         return self.__class__.__name__ in ['OptionDescription',
191                                            'DynOptionDescription',
192                                            'SynDynOptionDescription']
193
194     def impl_is_dynoptiondescription(self):
195         return self.__class__.__name__ in ['DynOptionDescription',
196                                            'SynDynOptionDescription']
197
198
199 class BaseOption(Base):
200     """This abstract base class stands for attribute access
201     in options that have to be set only once, it is of course done in the
202     __setattr__ method
203     """
204     __slots__ = tuple()
205
206     # ____________________________________________________________
207     # serialize object
208     def _impl_convert_requires(self, descr, load=False):
209         """export of the requires during the serialization process
210
211         :type descr: :class:`tiramisu.option.OptionDescription`
212         :param load: `True` if we are at the init of the option description
213         :type load: bool
214         """
215         if not load and self.impl_getrequires() == []:
216             self._state_requires = None
217         elif load and self._state_requires is None:
218             del(self._state_requires)
219         else:
220             if load:
221                 _requires = self._state_requires
222             else:
223                 _requires = self.impl_getrequires()
224             new_value = []
225             for requires in _requires:
226                 new_requires = []
227                 for require in requires:
228                     if load:
229                         new_require = [descr.impl_get_opt_by_path(require[0])]
230                     else:
231                         new_require = [descr.impl_get_path_by_opt(require[0])]
232                     new_require.extend(require[1:])
233                     new_requires.append(tuple(new_require))
234                 new_value.append(tuple(new_requires))
235             if load:
236                 del(self._state_requires)
237                 if new_value != []:
238                     self._requires = new_value
239             else:
240                 self._state_requires = new_value
241
242     # serialize
243     def _impl_getstate(self, descr):
244         """the under the hood stuff that need to be done
245         before the serialization.
246
247         :param descr: the parent :class:`tiramisu.option.OptionDescription`
248         """
249         self._stated = True
250         for func in dir(self):
251             if func.startswith('_impl_convert_'):
252                 getattr(self, func)(descr)
253
254     def __getstate__(self, stated=True):
255         """special method to enable the serialization with pickle
256         Usualy, a `__getstate__` method does'nt need any parameter,
257         but somme under the hood stuff need to be done before this action
258
259         :parameter stated: if stated is `True`, the serialization protocol
260                            can be performed, not ready yet otherwise
261         :parameter type: bool
262         """
263         try:
264             self._stated
265         except AttributeError:  # pragma: optional cover
266             raise SystemError(_('cannot serialize Option, '
267                                 'only in OptionDescription'))
268         if isinstance(self, SymLinkOption):
269             slots = frozenset(['_name', '_state_opt', '_stated'])
270         else:
271             slots = self._impl_getattributes()
272             slots -= frozenset(['_cache_paths', '_cache_consistencies',
273                                 '__weakref__'])
274         states = {}
275         for slot in slots:
276             # remove variable if save variable converted
277             # in _state_xxxx variable
278             if '_state' + slot not in slots:
279                 try:
280                     if slot.startswith('_state'):
281                         states[slot] = getattr(self, slot)
282                         # remove _state_xxx variable
283                         self.__delattr__(slot)
284                     else:
285                         states[slot] = getattr(self, slot)
286                 except AttributeError:
287                     pass
288         if not stated:
289             del(states['_stated'])
290         return states
291
292     # unserialize
293     def _impl_setstate(self, descr):
294         """the under the hood stuff that need to be done
295         before the serialization.
296
297         :type descr: :class:`tiramisu.option.OptionDescription`
298         """
299         for func in dir(self):
300             if func.startswith('_impl_convert_'):
301                 getattr(self, func)(descr, load=True)
302         try:
303             del(self._stated)
304         except AttributeError:  # pragma: optional cover
305             pass
306
307     def __setstate__(self, state):
308         """special method that enables us to serialize (pickle)
309
310         Usualy, a `__setstate__` method does'nt need any parameter,
311         but somme under the hood stuff need to be done before this action
312
313         :parameter state: a dict is passed to the loads, it is the attributes
314                           of the options object
315         :type state: dict
316         """
317         for key, value in state.items():
318             setattr(self, key, value)
319
320     def __setattr__(self, name, value):
321         """set once and only once some attributes in the option,
322         like `_name`. `_name` cannot be changed one the option and
323         pushed in the :class:`tiramisu.option.OptionDescription`.
324
325         if the attribute `_readonly` is set to `True`, the option is
326         "frozen" (which has noting to do with the high level "freeze"
327         propertie or "read_only" property)
328         """
329         if name != '_option' and \
330                 not isinstance(value, tuple) and \
331                 not name.startswith('_state') and \
332                 not name == '_sa_instance_state':
333             is_readonly = False
334             # never change _name dans _opt
335             if name == '_name':
336                 try:
337                     if self.impl_getname() is not None:
338                         #so _name is already set
339                         is_readonly = True
340                 except (KeyError, AttributeError):
341                     pass
342             elif name == '_opt':
343                 pass
344             elif name != '_readonly':
345                 is_readonly = self.impl_is_readonly()
346             if is_readonly:  # pragma: optional cover
347                 raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
348                                        " read-only").format(
349                                            self.__class__.__name__,
350                                            self,
351                                            #self.impl_getname(),
352                                            name))
353         super(BaseOption, self).__setattr__(name, value)
354
355     def impl_getpath(self, context):
356         return context.cfgimpl_get_description().impl_get_path_by_opt(self)
357
358     def impl_has_callback(self):
359         "to know if a callback has been defined or not"
360         return self.impl_get_callback()[0] is not None
361
362     def _is_subdyn(self):
363         return getattr(self, '_subdyn', None) is not None
364
365     def _impl_valid_unicode(self, value):
366         if sys.version_info[0] >= 3:
367             if not isinstance(value, str):
368                 return ValueError(_('invalid string'))
369         else:
370             if not isinstance(value, unicode) and not isinstance(value, str):
371                 return ValueError(_('invalid unicode or string'))
372
373     def impl_get_display_name(self):
374         name = self.impl_getdoc()
375         if name is None or name == '':
376             name = self.impl_getname()
377         return name
378
379
380 class OnlyOption(BaseOption):
381     __slots__ = tuple()
382
383
384 class Option(OnlyOption):
385     """
386     Abstract base class for configuration option's.
387
388     Reminder: an Option object is **not** a container for the value.
389     """
390     __slots__ = tuple()
391     _empty = ''
392
393     def _launch_consistency(self, current_opt, func, option, value, context,
394                             index, submulti_index, opts, warnings_only,
395                             transitive):
396         """Launch consistency now
397
398         :param func: function name, this name should start with _cons_
399         :type func: `str`
400         :param option: option that value is changing
401         :type option: `tiramisu.option.Option`
402         :param value: new value of this option
403         :param context: Config's context, if None, check default value instead
404         :type context: `tiramisu.config.Config`
405         :param index: only for multi option, consistency should be launch for
406                       specified index
407         :type index: `int`
408         :param opts: all options concerne by this consistency
409         :type opts: `list` of `tiramisu.option.Option`
410         :param warnings_only: specific raise error for warning
411         :type warnings_only: `boolean`
412         :param transitive: propertyerror is transitive
413         :type transitive: `boolean`
414         """
415         if context is not undefined:
416             descr = context.cfgimpl_get_description()
417
418         all_cons_vals = []
419         all_cons_opts = []
420         val_consistencies = True
421         for opt in opts:
422             if (isinstance(opt, DynSymLinkOption) and option._dyn == opt._dyn) or \
423                     option == opt:
424                 # option is current option
425                 # we have already value, so use it
426                 all_cons_vals.append(value)
427                 all_cons_opts.append(opt)
428             else:
429                 #if context, calculate value, otherwise get default value
430                 path = None
431                 is_multi = opt.impl_is_multi() and not opt.impl_is_master_slaves()
432                 if context is not undefined:
433                     if isinstance(opt, DynSymLinkOption):
434                         path = opt.impl_getpath(context)
435                     else:
436                         path = descr.impl_get_path_by_opt(opt)
437                     if is_multi:
438                         _index = None
439                     else:
440                         _index = index
441                     opt_value = context.getattr(path, validate=False,
442                                                 index=_index,
443                                                 force_permissive=True,
444                                                 returns_raise=True)
445                     if isinstance(opt_value, Exception):
446                         if isinstance(opt_value, PropertiesOptionError):
447                             if debug:
448                                 log.debug('propertyerror in _launch_consistency: {0}'.format(opt_value))
449                             if transitive:
450                                 return opt_value
451                             else:
452                                 opt_value = None
453                         else:
454                             return opt_value
455                 elif index is None:
456                     opt_value = opt.impl_getdefault()
457                 else:
458                     opt_value = opt.impl_getdefault()[index]
459
460                 if self.impl_is_multi() and index is None:
461                     # only check propertyerror for master/slaves is transitive
462                     val_consistencies = False
463                 if is_multi and isinstance(opt_value, list):
464                     all_cons_vals.extend(opt_value)
465                     for len_ in xrange(len(opt_value)):
466                         all_cons_opts.append(opt)
467                 else:
468                     all_cons_vals.append(opt_value)
469                     all_cons_opts.append(opt)
470
471         if val_consistencies:
472             return getattr(self, func)(current_opt, all_cons_opts, all_cons_vals, warnings_only)
473
474     def impl_validate(self, value, context=undefined, validate=True,
475                       force_index=None, force_submulti_index=None,
476                       current_opt=undefined, is_multi=None,
477                       display_warnings=True, multi=None):
478         """
479         :param value: the option's value
480         :param context: Config's context
481         :type context: :class:`tiramisu.config.Config`
482         :param validate: if true enables ``self._validator`` validation
483         :type validate: boolean
484         :param force_index: if multi, value has to be a list
485                             not if force_index is not None
486         :type force_index: integer
487         :param force_submulti_index: if submulti, value has to be a list
488                                      not if force_submulti_index is not None
489         :type force_submulti_index: integer
490         """
491         if not validate:
492             return
493         if current_opt is undefined:
494             current_opt = self
495
496         def _is_not_unique(value):
497             if self.impl_is_unique() and len(set(value)) != len(value):
498                 for idx, val in enumerate(value):
499                     if val in value[idx+1:]:
500                         return ValueError(_('invalid value "{}", this value is already in "{}"').format(
501                                             val, self.impl_get_display_name()))
502
503         def calculation_validator(val):
504             validator, validator_params = self.impl_get_validator()
505             if validator is not None:
506                 if validator_params != {}:
507                     validator_params_ = {}
508                     for val_param, values in validator_params.items():
509                         validator_params_[val_param] = values
510                     #inject value in calculation
511                     if '' in validator_params_:
512                         lst = list(validator_params_[''])
513                         lst.insert(0, val)
514                         validator_params_[''] = tuple(lst)
515                     else:
516                         validator_params_[''] = (val,)
517                 else:
518                     validator_params_ = {'': (val,)}
519                 # Raise ValueError if not valid
520                 value = carry_out_calculation(current_opt, context=context,
521                                               callback=validator,
522                                               callback_params=validator_params_)
523                 if isinstance(value, Exception):
524                     return value
525
526         def do_validation(_value, _index, submulti_index):
527             if _value is None:
528                 error = warning = None
529             else:
530                 # option validation
531                 err = self._validate(_value, context, current_opt)
532                 if err:
533                     if debug:
534                         log.debug('do_validation: value: {0}, index: {1}, '
535                                   'submulti_index: {2}'.format(_value, _index,
536                                                                submulti_index),
537                                   exc_info=True)
538                     err_msg = '{0}'.format(err)
539                     name = self.impl_getdoc()
540                     if name is None or name == '':
541                         name = self.impl_getname()
542                     if err_msg:
543                         msg = _('"{0}" is an invalid {1} for "{2}", {3}'
544                                 '').format(_value, self._display_name,
545                                            self.impl_get_display_name(), err_msg)
546                     else:
547                         msg = _('"{0}" is an invalid {1} for "{2}"'
548                                 '').format(_value, self._display_name,
549                                            self.impl_get_display_name())
550                     return ValueError(msg)
551                 warning = None
552                 error = None
553                 if display_warnings:
554                     error = calculation_validator(_value)
555                     if not error:
556                         error = self._second_level_validation(_value, self._is_warnings_only())
557                     if error:
558                         if debug:
559                             log.debug(_('do_validation for {0}: error in value').format(
560                                 self.impl_getname()), exc_info=True)
561                         if self._is_warnings_only():
562                             warning = error
563                             error = None
564             if error is None and warning is None:
565                 # if context launch consistency validation
566                 #if context is not undefined:
567                 ret = self._valid_consistency(current_opt, _value, context,
568                                               _index, submulti_index)
569                 if ret:
570                     if isinstance(ret, ValueWarning):
571                         if display_warnings:
572                             warning = ret
573                     elif isinstance(ret, ValueError):
574                         error = ret
575                     else:
576                         return ret
577             if warning:
578                 msg = _('attention, "{0}" could be an invalid {1} for "{2}", {3}').format(
579                     _value, self._display_name, self.impl_get_display_name(), warning)
580                 if context is undefined or 'warnings' in \
581                         context.cfgimpl_get_settings():
582                     warnings.warn_explicit(ValueWarning(msg, self),
583                                            ValueWarning,
584                                            self.__class__.__name__, 0)
585             elif error:
586                 err_msg = '{0}'.format(error)
587                 if err_msg:
588                     msg = _('"{0}" is an invalid {1} for "{2}", {3}'
589                             '').format(_value, self._display_name,
590                                        self.impl_get_display_name(), err_msg)
591                 else:
592                     msg = _('"{0}" is an invalid {1} for "{2}"'
593                             '').format(_value, self._display_name,
594                                        self.impl_get_display_name())
595                 return ValueError(msg)
596
597         # generic calculation
598         #if context is not undefined:
599         #    descr = context.cfgimpl_get_description()
600
601         if is_multi is None:
602             is_multi = self.impl_is_multi()
603
604         if not is_multi:
605             return do_validation(value, None, None)
606         elif force_index is not None:
607             if self.impl_is_submulti() and force_submulti_index is None:
608                 err = _is_not_unique(value)
609                 if err:
610                     return err
611                 if not isinstance(value, list):  # pragma: optional cover
612                     return ValueError(_('invalid value "{0}" for "{1}" which'
613                                         ' must be a list').format(
614                                            value, self.impl_get_display_name()))
615                 for idx, val in enumerate(value):
616                     if isinstance(val, list):
617                         return ValueError(_('invalid value "{}" for "{}" '
618                                             'which must not be a list'.format(val,
619                                                                               self.impl_get_display_name())))
620                     err = do_validation(val, force_index, idx)
621                     if err:
622                         return err
623             else:
624                 if self.impl_is_unique() and value in multi:
625                     return ValueError(_('invalid value "{}", this value is already'
626                                         ' in "{}"').format(value,
627                                                            self.impl_get_display_name()))
628                 return do_validation(value, force_index, force_submulti_index)
629         elif not isinstance(value, list):  # pragma: optional cover
630             return ValueError(_('invalid value "{0}" for "{1}" which '
631                                 'must be a list').format(value,
632                                                          self.impl_getname()))
633         elif self.impl_is_submulti() and force_submulti_index is None:
634             for idx, val in enumerate(value):
635                 err = _is_not_unique(val)
636                 if err:
637                     return err
638                 if not isinstance(val, list):  # pragma: optional cover
639                     return ValueError(_('invalid value "{0}" for "{1}" '
640                                         'which must be a list of list'
641                                         '').format(val,
642                                                    self.impl_getname()))
643                 for slave_idx, slave_val in enumerate(val):
644                     err = do_validation(slave_val, idx, slave_idx)
645                     if err:
646                         return err
647         else:
648             err = _is_not_unique(value)
649             if err:
650                 return err
651             for idx, val in enumerate(value):
652                 err = do_validation(val, idx, force_submulti_index)
653                 if err:
654                     return err
655             return self._valid_consistency(current_opt, None, context,
656                                            None, None)
657
658     def impl_is_dynsymlinkoption(self):
659         return False
660
661     def impl_is_master_slaves(self, type_='both'):
662         """FIXME
663         """
664         master_slaves = self.impl_get_master_slaves()
665         if master_slaves is not None:
666             if type_ in ('both', 'master') and \
667                     master_slaves.is_master(self):
668                 return True
669             if type_ in ('both', 'slave') and \
670                     not master_slaves.is_master(self):
671                 return True
672         return False
673
674     def impl_get_master_slaves(self):
675         masterslaves = self._get_master_slave()
676         if masterslaves is None:
677             return None
678         if not isinstance(masterslaves, MasterSlaves):
679             return MasterSlaves(masterslaves)
680         return masterslaves
681
682     def impl_getdoc(self):
683         "accesses the Option's doc"
684         return self.impl_get_information('doc')
685
686     def _valid_consistencies(self, other_opts, init=True):
687         if self._is_subdyn():
688             dynod = self._impl_getsubdyn()
689         else:
690             dynod = None
691         if self.impl_is_submulti():
692             raise ConfigError(_('cannot add consistency with submulti option'))
693         is_multi = self.impl_is_multi()
694         for opt in other_opts:
695             if opt.impl_is_submulti():
696                 raise ConfigError(_('cannot add consistency with submulti option'))
697             if not isinstance(opt, Option):  # pragma: optional cover
698                 raise ConfigError(_('consistency must be set with an option'))
699             if opt._is_subdyn():
700                 if dynod is None:
701                     raise ConfigError(_('almost one option in consistency is '
702                                         'in a dynoptiondescription but not all'))
703                 if dynod != opt._impl_getsubdyn():
704                     raise ConfigError(_('option in consistency must be in same'
705                                         ' dynoptiondescription'))
706                 dynod = opt._impl_getsubdyn()
707             elif dynod is not None:
708                 raise ConfigError(_('almost one option in consistency is in a '
709                                     'dynoptiondescription but not all'))
710             if self is opt:  # pragma: optional cover
711                 raise ConfigError(_('cannot add consistency with itself'))
712             if is_multi != opt.impl_is_multi():  # pragma: optional cover
713                 raise ConfigError(_('every options in consistency must be '
714                                     'multi or none'))
715             if init:
716                 #consistency could generate warnings or errors
717                 opt._set_has_dependency()
718
719     def impl_add_consistency(self, func, *other_opts, **params):
720         """Add consistency means that value will be validate with other_opts
721         option's values.
722
723         :param func: function's name
724         :type func: `str`
725         :param other_opts: options used to validate value
726         :type other_opts: `list` of `tiramisu.option.Option`
727         :param params: extra params (warnings_only and transitive are allowed)
728         """
729         if self.impl_is_readonly():  # pragma: optional cover
730             raise AttributeError(_("'{0}' ({1}) cannot add consistency, option is"
731                                    " read-only").format(
732                                        self.__class__.__name__,
733                                        self.impl_getname()))
734         self._valid_consistencies(other_opts)
735         func = '_cons_{0}'.format(func)
736         if func not in dir(self):
737             raise ConfigError(_('consistency {0} not available for this option').format(func))
738         all_cons_opts = tuple([self] + list(other_opts))
739         unknown_params = set(params.keys()) - set(['warnings_only', 'transitive'])
740         if unknown_params != set():
741             raise ValueError(_('unknow parameter {0} in consistency').format(unknown_params))
742         self._add_consistency(func, all_cons_opts, params)
743         #validate default value when add consistency
744         err = self.impl_validate(self.impl_getdefault())
745         if err:
746             self._del_consistency()
747             raise err
748         if func in allowed_const_list:
749             for opt in all_cons_opts:
750                 if getattr(opt, '_unique', undefined) == undefined:
751                     opt._unique = True
752         #consistency could generate warnings or errors
753         self._set_has_dependency()
754
755     def _valid_consistency(self, option, value, context, index, submulti_idx):
756         if context is not undefined:
757             descr = context.cfgimpl_get_description()
758             if descr._cache_consistencies is None:
759                 return
760             #consistencies is something like [('_cons_not_equal', (opt1, opt2))]
761             if isinstance(option, DynSymLinkOption):
762                 consistencies = descr._cache_consistencies.get(option._impl_getopt())
763             else:
764                 consistencies = descr._cache_consistencies.get(option)
765         else:
766             consistencies = option._get_consistencies()
767         if consistencies is not None:
768             for func, all_cons_opts, params in consistencies:
769                 warnings_only = params.get('warnings_only', False)
770                 transitive = params.get('transitive', True)
771                 #all_cons_opts[0] is the option where func is set
772                 if isinstance(option, DynSymLinkOption):
773                     subpath = '.'.join(option._dyn.split('.')[:-1])
774                     namelen = len(option._impl_getopt().impl_getname())
775                     suffix = option.impl_getname()[namelen:]
776                     opts = []
777                     for opt in all_cons_opts:
778                         name = opt.impl_getname() + suffix
779                         path = subpath + '.' + name
780                         opts.append(opt._impl_to_dyn(name, path))
781                 else:
782                     opts = all_cons_opts
783                 err = opts[0]._launch_consistency(self, func, option, value,
784                                                   context, index, submulti_idx,
785                                                   opts, warnings_only,
786                                                   transitive)
787                 if err:
788                     if warnings_only:
789                         return ValueWarning(str(err), option)
790                     else:
791                         return err
792
793     def _cons_not_equal(self, current_opt, opts, vals, warnings_only):
794         equal = set()
795         is_current = False
796         for idx_inf, val_inf in enumerate(vals):
797             for idx_sup, val_sup in enumerate(vals[idx_inf + 1:]):
798                 if val_inf == val_sup is not None:
799                     for opt_ in [opts[idx_inf], opts[idx_inf + idx_sup + 1]]:
800                         if opt_ == current_opt:
801                             is_current = True
802                         equal.add('"{}"'.format(opt_.impl_get_display_name()))
803         if equal:
804             if debug:
805                 log.debug(_('_cons_not_equal: {} are not different').format(display_list(list(equal))))
806             if is_current:
807                 equal.remove('"' + current_opt.impl_get_display_name() + '"')
808                 if len(equal) == 0:
809                     msg = _('this value is already present')
810                     return ValueError(msg)
811                 else:
812                     if warnings_only:
813                         msg = _('should be different from the value of {}')
814                     else:
815                         msg = _('must be different from the value of {}')
816                     return ValueError(msg.format(display_list(list(equal))))
817             else:
818                 if warnings_only:
819                     msg = _('value for {} should be different')
820                 else:
821                     msg = _('value for {} must be different')
822                 return ValueError(msg.format(display_list(list(equal))))
823
824     # serialize/unserialize
825     def _impl_convert_consistencies(self, descr, load=False):
826         """during serialization process, many things have to be done.
827         one of them is the localisation of the options.
828         The paths are set once for all.
829
830         :type descr: :class:`tiramisu.option.OptionDescription`
831         :param load: `True` if we are at the init of the option description
832         :type load: bool
833         """
834         if not load and self._get_consistencies() == ():
835             self._state_consistencies = None
836         elif load and self._state_consistencies is None:
837             del(self._state_consistencies)
838         else:
839             if load:
840                 consistencies = self._state_consistencies
841             else:
842                 consistencies = self._get_consistencies()
843             new_value = []
844             for consistency in consistencies:
845                 values = []
846                 for obj in consistency[1]:
847                     if load:
848                         values.append(descr.impl_get_opt_by_path(obj))
849                     else:
850                         values.append(descr.impl_get_path_by_opt(obj))
851                 new_value.append((consistency[0], tuple(values), consistency[2]))
852             if load:
853                 del(self._state_consistencies)
854                 for new_val in new_value:
855                     self._add_consistency(new_val[0], new_val[1], new_val[2])
856             else:
857                 self._state_consistencies = new_value
858
859     def _second_level_validation(self, value, warnings_only):
860         pass
861
862     def _impl_to_dyn(self, name, path):
863         return DynSymLinkOption(name, self, dyn=path)
864
865     def _validate_callback(self, callback, callback_params):
866         """callback_params:
867         * None
868         * {'': ((option, permissive),), 'ip': ((None,), (option, permissive))
869         """
870         if callback is None:
871             return
872         default_multi = self.impl_getdefault_multi()
873         is_multi = self.impl_is_multi()
874         default = self.impl_getdefault()
875         if (not is_multi and (default is not None or default_multi is not None)) or \
876                 (is_multi and (default != [] or default_multi is not None)):  # pragma: optional cover
877             raise ValueError(_("default value not allowed if option: {0} "
878                              "is calculated").format(self.impl_getname()))
879
880
881 def validate_requires_arg(multi, requires, name):
882     """check malformed requirements
883     and tranform dict to internal tuple
884
885     :param requires: have a look at the
886                      :meth:`tiramisu.setting.Settings.apply_requires` method to
887                      know more about
888                      the description of the requires dictionary
889     """
890     ret_requires = {}
891     config_action = {}
892
893     # start parsing all requires given by user (has dict)
894     # transforme it to a tuple
895     for require in requires:
896         if not isinstance(require, dict):  # pragma: optional cover
897             raise ValueError(_("malformed requirements type for option:"
898                                " {0}, must be a dict").format(name))
899         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
900                       'same_action')
901         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
902         if unknown_keys != frozenset():  # pragma: optional cover
903             raise ValueError(_('malformed requirements for option: {0}'
904                              ' unknown keys {1}, must only '
905                              '{2}').format(name,
906                                            unknown_keys,
907                                            valid_keys))
908         # prepare all attributes
909         if 'option' not in require or 'expected' not in require or \
910                 'action' not in require:
911             raise ValueError(_("malformed requirements for option: {0}"
912                                " require must have option, expected and"
913                                " action keys").format(name))
914         option = require['option']
915         expected = require['expected']
916         action = require['action']
917         if action == 'force_store_value':  # pragma: optional cover
918             raise ValueError(_("malformed requirements for option: {0}"
919                                " action cannot be force_store_value"
920                                ).format(name))
921         inverse = require.get('inverse', False)
922         if inverse not in [True, False]:  # pragma: optional cover
923             raise ValueError(_('malformed requirements for option: {0}'
924                                ' inverse must be boolean'))
925         transitive = require.get('transitive', True)
926         if transitive not in [True, False]:  # pragma: optional cover
927             raise ValueError(_('malformed requirements for option: {0}'
928                                ' transitive must be boolean'))
929         same_action = require.get('same_action', True)
930         if same_action not in [True, False]:  # pragma: optional cover
931             raise ValueError(_('malformed requirements for option: {0}'
932                                ' same_action must be boolean'))
933
934         if not isinstance(option, Option):  # pragma: optional cover
935             raise ValueError(_('malformed requirements '
936                                'must be an option in option {0}').format(name))
937         if not multi and option.impl_is_multi():
938             raise ValueError(_('malformed requirements '
939                                'multi option must not set '
940                                'as requires of non multi option {0}').format(name))
941         if expected is not None:
942             err = option._validate(expected)
943             if err:
944                 raise ValueError(_('malformed requirements second argument '
945                                    'must be valid for option {0}'
946                                    ': {1}').format(name, err))
947         if action in config_action:
948             if inverse != config_action[action]:  # pragma: optional cover
949                 raise ValueError(_("inconsistency in action types"
950                                    " for option: {0}"
951                                    " action: {1}").format(name, action))
952         else:
953             config_action[action] = inverse
954         if action not in ret_requires:
955             ret_requires[action] = {}
956         if option not in ret_requires[action]:
957             ret_requires[action][option] = (option, [expected], action,
958                                             inverse, transitive, same_action)
959         else:
960             ret_requires[action][option][1].append(expected)
961     # transform dict to tuple
962     ret = []
963     for opt_requires in ret_requires.values():
964         ret_action = []
965         for require in opt_requires.values():
966             ret_action.append((require[0], tuple(require[1]), require[2],
967                                require[3], require[4], require[5]))
968         ret.append(tuple(ret_action))
969     return frozenset(config_action.keys()), tuple(ret)
970
971
972 class SymLinkOption(OnlyOption):
973 #    __slots__ = ('_opt', '_state_opt')
974
975     def __init__(self, name, opt):
976         if not isinstance(opt, Option):  # pragma: optional cover
977             raise ValueError(_('malformed symlinkoption '
978                                'must be an option '
979                                'for symlink {0}').format(name))
980         session = self.getsession()
981         super(Base, self).__init__(name, undefined, undefined, undefined,
982                                    undefined, undefined, undefined, undefined,
983                                    undefined, undefined, opt=opt, session=session)
984         opt._set_has_dependency()
985         self.commit(session)
986
987     def __getattr__(self, name, context=undefined):
988         if name in ('_opt', '_readonly', 'impl_getpath', '_name',
989                     '_state_opt', '_impl_setopt'):
990             return object.__getattr__(self, name)
991         else:
992             return getattr(self._impl_getopt(), name)
993
994     def _impl_getstate(self, descr):
995         self._stated = True
996         self._state_opt = descr.impl_get_path_by_opt(self._impl_getopt())
997
998     def _impl_setstate(self, descr):
999         self._impl_setopt(descr.impl_get_opt_by_path(self._state_opt))
1000         del(self._state_opt)
1001         del(self._stated)
1002         self._set_readonly(True)
1003
1004     def impl_get_information(self, key, default=undefined):
1005         return self._impl_getopt().impl_get_information(key, default)
1006
1007     def impl_is_readonly(self):
1008         return True
1009
1010     def impl_getproperties(self):
1011         return self._impl_getopt()._properties
1012
1013     def impl_get_callback(self):
1014         return self._impl_getopt().impl_get_callback()
1015
1016     def impl_has_callback(self):
1017         "to know if a callback has been defined or not"
1018         return self._impl_getopt().impl_has_callback()
1019
1020     def impl_is_multi(self):
1021         return self._impl_getopt().impl_is_multi()
1022
1023     def _is_subdyn(self):
1024         return getattr(self._impl_getopt(), '_subdyn', None) is not None
1025
1026     def _get_consistencies(self):
1027         return ()
1028
1029
1030 class DynSymLinkOption(object):
1031     __slots__ = ('_dyn', '_opt', '_name')
1032
1033     def __init__(self, name, opt, dyn):
1034         self._name = name
1035         self._dyn = dyn
1036         self._opt = opt
1037
1038     def __getattr__(self, name, context=undefined):
1039         if name in ('_opt', '_readonly', 'impl_getpath', '_name', '_state_opt'):
1040             return object.__getattr__(self, name)
1041         else:
1042             return getattr(self._impl_getopt(), name)
1043
1044     def impl_getname(self):
1045         return self._name
1046
1047     def _impl_getopt(self):
1048         return self._opt
1049
1050     def impl_getsuffix(self):
1051         return self._dyn.split('.')[-1][len(self._impl_getopt().impl_getname()):]
1052
1053     def impl_getpath(self, context):
1054         path = self._impl_getopt().impl_getpath(context)
1055         base_path = '.'.join(path.split('.')[:-2])
1056         if self.impl_is_master_slaves() and base_path is not '':
1057             base_path = base_path + self.impl_getsuffix()
1058         if base_path == '':
1059             return self._dyn
1060         else:
1061             return base_path + '.' + self._dyn
1062
1063     def impl_validate(self, value, context=undefined, validate=True,
1064                       force_index=None, force_submulti_index=None, is_multi=None,
1065                       display_warnings=True, multi=None):
1066         return self._impl_getopt().impl_validate(value, context, validate,
1067                                                  force_index,
1068                                                  force_submulti_index,
1069                                                  current_opt=self,
1070                                                  is_multi=is_multi,
1071                                                  display_warnings=display_warnings,
1072                                                  multi=multi)
1073
1074     def impl_is_dynsymlinkoption(self):
1075         return True